mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 12:55:00 +00:00
refactor(core): move actions to footer of notification card (#11894)
This PR move all actions button to the footer of `NotificationCard`. There are some example as following: ### No Changes    ### Changes ### Before  #### After 
This commit is contained in:
@@ -109,12 +109,15 @@ export function copyAsImage(std: BlockStdScope) {
|
||||
notify.error({
|
||||
title: I18n.t('com.affine.copy.asImage.notAvailable.title'),
|
||||
message: I18n.t('com.affine.copy.asImage.notAvailable.message'),
|
||||
action: {
|
||||
label: I18n.t('com.affine.copy.asImage.notAvailable.action'),
|
||||
onClick: () => {
|
||||
window.open('https://affine.pro/download');
|
||||
actions: [
|
||||
{
|
||||
key: 'download',
|
||||
label: I18n.t('com.affine.copy.asImage.notAvailable.action'),
|
||||
onClick: () => {
|
||||
window.open('https://affine.pro/download');
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
Input,
|
||||
type Notification,
|
||||
notify,
|
||||
toast,
|
||||
type ToastOptions,
|
||||
@@ -96,17 +97,25 @@ export function patchNotificationService({
|
||||
throw new Error('Invalid notification accent');
|
||||
}
|
||||
|
||||
const toAffineNotificationActions = (
|
||||
actions: (typeof notification)['actions']
|
||||
): Notification['actions'] => {
|
||||
if (!actions) return undefined;
|
||||
|
||||
return actions.map(({ label, onClick, key }) => {
|
||||
return {
|
||||
key,
|
||||
label: toReactNode(label),
|
||||
onClick,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const toastId = fn(
|
||||
{
|
||||
title: toReactNode(notification.title),
|
||||
message: toReactNode(notification.message),
|
||||
footer: toReactNode(notification.footer),
|
||||
action: notification.action?.onClick
|
||||
? {
|
||||
label: toReactNode(notification.action?.label),
|
||||
onClick: notification.action.onClick,
|
||||
}
|
||||
: undefined,
|
||||
actions: toAffineNotificationActions(notification.actions),
|
||||
onDismiss: notification.onClose,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -20,10 +20,6 @@ export const thumbContent = style({
|
||||
height: 'calc(100% + 4px)',
|
||||
});
|
||||
|
||||
export const actionButton = style({
|
||||
fontSize: cssVar('fontSm'),
|
||||
lineHeight: '22px',
|
||||
});
|
||||
export const getStartedButtonText = style({
|
||||
color: cssVar('textSecondaryColor'),
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Button, FlexWrapper, notify } from '@affine/component';
|
||||
import { notify } from '@affine/component';
|
||||
import { type Notification } from '@affine/component/ui/notification';
|
||||
import { SubscriptionService } from '@affine/core/modules/cloud';
|
||||
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||
import { EditorService } from '@affine/core/modules/editor';
|
||||
@@ -63,6 +64,38 @@ export const AIOnboardingEdgeless = () => {
|
||||
});
|
||||
}, [workspaceDialogService]);
|
||||
|
||||
const actions = useMemo(() => {
|
||||
const result: NonNullable<Notification['actions']> = [
|
||||
{
|
||||
key: 'get-started',
|
||||
label: (
|
||||
<span className={styles.getStartedButtonText}>
|
||||
{t['com.affine.ai-onboarding.edgeless.get-started']()}
|
||||
</span>
|
||||
),
|
||||
onClick: () => {
|
||||
toggleEdgelessAIOnboarding(false);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
if (!aiSubscription) {
|
||||
result.push({
|
||||
key: 'purchase',
|
||||
label: (
|
||||
<span className={styles.purchaseButtonText}>
|
||||
{t['com.affine.ai-onboarding.edgeless.purchase']()}
|
||||
</span>
|
||||
),
|
||||
onClick: () => {
|
||||
goToPricingPlans();
|
||||
toggleEdgelessAIOnboarding(false);
|
||||
},
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}, [aiSubscription, goToPricingPlans, t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (generalAIOnboardingOpened) return;
|
||||
if (notifyId) return;
|
||||
@@ -83,50 +116,13 @@ export const AIOnboardingEdgeless = () => {
|
||||
thumb: <EdgelessOnboardingAnimation />,
|
||||
alignMessage: 'icon',
|
||||
onDismiss: () => toggleEdgelessAIOnboarding(false),
|
||||
footer: (
|
||||
<FlexWrapper marginTop={8} justifyContent="flex-end" gap="12px">
|
||||
<Button
|
||||
onClick={() => {
|
||||
notify.dismiss(id);
|
||||
toggleEdgelessAIOnboarding(false);
|
||||
}}
|
||||
variant="plain"
|
||||
className={styles.actionButton}
|
||||
>
|
||||
<span className={styles.getStartedButtonText}>
|
||||
{t['com.affine.ai-onboarding.edgeless.get-started']()}
|
||||
</span>
|
||||
</Button>
|
||||
{aiSubscription ? null : (
|
||||
<Button
|
||||
className={styles.actionButton}
|
||||
variant="plain"
|
||||
onClick={() => {
|
||||
goToPricingPlans();
|
||||
notify.dismiss(id);
|
||||
toggleEdgelessAIOnboarding(false);
|
||||
}}
|
||||
>
|
||||
<span className={styles.purchaseButtonText}>
|
||||
{t['com.affine.ai-onboarding.edgeless.purchase']()}
|
||||
</span>
|
||||
</Button>
|
||||
)}
|
||||
</FlexWrapper>
|
||||
),
|
||||
actions,
|
||||
},
|
||||
{ duration: 1000 * 60 * 10 }
|
||||
);
|
||||
edgelessNotifyId$.next(id);
|
||||
}, 1000);
|
||||
}, [
|
||||
aiSubscription,
|
||||
generalAIOnboardingOpened,
|
||||
goToPricingPlans,
|
||||
mode,
|
||||
notifyId,
|
||||
t,
|
||||
]);
|
||||
}, [actions, generalAIOnboardingOpened, mode, notifyId, t]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button, notify } from '@affine/component';
|
||||
import { type Notification, notify } from '@affine/component';
|
||||
import {
|
||||
RouteLogic,
|
||||
useNavigateHelper,
|
||||
@@ -8,7 +8,7 @@ import { useI18n } from '@affine/i18n';
|
||||
import { AiIcon } from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useEffect, useMemo, useRef } from 'react';
|
||||
|
||||
import { toggleLocalAIOnboarding } from './apis';
|
||||
import * as styles from './local.dialog.css';
|
||||
@@ -29,51 +29,41 @@ const LocalOnboardingAnimation = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const FooterActions = ({ onDismiss }: { onDismiss: () => void }) => {
|
||||
const t = useI18n();
|
||||
const authService = useService(AuthService);
|
||||
const loginStatus = useLiveData(authService.session.status$);
|
||||
const loggedIn = loginStatus === 'authenticated';
|
||||
const { jumpToSignIn } = useNavigateHelper();
|
||||
|
||||
return (
|
||||
<div className={styles.footerActions}>
|
||||
<a href="https://ai.affine.pro" target="_blank" rel="noreferrer">
|
||||
<Button
|
||||
className={styles.actionButton}
|
||||
variant="plain"
|
||||
onClick={onDismiss}
|
||||
>
|
||||
{t['com.affine.ai-onboarding.local.action-learn-more']()}
|
||||
</Button>
|
||||
</a>
|
||||
{loggedIn ? null : (
|
||||
<Button
|
||||
className={styles.actionButton}
|
||||
variant="plain"
|
||||
onClick={() => {
|
||||
onDismiss();
|
||||
jumpToSignIn('', RouteLogic.REPLACE, {}, { initCloud: 'true' });
|
||||
}}
|
||||
>
|
||||
{t['com.affine.ai-onboarding.local.action-get-started']()}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const AIOnboardingLocal = () => {
|
||||
const t = useI18n();
|
||||
const authService = useService(AuthService);
|
||||
const notifyId = useLiveData(localNotifyId$);
|
||||
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const { jumpToSignIn } = useNavigateHelper();
|
||||
|
||||
const loginStatus = useLiveData(authService.session.status$);
|
||||
const notSignedIn = loginStatus !== 'authenticated';
|
||||
|
||||
const actions = useMemo(() => {
|
||||
const result: NonNullable<Notification['actions']> = [
|
||||
{
|
||||
key: 'learn-more',
|
||||
label: t['com.affine.ai-onboarding.local.action-learn-more'](),
|
||||
onClick: () => {
|
||||
window.open('https://ai.affine.pro', '_blank', 'noreferrer');
|
||||
},
|
||||
},
|
||||
];
|
||||
if (notSignedIn) {
|
||||
result.push({
|
||||
key: 'get-started',
|
||||
label: t['com.affine.ai-onboarding.local.action-get-started'](),
|
||||
onClick: () => {
|
||||
jumpToSignIn('', RouteLogic.REPLACE, {}, { initCloud: 'true' });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}, [t, jumpToSignIn, notSignedIn]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!notSignedIn) return;
|
||||
// if (!notSignedIn) return;
|
||||
if (notifyId) return;
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
@@ -95,21 +85,14 @@ export const AIOnboardingLocal = () => {
|
||||
thumb: <LocalOnboardingAnimation />,
|
||||
alignMessage: 'icon',
|
||||
onDismiss: () => toggleLocalAIOnboarding(false),
|
||||
footer: (
|
||||
<FooterActions
|
||||
onDismiss={() => {
|
||||
toggleLocalAIOnboarding(false);
|
||||
notify.dismiss(id);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
actions,
|
||||
rootAttrs: { className: styles.card },
|
||||
},
|
||||
{ duration: 1000 * 60 * 10 }
|
||||
);
|
||||
localNotifyId$.next(id);
|
||||
}, 1000);
|
||||
}, [notSignedIn, notifyId, t]);
|
||||
}, [actions, notSignedIn, notifyId, t]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -6,13 +6,6 @@ export const notifyHeader = style({
|
||||
fontSize: 15,
|
||||
});
|
||||
|
||||
export const notifyFooter = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'end',
|
||||
gap: 12,
|
||||
paddingTop: 8,
|
||||
});
|
||||
|
||||
export const actionButton = style({
|
||||
fontSize: cssVar('fontSm'),
|
||||
fontWeight: 500,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button, notify } from '@affine/component';
|
||||
import { type Notification, notify } from '@affine/component';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import clsx from 'clsx';
|
||||
import { useCallback, useRef } from 'react';
|
||||
@@ -7,48 +7,9 @@ import {
|
||||
actionButton,
|
||||
cancelButton,
|
||||
confirmButton,
|
||||
notifyFooter,
|
||||
notifyHeader,
|
||||
} from './notify.css';
|
||||
|
||||
interface SubscriptionChangedNotifyFooterProps {
|
||||
onCancel: () => void;
|
||||
onConfirm?: () => void;
|
||||
to: string;
|
||||
okText: string;
|
||||
cancelText: string;
|
||||
}
|
||||
|
||||
const SubscriptionChangedNotifyFooter = ({
|
||||
to,
|
||||
okText,
|
||||
cancelText,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
}: SubscriptionChangedNotifyFooterProps) => {
|
||||
return (
|
||||
<div className={notifyFooter}>
|
||||
<Button
|
||||
className={clsx(actionButton, cancelButton)}
|
||||
size={'default'}
|
||||
onClick={onCancel}
|
||||
variant="plain"
|
||||
>
|
||||
{cancelText}
|
||||
</Button>
|
||||
<a href={to} target="_blank" rel="noreferrer">
|
||||
<Button
|
||||
onClick={onConfirm}
|
||||
className={clsx(actionButton, confirmButton)}
|
||||
variant="plain"
|
||||
>
|
||||
{okText}
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const useDowngradeNotify = () => {
|
||||
const t = useI18n();
|
||||
const prevNotifyIdRef = useRef<string | number | null>(null);
|
||||
@@ -56,6 +17,30 @@ export const useDowngradeNotify = () => {
|
||||
return useCallback(
|
||||
(link: string) => {
|
||||
prevNotifyIdRef.current && notify.dismiss(prevNotifyIdRef.current);
|
||||
|
||||
const actions: Notification['actions'] = [
|
||||
{
|
||||
key: 'later',
|
||||
label: t['com.affine.payment.downgraded-notify.later'](),
|
||||
onClick: () => {},
|
||||
buttonProps: {
|
||||
className: clsx(actionButton, cancelButton),
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'ok',
|
||||
label: BUILD_CONFIG.isElectron
|
||||
? t['com.affine.payment.downgraded-notify.ok-client']()
|
||||
: t['com.affine.payment.downgraded-notify.ok-web'](),
|
||||
onClick: () => {
|
||||
window.open(link, '_blank', 'noreferrer');
|
||||
},
|
||||
buttonProps: {
|
||||
className: clsx(actionButton, confirmButton),
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const id = notify(
|
||||
{
|
||||
title: (
|
||||
@@ -66,19 +51,7 @@ export const useDowngradeNotify = () => {
|
||||
message: t['com.affine.payment.downgraded-notify.content'](),
|
||||
alignMessage: 'title',
|
||||
icon: null,
|
||||
footer: (
|
||||
<SubscriptionChangedNotifyFooter
|
||||
to={link}
|
||||
okText={
|
||||
BUILD_CONFIG.isElectron
|
||||
? t['com.affine.payment.downgraded-notify.ok-client']()
|
||||
: t['com.affine.payment.downgraded-notify.ok-web']()
|
||||
}
|
||||
cancelText={t['com.affine.payment.downgraded-notify.later']()}
|
||||
onCancel={() => notify.dismiss(id)}
|
||||
onConfirm={() => notify.dismiss(id)}
|
||||
/>
|
||||
),
|
||||
actions,
|
||||
},
|
||||
{ duration: 24 * 60 * 60 * 1000 }
|
||||
);
|
||||
|
||||
@@ -43,10 +43,13 @@ export const OverCapacityNotification = () => {
|
||||
title: t['com.affine.payment.storage-limit.new-title'](),
|
||||
message:
|
||||
t['com.affine.payment.storage-limit.new-description.owner'](),
|
||||
action: {
|
||||
label: t['com.affine.payment.upgrade'](),
|
||||
onClick: jumpToPricePlan,
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
key: 'upgrade',
|
||||
label: t['com.affine.payment.upgrade'](),
|
||||
onClick: jumpToPricePlan,
|
||||
},
|
||||
],
|
||||
});
|
||||
} else {
|
||||
notify.warning({
|
||||
|
||||
@@ -87,14 +87,17 @@ const BackupWorkspaceItem = ({ item }: { item: BackupWorkspaceItem }) => {
|
||||
}
|
||||
notify.success({
|
||||
title: t['com.affine.settings.workspace.backup.import.success'](),
|
||||
action: {
|
||||
label:
|
||||
t['com.affine.settings.workspace.backup.import.success.action'](),
|
||||
onClick: () => {
|
||||
jumpToPage(workspaceId, 'all');
|
||||
actions: [
|
||||
{
|
||||
key: 'open',
|
||||
label:
|
||||
t['com.affine.settings.workspace.backup.import.success.action'](),
|
||||
onClick: () => {
|
||||
jumpToPage(workspaceId, 'all');
|
||||
},
|
||||
autoClose: false,
|
||||
},
|
||||
autoClose: false,
|
||||
},
|
||||
],
|
||||
});
|
||||
setMenuOpen(false);
|
||||
setImporting(false);
|
||||
|
||||
@@ -459,45 +459,49 @@ export class AtMenuConfigService extends Service {
|
||||
]({
|
||||
username,
|
||||
}),
|
||||
action: {
|
||||
label: 'Invite',
|
||||
onClick: async () => {
|
||||
track.$.sharePanel.$.inviteUserDocRole({
|
||||
control: 'member list',
|
||||
role: 'reader',
|
||||
});
|
||||
|
||||
try {
|
||||
await this.docGrantedUsersService.updateUserRole(
|
||||
id,
|
||||
DocRole.Reader
|
||||
);
|
||||
|
||||
await notificationService.mentionUser(
|
||||
id,
|
||||
workspaceId,
|
||||
{
|
||||
id: docId,
|
||||
title:
|
||||
this.docDisplayMetaService.title$(docId).value,
|
||||
blockId: block.blockId,
|
||||
mode: mode as GraphqlDocMode,
|
||||
}
|
||||
);
|
||||
|
||||
notify.success({
|
||||
title: I18n.t(
|
||||
'com.affine.editor.at-menu.invited-and-notified'
|
||||
),
|
||||
actions: [
|
||||
{
|
||||
key: 'invite',
|
||||
label: 'Invite',
|
||||
onClick: async () => {
|
||||
track.$.sharePanel.$.inviteUserDocRole({
|
||||
control: 'member list',
|
||||
role: 'reader',
|
||||
});
|
||||
} catch (error) {
|
||||
const err = UserFriendlyError.fromAny(error);
|
||||
notify.error({
|
||||
title: I18n[`error.${err.name}`](err.data),
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await this.docGrantedUsersService.updateUserRole(
|
||||
id,
|
||||
DocRole.Reader
|
||||
);
|
||||
|
||||
await notificationService.mentionUser(
|
||||
id,
|
||||
workspaceId,
|
||||
{
|
||||
id: docId,
|
||||
title:
|
||||
this.docDisplayMetaService.title$(docId)
|
||||
.value,
|
||||
blockId: block.blockId,
|
||||
mode: mode as GraphqlDocMode,
|
||||
}
|
||||
);
|
||||
|
||||
notify.success({
|
||||
title: I18n.t(
|
||||
'com.affine.editor.at-menu.invited-and-notified'
|
||||
),
|
||||
});
|
||||
} catch (error) {
|
||||
const err = UserFriendlyError.fromAny(error);
|
||||
notify.error({
|
||||
title: I18n[`error.${err.name}`](err.data),
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
} else {
|
||||
notify.error({
|
||||
|
||||
Reference in New Issue
Block a user