5674 adds ability to view truncated text in notification toast as a dialog

This commit is contained in:
Seyi Adebajo 2018-05-21 16:31:20 -07:00
parent 4cf03ed9e2
commit b47317dcd2
7 changed files with 160 additions and 38 deletions

View File

@ -66,6 +66,7 @@ import { emptyRegexSource } from 'wherehows-web/utils/validators/regexp';
import { NonIdLogicalType } from 'wherehows-web/constants/datasets/compliance'; import { NonIdLogicalType } from 'wherehows-web/constants/datasets/compliance';
import { pick } from 'lodash'; import { pick } from 'lodash';
import { trackableEvent, TrackableEventCategory } from 'wherehows-web/constants/analytics/event-tracking'; import { trackableEvent, TrackableEventCategory } from 'wherehows-web/constants/analytics/event-tracking';
import { notificationDialogActionFactory } from 'wherehows-web/utils/notifications/notifications';
const { const {
complianceDataException, complianceDataException,
@ -860,7 +861,7 @@ export default class DatasetCompliance extends Component {
* @return {Promise<void>} * @return {Promise<void>}
*/ */
showPurgeExemptionWarning(this: DatasetCompliance): Promise<void> { showPurgeExemptionWarning(this: DatasetCompliance): Promise<void> {
const dialogActions = <IConfirmOptions['dialogActions']>{}; const { dialogActions, dismissedOrConfirmed } = notificationDialogActionFactory();
get(this, 'notifications').notify(NotificationEvent.confirm, { get(this, 'notifications').notify(NotificationEvent.confirm, {
header: 'Confirm purge exemption', header: 'Confirm purge exemption',
@ -869,10 +870,7 @@ export default class DatasetCompliance extends Component {
dialogActions dialogActions
}); });
return new Promise((resolve, reject): void => { return dismissedOrConfirmed;
dialogActions['didConfirm'] = (): void => resolve();
dialogActions['didDismiss'] = (): void => reject();
});
} }
/** /**
@ -1127,9 +1125,9 @@ export default class DatasetCompliance extends Component {
/** /**
* Handles post processing tasks after the purge policy step has been completed * Handles post processing tasks after the purge policy step has been completed
* @returns {(Promise<void | {}>)} * @returns {(Promise<void>)}
*/ */
didEditPurgePolicy(this: DatasetCompliance): Promise<void | {}> { didEditPurgePolicy(this: DatasetCompliance): Promise<void> {
const { complianceType = null } = get(this, 'complianceInfo') || {}; const { complianceType = null } = get(this, 'complianceInfo') || {};
if (!complianceType) { if (!complianceType) {

View File

@ -2,6 +2,8 @@ import Service from '@ember/service';
import { setProperties, get, set } from '@ember/object'; import { setProperties, get, set } from '@ember/object';
import { delay } from 'wherehows-web/utils/promise-delay'; import { delay } from 'wherehows-web/utils/promise-delay';
import { action } from '@ember-decorators/object'; import { action } from '@ember-decorators/object';
import { fleece } from 'wherehows-web/utils/object';
import { notificationDialogActionFactory } from 'wherehows-web/utils/notifications/notifications';
/** /**
* Flag indicating the current notification queue is being processed * Flag indicating the current notification queue is being processed
@ -10,7 +12,7 @@ import { action } from '@ember-decorators/object';
let isBuffering = false; let isBuffering = false;
/** /**
* String literal of available notifications * String enum of available notifications
*/ */
export enum NotificationEvent { export enum NotificationEvent {
success = 'success', success = 'success',
@ -20,9 +22,12 @@ export enum NotificationEvent {
} }
/** /**
* String literal for notification types * String enum of notification types
*/ */
type NotificationType = 'modal' | 'toast'; enum NotificationType {
Modal = 'modal',
Toast = 'toast'
}
/** /**
* Describes the proxy handler for INotifications * Describes the proxy handler for INotifications
@ -37,10 +42,15 @@ interface INotificationHandlerTrap<T, K extends keyof T> {
* @interface IConfirmOptions * @interface IConfirmOptions
*/ */
export interface IConfirmOptions { export interface IConfirmOptions {
// Header text for the confirmation dialog
header: string; header: string;
// Content to be displayed in the confirmation dialog
content: string; content: string;
dismissButtonText?: string; // Text for button to dismiss dialog action, if false, button will not be rendered
confirmButtonText?: string; dismissButtonText?: string | false;
// Text for button to confirm dialog action, if false, button will not be rendered
confirmButtonText?: string | false;
// Action handlers for dialog button on dismissal or otherwise
dialogActions: { dialogActions: {
didConfirm: () => any; didConfirm: () => any;
didDismiss: () => any; didDismiss: () => any;
@ -64,7 +74,7 @@ interface IToast {
interface INotification { interface INotification {
// The properties for the notification // The properties for the notification
props: IConfirmOptions | IToast; props: IConfirmOptions | IToast;
// The type if the notification // The type of the notification
type: NotificationType; type: NotificationType;
// Object holding the queue state for the notification // Object holding the queue state for the notification
notificationResolution: INotificationResolver; notificationResolution: INotificationResolver;
@ -119,7 +129,7 @@ const makeToast = (props: IToast): INotification => {
return { return {
props, props,
type: 'toast', type: NotificationType.Toast,
notificationResolution notificationResolution
}; };
}; };
@ -131,18 +141,27 @@ const notificationHandlers: INotificationHandler = {
* @return {INotification} * @return {INotification}
*/ */
confirm(props: IConfirmOptions): INotification { confirm(props: IConfirmOptions): INotification {
let notificationResolution: INotificationResolver = { const notificationResolution: INotificationResolver = {
get queueAwaiter() { get queueAwaiter() {
return createNotificationAwaiter(this); return createNotificationAwaiter(this);
} }
}; };
// Set default values for button text if none are provided by consumer // Set default values for button text if none are provided by consumer
props = { dismissButtonText: 'No', confirmButtonText: 'Yes', ...props }; props = { dismissButtonText: 'No', confirmButtonText: 'Yes', ...props };
const { dismissButtonText, confirmButtonText } = props;
// Removes dismiss or confirm buttons if set to false
let resolvedProps: IConfirmOptions =
dismissButtonText === false
? <IConfirmOptions>fleece<IConfirmOptions, 'dismissButtonText'>(['dismissButtonText'])(props)
: props;
resolvedProps =
confirmButtonText === false
? <IConfirmOptions>fleece<IConfirmOptions, 'confirmButtonText'>(['confirmButtonText'])(props)
: props;
return { return {
props, props: resolvedProps,
type: 'modal', type: NotificationType.Modal,
notificationResolution notificationResolution
}; };
}, },
@ -223,7 +242,7 @@ const asyncDequeue = function(notificationsQueue: Array<INotification>) {
* @param {INotification} notification * @param {INotification} notification
* @param {Array<INotification>} notificationsQueue * @param {Array<INotification>} notificationsQueue
*/ */
const enqueue = (notification: INotification, notificationsQueue: Array<INotification>) => { const enqueue = (notification: INotification, notificationsQueue: Array<INotification>): void => {
notificationsQueue.unshift(notification); notificationsQueue.unshift(notification);
asyncDequeue(notificationsQueue); asyncDequeue(notificationsQueue);
}; };
@ -263,12 +282,14 @@ export default class Notifications extends Service {
* @param {INotification} notification * @param {INotification} notification
*/ */
setCurrentNotification = async (notification: INotification) => { setCurrentNotification = async (notification: INotification) => {
if (notification.type === 'modal') { const { type, props } = notification;
setProperties<Notifications, 'modal' | 'isShowingModal'>(this, { modal: notification, isShowingModal: true });
if (type === NotificationType.Modal) {
setProperties(this, { modal: notification, isShowingModal: true });
} else { } else {
const { props } = notification;
const toastDelay = delay((<IToast>props).duration); const toastDelay = delay((<IToast>props).duration);
setProperties<Notifications, 'toast' | 'isShowingToast'>(this, { toast: notification, isShowingToast: true });
setProperties(this, { toast: notification, isShowingToast: true });
if (!(<IToast>props).isSticky) { if (!(<IToast>props).isSticky) {
await toastDelay; await toastDelay;
@ -297,21 +318,24 @@ export default class Notifications extends Service {
/** /**
* Removes the current toast from view and invokes the notification resolution resolver * Removes the current toast from view and invokes the notification resolution resolver
* @memberof Notifications
*/ */
@action @action
dismissToast(this: Notifications) { dismissToast() {
const { const {
notificationResolution: { onComplete } notificationResolution: { onComplete }
}: INotification = get(this, 'toast'); }: INotification = get(this, 'toast');
set(this, 'isShowingToast', false); set(this, 'isShowingToast', false);
onComplete && onComplete(); onComplete && onComplete();
} }
/** /**
* Ignores the modal, invokes the user supplied didDismiss callback * Ignores the modal, invokes the user supplied didDismiss callback
* @memberof Notifications
*/ */
@action @action
dismissModal(this: Notifications) { dismissModal() {
const { const {
props, props,
notificationResolution: { onComplete } notificationResolution: { onComplete }
@ -327,13 +351,15 @@ export default class Notifications extends Service {
/** /**
* Confirms the dialog and invokes the user supplied didConfirm callback * Confirms the dialog and invokes the user supplied didConfirm callback
* @memberof Notifications
*/ */
@action @action
confirmModal(this: Notifications) { confirmModal() {
const { const {
props, props,
notificationResolution: { onComplete } notificationResolution: { onComplete }
}: INotification = get(this, 'modal'); }: INotification = get(this, 'modal');
if ((<IConfirmOptions>props).dialogActions) { if ((<IConfirmOptions>props).dialogActions) {
const { didConfirm } = (<IConfirmOptions>props).dialogActions; const { didConfirm } = (<IConfirmOptions>props).dialogActions;
set(this, 'isShowingModal', false); set(this, 'isShowingModal', false);
@ -341,4 +367,27 @@ export default class Notifications extends Service {
onComplete && onComplete(); onComplete && onComplete();
} }
} }
/**
* Renders a dialog with the full text of the last IToast instance content,
* with the option to dismiss the modal
* @memberof Notifications
*/
@action
async showContentDetail() {
const { dialogActions } = notificationDialogActionFactory();
const {
props: { content }
} = get(this, 'toast');
this.notify(NotificationEvent.confirm, {
header: 'Notification Detail',
content,
dialogActions,
dismissButtonText: false,
confirmButtonText: 'Dismiss'
});
this.dismissToast.call(this);
}
} }

View File

@ -2,6 +2,9 @@
/// Contains the states --info, --success, --error /// Contains the states --info, --success, --error
/// Also includes a __dismiss element when applicable i.e.shown /// Also includes a __dismiss element when applicable i.e.shown
.notification-toast { .notification-toast {
$right-index: 7;
$right-region-width: item-spacing($right-index);
position: fixed; position: fixed;
text-align: left; text-align: left;
border-radius: 2px; border-radius: 2px;
@ -13,14 +16,14 @@
background: set-color(white, base); background: set-color(white, base);
padding: 0; padding: 0;
overflow: hidden; overflow: hidden;
height: 96px; height: item-spacing(5) * 5;
box-shadow: 0 0 0 1px rgba(0, 0, 0, .1), 0 6px 9px rgba(0, 0, 0, .2); box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1), 0 6px 9px rgba(0, 0, 0, 0.2);
margin-top: item-spacing(3); margin-top: item-spacing(3);
&__content { &__content {
overflow: hidden; overflow: hidden;
height: 100%; height: 100%;
padding: item-spacing(4 7 4 8); padding: item-spacing(5 $right-index 5 8);
&::before { &::before {
content: ''; content: '';
@ -32,6 +35,12 @@
z-index: 2; z-index: 2;
} }
&__msg {
overflow: hidden;
height: 100%;
margin: 0;
}
&--info { &--info {
&::before { &::before {
background-color: set-color(grey, light); background-color: set-color(grey, light);
@ -61,7 +70,7 @@
top: 0; top: 0;
padding: 0; padding: 0;
z-index: 1; z-index: 1;
width: item-spacing(7); width: $right-region-width;
font-size: item-spacing(6); font-size: item-spacing(6);
cursor: pointer; cursor: pointer;
@ -69,4 +78,12 @@
background-color: set-color(grey, light); background-color: set-color(grey, light);
} }
} }
&__content-detail {
&#{&} {
position: absolute;
bottom: item-spacing(1);
right: $right-region-width;
}
}
} }

View File

@ -19,12 +19,16 @@
</section> </section>
<footer class="notification-confirm-modal__footer"> <footer class="notification-confirm-modal__footer">
<button class="nacho-button--large nacho-button--secondary" {{action "onClose"}}> {{#unless (eq dismissButtonText false)}}
{{dismissButtonText}} <button class="nacho-button--large nacho-button--secondary" {{action "onClose"}}>
</button> {{dismissButtonText}}
</button>
{{/unless}}
<button class="nacho-button nacho-button--large-inverse" {{action "onConfirm"}}> {{#unless (eq confirmButtonText false)}}
{{confirmButtonText}} <button class="nacho-button nacho-button--large-inverse" {{action "onConfirm"}}>
</button> {{confirmButtonText}}
</button>
{{/unless}}
</footer> </footer>
{{/modal-dialog}} {{/modal-dialog}}

View File

@ -21,7 +21,17 @@
<i class="notification-toast__icon"></i> <i class="notification-toast__icon"></i>
<div class="notification-toast__content notification-toast__content--{{service.toast.props.type}}"> <div class="notification-toast__content notification-toast__content--{{service.toast.props.type}}">
<p>{{service.toast.props.content}}</p> <p class="notification-toast__content__msg">
{{split-text service.toast.props.content 140}}
</p>
{{#if (gt service.toast.props.content.length 140)}}
<button
class="nacho-button nacho-button--tertiary notification-toast__content-detail"
onclick={{action "showContentDetail" target=service}}>
See Detail
</button>
{{/if}}
</div> </div>
<button class="notification-toast__dismiss" onclick={{action "dismissToast" target=service}}> <button class="notification-toast__dismiss" onclick={{action "dismissToast" target=service}}>

View File

@ -13,7 +13,7 @@ import { Classification, ComplianceFieldIdValue, IdLogicalType } from 'wherehows
*/ */
interface IDatasetComplianceActions { interface IDatasetComplianceActions {
didEditCompliancePolicy: () => Promise<void>; didEditCompliancePolicy: () => Promise<void>;
didEditPurgePolicy: () => Promise<{} | void>; didEditPurgePolicy: () => Promise<void>;
didEditDatasetLevelCompliancePolicy: () => Promise<void>; didEditDatasetLevelCompliancePolicy: () => Promise<void>;
[K: string]: (...args: Array<any>) => any; [K: string]: (...args: Array<any>) => any;
} }

View File

@ -0,0 +1,44 @@
import { IConfirmOptions } from 'wherehows-web/services/notifications';
/**
* Defines the interface for properties used in proxy-ing or handling dialog's
* events async
* @interface INotificationDialogProps
*/
interface INotificationDialogProps {
dismissedOrConfirmed: Promise<void>;
dialogActions: IConfirmOptions['dialogActions'];
}
/**
* Creates an instance of INotificationDialogProps when invoked. Includes a promise for dismissing or
* confirming the dialog
* @returns {INotificationDialogProps}
*/
const notificationDialogActionFactory = (): INotificationDialogProps => {
let dialogActions = <IConfirmOptions['dialogActions']>{};
/**
* Inner function to create a dialog object
* @template T
* @param {((value?: T | PromiseLike<T>) => void)} resolveFn
* @param {(reason?: any) => void} rejectFn
* @returns {IConfirmOptions.dialogActions}
*/
const createDialogActions = <T>(
resolveFn: (value?: T | PromiseLike<T>) => void,
rejectFn: (reason?: any) => void
): IConfirmOptions['dialogActions'] => ({
didConfirm: () => resolveFn(),
didDismiss: () => rejectFn()
});
const dismissedOrConfirmed = new Promise<void>((resolve, reject): void => {
dialogActions = { ...dialogActions, ...createDialogActions(resolve, reject) };
});
return {
dismissedOrConfirmed,
dialogActions
};
};
export { notificationDialogActionFactory };