(core) Adding colors to toast notification

Summary:
Styling toast notification. Adding colors and icons.
In Grist, changed the default style for errors (will be shown in red), and a style for
Linked copied to clipboard (will be shown in Green).
All other colors are not used currently, left for another diff.

Test Plan: manual

Reviewers: georgegevoian

Reviewed By: georgegevoian

Differential Revision: https://phab.getgrist.com/D3053
This commit is contained in:
Jarosław Sadziński
2021-10-01 21:38:58 +02:00
parent 43a62e7254
commit 40ddb57dfc
12 changed files with 203 additions and 22 deletions

View File

@@ -17,7 +17,7 @@ var commands = require('./commands');
var BackboneEvents = require('backbone').Events;
const {LinkingState} = require('./LinkingState');
const {ClientColumnGetters} = require('app/client/models/ClientColumnGetters');
const {reportError, UserError} = require('app/client/models/errors');
const {reportError, reportSuccess} = require('app/client/models/errors');
const {urlState} = require('app/client/models/gristUrlState');
const {SectionFilter} = require('app/client/models/SectionFilter');
const {copyToClipboard} = require('app/client/lib/copyToClipboard');
@@ -318,7 +318,7 @@ BaseView.prototype.copyLink = async function() {
const link = urlState().makeUrl({ hash: { sectionId, rowId, colRef } });
await copyToClipboard(link);
setTestState({clipboard: link});
reportError(new UserError('Link copied to clipboard', {key: 'clipboard'}));
reportSuccess('Link copied to clipboard', {key: 'clipboard'});
} catch (e) {
throw new Error('cannot copy to clipboard');
}

View File

@@ -270,7 +270,7 @@ class NotificationAdapter extends Disposable implements Notification {
}
public showUndoDiscard() {
const notifier = this._doc.app.topAppModel.notifier;
const notification = notifier.createUserError("Undo discard", {
const notification = notifier.createUserMessage("Undo discard", {
message: () =>
discardNotification(
dom.on("click", () => {

View File

@@ -139,7 +139,7 @@ export class TopAppModelImpl extends Disposable implements TopAppModel {
}
if (org.billingAccount && org.billingAccount.product &&
org.billingAccount.product.name === 'suspended') {
this.notifier.createUserError(
this.notifier.createUserMessage(
'This team site is suspended. Documents can be read, but not modified.',
{actions: ['renew', 'personal']}
);

View File

@@ -17,6 +17,7 @@ interface INotifier {
// If you are looking to report errors, please do that via reportError rather
// than these methods so that we have a chance to send the error to our logs.
createUserError(message: string, options?: INotifyOptions): INotification;
createUserMessage(message: string, options?: INotifyOptions): INotification;
createAppError(error: Error): void;
createProgressIndicator(name: string, size: string, expireOnComplete: boolean): IProgress;
@@ -46,6 +47,7 @@ export interface INotifyOptions {
inDropdown?: boolean;
expireSec?: number;
badgeCounter?: boolean;
level: 'message' | 'info' | 'success' | 'warning' | 'error';
memos?: string[]; // A list of relevant notes.
@@ -93,6 +95,7 @@ export class Notification extends Expirable implements INotification {
actions: [],
memos: [],
key: null,
level: 'message'
};
constructor(_opts: INotifyOptions) {
@@ -196,6 +199,7 @@ export class Notifier extends Disposable implements INotifier {
title: msg.title,
canUserClose: true,
inToast: true,
level : 'message'
})) : null);
}
@@ -222,6 +226,21 @@ export class Notifier extends Disposable implements INotifier {
* that we have a chance to send the error to our logs.
*/
public createUserError(message: string, options: Partial<INotifyOptions> = {}): INotification {
return this.createUserMessage(message, {
level: 'error',
...options
});
}
/**
* Creates a basic toast notification. By default, expires in 10 seconds.
* Takes an options objects to configure `expireSec` and `canUserClose`.
* Set `expireSec` to 0 to prevent expiration.
*
* Additional option level, can be used to style the notification to like a success, warning,
* info or error message.
*/
public createUserMessage(message: string, options: Partial<INotifyOptions> = {}): INotification {
const timestamp = Date.now();
if (options.actions && options.actions.includes('ask-for-help')) {
// If user should be able to ask for help, add this error to the notifier dropdown too for a
@@ -232,6 +251,7 @@ export class Notifier extends Disposable implements INotifier {
inToast: false,
expireSec: 300,
canUserClose: true,
level: 'message',
inDropdown: true,
...options,
key: options.key && ("dropdown:" + options.key),
@@ -244,6 +264,7 @@ export class Notifier extends Disposable implements INotifier {
expireSec: 10,
canUserClose: true,
inDropdown: false,
level: 'message',
...options,
});
}
@@ -312,6 +333,7 @@ export class Notifier extends Disposable implements INotifier {
message: "Still working...",
canUserClose: false,
inToast: true,
level: 'message',
}));
}
await this._slowNotificationInactivityTimer.disableUntilFinish(promise);
@@ -366,6 +388,7 @@ export class Notifier extends Disposable implements INotifier {
expireSec: where === 'toast' ? 10 : 0,
inDropdown: where === 'dropdown',
actions: ['report-problem'],
level: 'error',
});
}
}

View File

@@ -41,6 +41,30 @@ export function getAppErrors(): string[] {
return _notifier.getFullAppErrors().map((e) => e.error.message);
}
/**
* Shows normal notification without any styling or icon.
*/
export function reportMessage(msg: string, options?: Partial<INotifyOptions>) {
if (_notifier && !_notifier.isDisposed()) {
_notifier.createUserMessage(msg, {
level : 'message',
...options
});
}
}
/**
* Shows notification with green border and a tick icon.
*/
export function reportSuccess(msg: string, options?: Partial<INotifyOptions>) {
if (_notifier && !_notifier.isDisposed()) {
_notifier.createUserMessage(msg, {
level : 'success',
...options
});
}
}
/**
* Report an error to the user using the global Notifier instance. If the argument is a UserError
* or an error with a status in the 400 range, it indicates a user error. Otherwise, it's an
@@ -75,7 +99,7 @@ export function reportError(err: Error|string): void {
options.title = "Add users as team members first";
options.actions = [];
}
_notifier.createUserError(message, options);
_notifier.createUserMessage(message, options);
} else if (err.name === 'UserError' || (typeof status === 'number' && status >= 400 && status < 500)) {
// This is explicitly a user error, or one in the "Client Error" range, so treat it as user
// error rather than a bug. Using message as the key causes same-message notifications to
@@ -86,9 +110,9 @@ export function reportError(err: Error|string): void {
}
_notifier.createUserError(message, options);
} else if (err.name === 'NeedUpgradeError') {
_notifier.createUserError(err.message, {actions: ['upgrade'], key: 'NEED_UPGRADE'});
_notifier.createUserMessage(err.message, {actions: ['upgrade'], key: 'NEED_UPGRADE'});
} else if (code === 'AUTH_NO_EDIT' || code === 'ACL_DENY') {
_notifier.createUserError(err.message, {key: code, memos: details?.memos});
_notifier.createUserMessage(err.message, {key: code, memos: details?.memos});
} else {
// If we don't recognize it, consider it an application error (bug) that the user should be
// able to report.

View File

@@ -6,6 +6,7 @@ import {Expirable, IAppError, Notification, Notifier, NotifyAction, Progress} fr
import {cssHoverCircle, cssTopBarBtn} from 'app/client/ui/TopBarCss';
import {colors, vars} from 'app/client/ui2018/cssVars';
import {icon} from 'app/client/ui2018/icons';
import {IconName} from "app/client/ui2018/IconList";
import {menuCssClass} from 'app/client/ui2018/menus';
import {commonUrls} from 'app/common/gristUrls';
import {dom, makeTestId, styled} from 'grainjs';
@@ -57,11 +58,27 @@ function buildAction(action: NotifyAction, item: Notification, options: IBeaconO
}
}
function notificationIcon(item: Notification) {
let iconName: IconName|null = null;
switch(item.options.level) {
case "error": iconName = "Warning"; break;
case "warning": iconName = "Warning"; break;
case "success": iconName = "TickSolid"; break;
case "info": iconName = "Info"; break;
}
return iconName ? icon(iconName, dom.cls(cssToastIcon.className)) : null;
}
function buildNotificationDom(item: Notification, options: IBeaconOpenOptions) {
const iconElement = notificationIcon(item);
const hasLeftIcon = Boolean(!item.options.title && iconElement);
return cssToastWrapper(testId('toast-wrapper'),
cssToastWrapper.cls(use => `-${use(item.status)}`),
cssToastWrapper.cls(`-${item.options.level}`),
cssToastWrapper.cls(hasLeftIcon ? '-left-icon' : ''),
item.options.title ? null : iconElement,
cssToastBody(
item.options.title ? cssToastTitle(item.options.title) : null,
item.options.title ? cssToastTitle(notificationIcon(item), cssToastTitle(item.options.title)) : null,
cssToastText(testId('toast-message'),
item.options.message,
),
@@ -169,6 +186,7 @@ function buildConnectStateButton(state: ConnectState): Element {
}
}
const cssDropdownWrapper = styled('div', `
background-color: white;
border: 1px solid ${colors.darkGrey};
@@ -238,6 +256,27 @@ const cssSnackbarWrapper = styled('div', `
pointer-events: none; /* Allow mouse clicks through */
`);
const cssToastBody = styled('div', `
display: flex;
flex-direction: column;
flex-grow: 1;
padding: 0 12px;
overflow-wrap: anywhere;
`);
const cssToastIcon = styled('div', `
flex-shrink: 0;
height: 18px;
width: 18px;
`);
const cssToastActions = styled('div', `
display: flex;
align-items: flex-end;
margin-top: 16px;
color: ${colors.lightGreen};
`);
const cssToastWrapper = styled('div', `
display: flex;
min-width: 240px;
@@ -256,9 +295,44 @@ const cssToastWrapper = styled('div', `
opacity: 1;
transition: opacity ${Expirable.fadeDelay}ms;
&-error {
border-left: 6px solid ${colors.error};
padding-left: 6px;
--icon-color: ${colors.error};
}
&-success {
border-left: 6px solid ${colors.darkGreen};
padding-left: 6px;
--icon-color: ${colors.darkGreen};
}
&-warning {
border-left: 6px solid ${colors.warningBg};
padding-left: 6px;
--icon-color: ${colors.warning};
}
&-info {
border-left: 6px solid ${colors.lightBlue};
padding-left: 6px;
--icon-color: ${colors.lightBlue};
}
&-info .${cssToastActions.className} {
color: ${colors.lighterBlue};
}
&-left-icon {
padding-left: 12px;
}
&-left-icon > .${cssToastBody.className} {
padding-left: 10px;
}
&-expiring, &-expired {
opacity: 0;
}
.${cssDropdownContent.className} > & > .notification-icon {
display: none;
}
.${cssDropdownContent.className} > & {
background-color: unset;
color: unset;
@@ -269,18 +343,13 @@ const cssToastWrapper = styled('div', `
}
`);
const cssToastBody = styled('div', `
display: flex;
flex-direction: column;
flex-grow: 1;
padding: 0 12px;
overflow-wrap: anywhere;
`);
const cssToastText = styled('div', `
`);
const cssToastTitle = styled(cssToastText, `
display: flex;
gap: 8px;
font-weight: bold;
margin-bottom: 8px;
`);
@@ -295,13 +364,6 @@ const cssToastClose = styled('div', `
margin: -4px -4px -4px 4px;
`);
const cssToastActions = styled('div', `
display: flex;
align-items: flex-end;
margin-top: 16px;
color: ${colors.lightGreen};
`);
const cssToastAction = styled('div', `
cursor: pointer;
user-select: none;

View File

@@ -55,6 +55,7 @@ export type IconName = "ChartArea" |
"Home" |
"Idea" |
"Import" |
"Info" |
"LeftAlign" |
"Lock" |
"Log" |
@@ -83,9 +84,11 @@ export type IconName = "ChartArea" |
"Share" |
"Sort" |
"Tick" |
"TickSolid" |
"Undo" |
"Validation" |
"Video" |
"Warning" |
"Widget" |
"Wrap" |
"Zoom";
@@ -147,6 +150,7 @@ export const IconList: IconName[] = ["ChartArea",
"Home",
"Idea",
"Import",
"Info",
"LeftAlign",
"Lock",
"Log",
@@ -175,9 +179,11 @@ export const IconList: IconName[] = ["ChartArea",
"Share",
"Sort",
"Tick",
"TickSolid",
"Undo",
"Validation",
"Video",
"Warning",
"Widget",
"Wrap",
"Zoom"];

View File

@@ -41,6 +41,9 @@ export const colors = {
darkerGreen: new CustomProp('color-darker-green', '#007548'),
lighterGreen: new CustomProp('color-lighter-green', '#b1ffe2'),
lighterBlue: new CustomProp('color-lighter-blue', '#87b2f9'),
lightBlue: new CustomProp('color-light-blue', '#3B82F6'),
cursor: new CustomProp('color-cursor', '#16B378'), // cursor is lightGreen
selection: new CustomProp('color-selection', 'rgba(22,179,120,0.15)'),
selectionOpaque: new CustomProp('color-selection-opaque', '#DCF4EB'),
@@ -49,6 +52,8 @@ export const colors = {
hover: new CustomProp('color-hover', '#bfbfbf'),
error: new CustomProp('color-error', '#D0021B'),
warning: new CustomProp('color-warning', '#F9AE41'),
warningBg: new CustomProp('color-warning-bg', '#dd962c'),
backdrop: new CustomProp('color-backdrop', 'rgba(38,38,51,0.9)')
};