Merge pull request #212 from dbeaver/refactor/snackbar-reusable-component

refactor(core-blocks): CB-479 apply reusable snackbar components to a…
This commit is contained in:
Alexey
2020-11-16 20:06:35 +03:00
committed by GitHub
12 changed files with 140 additions and 289 deletions
@@ -37,7 +37,7 @@ export class SessionExpiredDialogService extends Bootstrap {
this.notificationService.customNotification(() => ActionSnackbar, {
actionText: 'app_root_session_expired_reload',
onAction: () => location.reload(),
}, { title: 'app_root_session_expired_title', persistent: true, type: ENotificationType.Info });
}, { title: 'app_root_session_expired_title', persistent: true, type: ENotificationType.Error });
}
}
}
@@ -7,16 +7,16 @@
*/
import { observer } from 'mobx-react';
import { useEffect, useState } from 'react';
import styled, { use } from 'reshadow';
import { Button, IconButton } from '@cloudbeaver/core-blocks';
import { Button } from '@cloudbeaver/core-blocks';
import { INotificationExtraProps, NotificationComponent } from '@cloudbeaver/core-events';
import { useTranslate } from '@cloudbeaver/core-localization';
import { useStyles } from '@cloudbeaver/core-theming';
import { NotificationMark } from './NotificationMark';
import { SNACKBAR_COMMON_STYLES } from './SnackbarCommonStyles';
import { SnackbarBody } from './SnackbarMarkups/SnackbarBody';
import { SnackbarContent } from './SnackbarMarkups/SnackbarContent';
import { SnackbarFooter } from './SnackbarMarkups/SnackbarFooter';
import { SnackbarStatus } from './SnackbarMarkups/SnackbarStatus';
import { SnackbarWrapper } from './SnackbarMarkups/SnackbarWrapper';
interface Props extends INotificationExtraProps {
onAction: () => void;
@@ -26,36 +26,21 @@ interface Props extends INotificationExtraProps {
export const ActionSnackbar: NotificationComponent<Props> = observer(function ActionSnackbar({
notification, onAction, actionText,
}) {
const styles = useStyles(SNACKBAR_COMMON_STYLES);
const [mounted, setMounted] = useState(false);
const translate = useTranslate();
const timeStringFromTimestamp = notification.timestamp ? new Date(notification.timestamp).toLocaleTimeString() : '';
const translatedTitle = translate(notification.title);
useEffect(() => {
setMounted(true);
}, []);
return styled(styles)(
<notification as="div" {...use({ mounted })}>
<NotificationMark type={notification.type} />
<notification-body as="div">
<body-text-block as='div'>
<text-block-title title={translatedTitle} as='h2'>{translatedTitle}</text-block-title>
{notification.message && <message as="div">{translate(notification.message)}</message>}
</body-text-block>
<notification-footer as='div'>
<footer-time as='span'>{timeStringFromTimestamp}</footer-time>
<actions as="div">
<Button type="button" mod={['outlined']} onClick={onAction}>
{translate(actionText)}
</Button>
</actions>
</notification-footer>
</notification-body>
{!notification.persistent && (
<IconButton name="cross" viewBox="0 0 16 16" onClick={() => notification.close(false)} />
)}
</notification>
return (
<SnackbarWrapper unclosable={notification.persistent} onClose={() => notification.close(false)}>
<SnackbarStatus status={notification.type} />
<SnackbarContent>
<SnackbarBody title={translate(notification.title)}>
{notification.message && translate(notification.message)}
</SnackbarBody>
<SnackbarFooter timestamp={notification.timestamp}>
<Button type="button" mod={['outlined']} onClick={onAction}>
{translate(actionText)}
</Button>
</SnackbarFooter>
</SnackbarContent>
</SnackbarWrapper>
);
});
@@ -19,7 +19,6 @@ import { SnackbarContent } from './SnackbarMarkups/SnackbarContent';
import { SnackbarFooter } from './SnackbarMarkups/SnackbarFooter';
import { SnackbarStatus } from './SnackbarMarkups/SnackbarStatus';
import { SnackbarWrapper } from './SnackbarMarkups/SnackbarWrapper';
interface Props extends INotificationProcessExtraProps {
closeDelay?: number;
displayDelay?: number;
@@ -46,12 +45,14 @@ export const ProcessSnackbar: NotificationComponent<Props> = observer(function P
return (
<SnackbarWrapper
closing={!!notification.state.deleteDelay}
closeable={status !== ENotificationType.Loading}
unclosable={status === ENotificationType.Loading}
onClose={() => notification.close(false)}
>
<SnackbarStatus status={status} />
<SnackbarContent>
<SnackbarBody title={translate(title)} message={message && translate(message)} />
<SnackbarBody title={translate(title)}>
{message && translate(message)}
</SnackbarBody>
<SnackbarFooter timestamp={notification.timestamp}>
{error && (
<Button
@@ -7,24 +7,25 @@
*/
import { observer } from 'mobx-react';
import { useEffect, useState } from 'react';
import styled, { use } from 'reshadow';
import { Button, IconButton, SNACKBAR_COMMON_STYLES } from '@cloudbeaver/core-blocks';
import { Button } from '@cloudbeaver/core-blocks';
import { ENotificationType } from '@cloudbeaver/core-events';
import { useTranslate } from '@cloudbeaver/core-localization';
import { useStyles } from '@cloudbeaver/core-theming';
import { useStateDelay } from '../useStateDelay';
import { NotificationMark } from './NotificationMark';
import { SnackbarBody } from './SnackbarMarkups/SnackbarBody';
import { SnackbarContent } from './SnackbarMarkups/SnackbarContent';
import { SnackbarFooter } from './SnackbarMarkups/SnackbarFooter';
import { SnackbarStatus } from './SnackbarMarkups/SnackbarStatus';
import { SnackbarWrapper } from './SnackbarMarkups/SnackbarWrapper';
interface SnackbarProps {
type?: ENotificationType;
type: ENotificationType;
message?: string;
title: string;
closeDelay: number;
disableShowDetails?: boolean;
time?: number;
time: number;
onClose: (delayDeleting?: boolean) => void;
state?: { deleteDelay: number };
onShowDetails?: () => void;
@@ -41,42 +42,29 @@ export const Snackbar: React.FC<SnackbarProps> = observer(function Snackbar({
state,
time,
}) {
const styles = useStyles(SNACKBAR_COMMON_STYLES);
const [mounted, setMounted] = useState(false);
const translate = useTranslate();
const timeStringFromTimestamp = time ? new Date(time).toLocaleTimeString() : '';
const translatedTitle = translate(title);
useStateDelay(closeDelay > 0, closeDelay, onClose);
useEffect(() => {
setMounted(true);
}, []);
return styled(styles)(
<notification as="div" {...use({ mounted, closing: !!state?.deleteDelay })}>
{type && <NotificationMark type={type} />}
<notification-body as="div">
<body-text-block as='div'>
<text-block-title title={translatedTitle} as='h2'>{translatedTitle}</text-block-title>
{message && <message as="div">{translate(message)}</message>}
</body-text-block>
<notification-footer as='div'>
<footer-time as='span'>{timeStringFromTimestamp}</footer-time>
return (
<SnackbarWrapper closing={!!state?.deleteDelay} onClose={() => onClose(false)}>
<SnackbarStatus status={type} />
<SnackbarContent>
<SnackbarBody title={translate(title)}>
{message && translate(message)}
</SnackbarBody>
<SnackbarFooter timestamp={time}>
{onShowDetails && (
<actions as="div">
<Button
type="button"
mod={['outlined']}
disabled={disableShowDetails}
onClick={onShowDetails}
>
{translate('ui_errors_details')}
</Button>
</actions>
<Button
type="button"
mod={['outlined']}
disabled={disableShowDetails}
onClick={onShowDetails}
>
{translate('ui_errors_details')}
</Button>
)}
</notification-footer>
</notification-body>
<IconButton name="cross" viewBox="0 0 16 16" onClick={() => onClose(false)} />
</notification>
</SnackbarFooter>
</SnackbarContent>
</SnackbarWrapper>
);
});
@@ -1,111 +0,0 @@
/*
* cloudbeaver - Cloud Database Manager
* Copyright (C) 2020 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { css } from 'reshadow';
import { composes } from '@cloudbeaver/core-theming';
export const SNACKBAR_COMMON_STYLES = composes(
css`
notification {
composes: theme-background-surface theme-text-on-surface from global;
}
`,
css`
notification {
composes: theme-elevation-z5 from global;
position: relative;
display: flex;
box-sizing: border-box;
overflow: hidden;
width: 500px;
margin-bottom: 16px;
margin-left: 16px;
padding: 16px 16px;
line-height: 1.5;
opacity: 0;
border-radius: 4px;
transition: opacity 0.3s ease-in-out, transform 0.5s ease-in-out;
transform: translateX(-100%);
&[use|mounted] {
transform: translateX(0);
opacity: 1;
}
&[use|closing] {
opacity: 0;
}
}
notification-body {
display: flex;
flex-direction: column;
flex: 1 0 0;
& body-text-block {
margin-top: 8px;
padding-right: 24px;
& message {
font-size: 16px;
opacity: 0.8;
overflow: auto;
max-height: 200px;
margin-bottom: 8px;
word-break: break-word;
}
}
}
text-block-title {
composes: theme-typography--headline6 from global;
max-width: 392px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
line-height: 1.55rem;
font-weight: 700;
margin: 0;
padding: 0;
margin-bottom: 8px;
}
notification-footer {
display: flex;
align-items: flex-end;
justify-content: space-between;
}
footer-time {
composes: theme-typography--caption from global;
opacity: 0.7;
}
NotificationMark {
padding-right: 12px;
}
actions {
display: flex;
& Button {
margin-left: 16px;
&:first-child {
margin-left: auto;
}
}
}
IconButton {
position: absolute;
top: 8px;
right: 8px;
height: 22px;
width: 22px;
&:hover {
opacity: 0.7;
}
}
`
);
@@ -43,16 +43,19 @@ const SNACKBAR_BODY_STYLES = css`
interface ISnackbarBodyProps {
title: string;
message: string | null;
className?: string;
}
export const SnackbarBody: React.FC<ISnackbarBodyProps> = function SnackbarBody({ title, message, className }) {
export const SnackbarBody: React.FC<ISnackbarBodyProps> = function SnackbarBody({
title, className, children,
}) {
return styled(SNACKBAR_BODY_STYLES)(
<notification-body as="div" className={className}>
<body-text-block as='div'>
<text-block-title title={title} as='h2'>{title}</text-block-title>
{message && <message as="div">{message}</message>}
<message as="div">
{children}
</message>
</body-text-block>
</notification-body>
);
@@ -21,6 +21,11 @@ const SNACKBAR_FOOTER_STYLES = css`
actions:empty {
display: none;
}
actions:not(:empty) {
& > *:not(:first-child) {
margin-left: 16px;
}
}
`;
interface ISnackbarFooter {
@@ -56,14 +56,14 @@ const SNACKBAR_WRAPPER_STYLES = composes(
);
interface ISnackbarWrapperProps {
closing: boolean;
closeable: boolean;
closing?: boolean;
unclosable?: boolean;
onClose?: () => void;
className?: string;
}
export const SnackbarWrapper: React.FC<ISnackbarWrapperProps> = function SnackbarWrapper({
closing, closeable, onClose, children, className,
closing = false, unclosable, onClose, children, className,
}) {
const styles = useStyles(SNACKBAR_WRAPPER_STYLES);
const [mounted, setMounted] = useState(false);
@@ -75,7 +75,7 @@ export const SnackbarWrapper: React.FC<ISnackbarWrapperProps> = function Snackba
return styled(styles)(
<notification as="div" className={className} {...use({ mounted, closing })}>
{children}
{closeable && onClose && (
{!unclosable && onClose && (
<IconButton name="cross" viewBox="0 0 16 16" onClick={onClose} />
)}
</notification>
+5 -1
View File
@@ -52,7 +52,11 @@ export * from './useErrorDetails';
export * from './useAppLoadingScreen';
export * from './BlocksLocaleService';
export * from './Snackbars/NotificationMark';
export * from './Snackbars/SnackbarMarkups/SnackbarWrapper';
export * from './Snackbars/SnackbarMarkups/SnackbarStatus';
export * from './Snackbars/SnackbarMarkups/SnackbarContent';
export * from './Snackbars/SnackbarMarkups/SnackbarBody';
export * from './Snackbars/SnackbarMarkups/SnackbarFooter';
export * from './Snackbars/Snackbar';
export * from './Snackbars/ActionSnackbar';
export * from './Snackbars/ProcessSnackbar';
export * from './Snackbars/SnackbarCommonStyles';
@@ -6,6 +6,8 @@
* you may not use this file except in compliance with the License.
*/
import { observer } from 'mobx-react';
import { Snackbar } from '@cloudbeaver/core-blocks';
import { useController } from '@cloudbeaver/core-di';
import { INotification } from '@cloudbeaver/core-events';
@@ -16,7 +18,7 @@ interface Props {
notification: INotification<any>;
}
export const NotificationsItem: React.FC<Props> = function Notification({ notification }) {
export const NotificationsItem: React.FC<Props> = observer(function Notification({ notification }) {
const controller = useController(NotificationItemController, notification);
if (notification.customComponent) {
@@ -37,4 +39,4 @@ export const NotificationsItem: React.FC<Props> = function Notification({ notifi
onShowDetails={controller.handleShowDetails}
/>
);
};
});
@@ -7,11 +7,10 @@
*/
import { observer } from 'mobx-react';
import { useEffect, useState } from 'react';
import styled, { use, css } from 'reshadow';
import styled, { css } from 'reshadow';
import {
Button, Loader, IconButton, SNACKBAR_COMMON_STYLES, NotificationMark
Button, SnackbarWrapper, SnackbarStatus, SnackbarContent, SnackbarBody, SnackbarFooter
} from '@cloudbeaver/core-blocks';
import { useController } from '@cloudbeaver/core-di';
import { ENotificationType, NotificationComponentProps } from '@cloudbeaver/core-events';
@@ -22,9 +21,6 @@ import { EDeferredState } from '@cloudbeaver/core-utils';
import { ExportNotificationController } from './ExportNotificationController';
const styles = css`
Loader {
margin-right: 16px;
}
source-name {
composes: theme-typography--body2 from global;
padding-top: 16px;
@@ -35,12 +31,6 @@ const styles = css`
margin: 0;
}
}
loader-container {
& Loader {
width: 40px;
height: 40px;
}
}
`;
type Props = NotificationComponentProps<{
@@ -52,84 +42,64 @@ export const ExportNotification: React.FC<Props> = observer(function ExportNotif
}) {
const controller = useController(ExportNotificationController, notification);
const translate = useTranslate();
const [mounted, setMounted] = useState(false);
const timeStringFromTimestamp = notification.timestamp ? new Date(notification.timestamp).toLocaleTimeString() : '';
const exportNotificationType = controller.isSuccess ? ENotificationType.Info : ENotificationType.Error;
const translatedStatus = translate(controller.status);
const { title, status } = controller.status;
useEffect(() => {
setMounted(true);
}, []);
return styled(useStyles(SNACKBAR_COMMON_STYLES, styles))(
<notification as="div" {...use({ mounted })}>
{!controller.isPending && <NotificationMark type={exportNotificationType} />}
{controller.isPending && (
<loader-container as='div'>
<Loader fullSize hideMessage />
</loader-container>
)}
<notification-body as="div">
<body-text-block as='div'>
<text-block-title title={translatedStatus} as="h2">{translatedStatus}</text-block-title>
<message as="div">
{controller.sourceName}
{controller.task?.context.sourceName && (
<pre title={controller.task?.context.sourceName}>
{controller.task?.context.sourceName}
</pre>
)}
</message>
</body-text-block>
<notification-footer as='div'>
<footer-time as='span'>{timeStringFromTimestamp}</footer-time>
<actions as="div">
{controller.isSuccess && (
<>
<Button
type="button"
mod={['outlined']}
onClick={controller.delete}
>
{translate('data_transfer_notification_delete')}
</Button>
<Button
tag='a'
href={controller.downloadUrl}
mod={['unelevated']}
download
onClick={controller.download}
>
{translate('data_transfer_notification_download')}
</Button>
</>
)}
{controller.hasDetails && (
return styled(useStyles(styles))(
<SnackbarWrapper unclosable={status === ENotificationType.Loading} onClose={controller.delete}>
<SnackbarStatus status={status} />
<SnackbarContent>
<SnackbarBody title={translate(title)}>
{controller.sourceName}
{controller.task?.context.sourceName && (
<pre title={controller.task?.context.sourceName}>
{controller.task?.context.sourceName}
</pre>
)}
</SnackbarBody>
<SnackbarFooter timestamp={notification.timestamp}>
{status === ENotificationType.Info && (
<>
<Button
type="button"
mod={['outlined']}
disabled={controller.isDetailsDialogOpen}
onClick={controller.showDetails}
onClick={controller.delete}
>
{translate('ui_errors_details')}
{translate('data_transfer_notification_delete')}
</Button>
)}
{controller.isPending && (
<Button
type="button"
mod={['outlined']}
disabled={controller.process?.getState() === EDeferredState.CANCELLING}
onClick={controller.cancel}
tag='a'
href={controller.downloadUrl}
mod={['unelevated']}
download
onClick={controller.download}
>
{translate('ui_processing_cancel')}
{translate('data_transfer_notification_download')}
</Button>
)}
</actions>
</notification-footer>
</notification-body>
{!controller.isPending && (
<IconButton name="cross" viewBox="0 0 16 16" onClick={controller.delete} />
)}
</notification>
</>
)}
{status === ENotificationType.Error && (
<Button
type="button"
mod={['outlined']}
disabled={controller.isDetailsDialogOpen}
onClick={controller.showDetails}
>
{translate('ui_errors_details')}
</Button>
)}
{status === ENotificationType.Loading && (
<Button
type="button"
mod={['outlined']}
disabled={controller.process?.getState() === EDeferredState.CANCELLING}
onClick={controller.cancel}
>
{translate('ui_processing_cancel')}
</Button>
)}
</SnackbarFooter>
</SnackbarContent>
</SnackbarWrapper>
);
});
@@ -11,13 +11,17 @@ import { observable, computed } from 'mobx';
import { NavNodeManagerService } from '@cloudbeaver/core-app';
import { IInitializableController, injectable } from '@cloudbeaver/core-di';
import { CommonDialogService } from '@cloudbeaver/core-dialogs';
import { INotification } from '@cloudbeaver/core-events';
import { ENotificationType, INotification } from '@cloudbeaver/core-events';
import { LocalizationService } from '@cloudbeaver/core-localization';
import { ErrorDetailsDialog } from '@cloudbeaver/core-notifications';
import { Deferred, EDeferredState } from '@cloudbeaver/core-utils';
import { DataExportProcessService, ExportProcess } from '../DataExportProcessService';
interface ExportNotificationStatus {
title: string;
status: ENotificationType;
}
@injectable()
export class ExportNotificationController implements IInitializableController {
@observable isDetailsDialogOpen = false;
@@ -55,16 +59,16 @@ export class ExportNotificationController implements IInitializableController {
return this.localization.translate('data_transfer_exporting_sql');
}
get status(): string {
get status(): ExportNotificationStatus {
switch (this.process?.getState()) {
case EDeferredState.PENDING:
return 'data_transfer_notification_preparation';
return { title: 'data_transfer_notification_preparation', status: ENotificationType.Loading };
case EDeferredState.CANCELLING:
return 'ui_processing_canceling';
return { title: 'ui_processing_canceling', status: ENotificationType.Loading };
case EDeferredState.RESOLVED:
return 'data_transfer_notification_ready';
return { title: 'data_transfer_notification_ready', status: ENotificationType.Info };
default:
return 'data_transfer_notification_error';
return { title: 'data_transfer_notification_error', status: ENotificationType.Error };
}
}
@@ -87,12 +91,12 @@ export class ExportNotificationController implements IInitializableController {
delete = (): void => {
this.dataExportProcessService.delete(this.notification.extraProps.source);
this.notification.close();
this.notification.close(false);
};
download = (): void => {
this.dataExportProcessService.download(this.notification.extraProps.source);
this.notification.close();
this.notification.close(false);
};
cancel = (): void => {