mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(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:
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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']}
|
||||
);
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"];
|
||||
|
||||
@@ -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)')
|
||||
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user