mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-20 11:36:25 +08:00
feat(core): add sync paused dialog (#9135)
close AF-1932 AF-1954 AF-1953 AF-1955 Add a pop-up reminder when the workspace capacity exceeds the limit or the number of members exceeds the limit.
This commit is contained in:
@@ -80,9 +80,6 @@ export class WorkspacePermission extends Entity {
|
||||
permission: Permission,
|
||||
sendInviteMail?: boolean
|
||||
) {
|
||||
if (!this.isAdmin$.value && !this.isOwner$.value) {
|
||||
throw new Error('User has no permission to invite members');
|
||||
}
|
||||
return await this.store.inviteMember(
|
||||
this.workspaceService.workspace.id,
|
||||
email,
|
||||
@@ -92,9 +89,6 @@ export class WorkspacePermission extends Entity {
|
||||
}
|
||||
|
||||
async inviteMembers(emails: string[], sendInviteMail?: boolean) {
|
||||
if (!this.isAdmin$.value && !this.isOwner$.value) {
|
||||
throw new Error('User has no permission to invite members');
|
||||
}
|
||||
return await this.store.inviteBatch(
|
||||
this.workspaceService.workspace.id,
|
||||
emails,
|
||||
@@ -103,9 +97,6 @@ export class WorkspacePermission extends Entity {
|
||||
}
|
||||
|
||||
async generateInviteLink(expireTime: WorkspaceInviteLinkExpireTime) {
|
||||
if (!this.isAdmin$.value && !this.isOwner$.value) {
|
||||
throw new Error('User has no permission to generate invite link');
|
||||
}
|
||||
return await this.store.generateInviteLink(
|
||||
this.workspaceService.workspace.id,
|
||||
expireTime
|
||||
@@ -113,18 +104,12 @@ export class WorkspacePermission extends Entity {
|
||||
}
|
||||
|
||||
async revokeInviteLink() {
|
||||
if (!this.isAdmin$.value && !this.isOwner$.value) {
|
||||
throw new Error('User has no permission to revoke invite link');
|
||||
}
|
||||
return await this.store.revokeInviteLink(
|
||||
this.workspaceService.workspace.id
|
||||
);
|
||||
}
|
||||
|
||||
async revokeMember(userId: string) {
|
||||
if (!this.isAdmin$.value && !this.isOwner$.value) {
|
||||
throw new Error('User has no permission to revoke members');
|
||||
}
|
||||
return await this.store.revokeMemberPermission(
|
||||
this.workspaceService.workspace.id,
|
||||
userId
|
||||
@@ -140,9 +125,6 @@ export class WorkspacePermission extends Entity {
|
||||
}
|
||||
|
||||
async approveMember(userId: string) {
|
||||
if (!this.isAdmin$.value && !this.isOwner$.value) {
|
||||
throw new Error('User has no permission to accept invite');
|
||||
}
|
||||
return await this.store.approveMember(
|
||||
this.workspaceService.workspace.id,
|
||||
userId
|
||||
@@ -150,9 +132,6 @@ export class WorkspacePermission extends Entity {
|
||||
}
|
||||
|
||||
async adjustMemberPermission(userId: string, permission: Permission) {
|
||||
if (!this.isAdmin$.value) {
|
||||
throw new Error('User has no permission to adjust member permissions');
|
||||
}
|
||||
return await this.store.adjustMemberPermission(
|
||||
this.workspaceService.workspace.id,
|
||||
userId,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export { WorkspaceQuotaService } from './services/quota';
|
||||
export { QuotaCheck } from './views/quota-check';
|
||||
|
||||
import {
|
||||
type Framework,
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
import { useConfirmModal } from '@affine/component';
|
||||
import { GlobalDialogService } from '@affine/core/modules/dialogs';
|
||||
import { type I18nString, useI18n } from '@affine/i18n';
|
||||
import {
|
||||
useLiveData,
|
||||
useService,
|
||||
type WorkspaceMetadata,
|
||||
WorkspacesService,
|
||||
} from '@toeverything/infra';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
import { WorkspaceQuotaService } from '../services/quota';
|
||||
import * as styles from './styles.css';
|
||||
|
||||
interface Message {
|
||||
title: I18nString;
|
||||
description: I18nString;
|
||||
confirmText: I18nString;
|
||||
tips?: I18nString[];
|
||||
cancelText?: I18nString;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Notification that the cloud workspace quota has exceeded the limit
|
||||
*
|
||||
*/
|
||||
export const QuotaCheck = ({
|
||||
workspaceMeta,
|
||||
}: {
|
||||
workspaceMeta: WorkspaceMetadata;
|
||||
}) => {
|
||||
const { openConfirmModal } = useConfirmModal();
|
||||
const workspacesService = useService(WorkspacesService);
|
||||
const workspaceQuota = useService(WorkspaceQuotaService).quota;
|
||||
const workspaceProfile = workspacesService.getProfile(workspaceMeta);
|
||||
const quota = useLiveData(workspaceQuota.quota$);
|
||||
const usedPercent = useLiveData(workspaceQuota.percent$);
|
||||
const isOwner = useLiveData(workspaceProfile.profile$)?.isOwner;
|
||||
const globalDialogService = useService(GlobalDialogService);
|
||||
const t = useI18n();
|
||||
|
||||
const onConfirm = useCallback(() => {
|
||||
if (!isOwner) {
|
||||
return;
|
||||
}
|
||||
globalDialogService.open('setting', {
|
||||
activeTab: 'plans',
|
||||
scrollAnchor: 'cloudPricingPlan',
|
||||
});
|
||||
}, [globalDialogService, isOwner]);
|
||||
|
||||
useEffect(() => {
|
||||
workspaceQuota?.revalidate();
|
||||
}, [workspaceQuota]);
|
||||
|
||||
useEffect(() => {
|
||||
if (workspaceMeta.flavour === 'local' || !quota) {
|
||||
return;
|
||||
}
|
||||
const memberOverflow = quota.memberCount > quota.memberLimit;
|
||||
// remember to use real percent
|
||||
const storageOverflow = usedPercent && usedPercent >= 100;
|
||||
const message = getSyncPausedMessage(
|
||||
!!isOwner,
|
||||
memberOverflow,
|
||||
!!storageOverflow
|
||||
);
|
||||
|
||||
if (memberOverflow || storageOverflow) {
|
||||
openConfirmModal({
|
||||
title: t.t(message.title),
|
||||
description: t.t(message.description),
|
||||
confirmText: t.t(message.confirmText),
|
||||
cancelText: message.cancelText ? t.t(message.cancelText) : undefined,
|
||||
children: <Tips tips={message.tips} />,
|
||||
childrenContentClassName: styles.modalChildren,
|
||||
onConfirm: onConfirm,
|
||||
confirmButtonOptions: {
|
||||
variant: 'primary',
|
||||
},
|
||||
cancelButtonOptions: {
|
||||
style: {
|
||||
visibility: message.cancelText ? 'visible' : 'hidden',
|
||||
},
|
||||
},
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}, [
|
||||
isOwner,
|
||||
onConfirm,
|
||||
openConfirmModal,
|
||||
quota,
|
||||
t,
|
||||
usedPercent,
|
||||
workspaceMeta.flavour,
|
||||
]);
|
||||
return null;
|
||||
};
|
||||
|
||||
const messages: Record<
|
||||
'owner' | 'member',
|
||||
Record<'both' | 'storage' | 'member', Message>
|
||||
> = {
|
||||
owner: {
|
||||
both: {
|
||||
title: 'com.affine.payment.sync-paused.owner.title',
|
||||
description: 'com.affine.payment.sync-paused.owner.both.description',
|
||||
tips: [
|
||||
'com.affine.payment.sync-paused.owner.both.tips-1',
|
||||
'com.affine.payment.sync-paused.owner.both.tips-2',
|
||||
],
|
||||
cancelText: 'Cancel',
|
||||
confirmText: 'com.affine.payment.upgrade',
|
||||
},
|
||||
storage: {
|
||||
title: 'com.affine.payment.sync-paused.owner.title',
|
||||
description: 'com.affine.payment.sync-paused.owner.storage.description',
|
||||
tips: [
|
||||
'com.affine.payment.sync-paused.owner.storage.tips-1',
|
||||
'com.affine.payment.sync-paused.owner.storage.tips-2',
|
||||
],
|
||||
cancelText: 'Cancel',
|
||||
confirmText: 'com.affine.payment.upgrade',
|
||||
},
|
||||
member: {
|
||||
title: 'com.affine.payment.sync-paused.owner.title',
|
||||
description: 'com.affine.payment.sync-paused.owner.member.description',
|
||||
tips: [
|
||||
'com.affine.payment.sync-paused.owner.member.tips-1',
|
||||
'com.affine.payment.sync-paused.owner.member.tips-2',
|
||||
],
|
||||
cancelText: 'Cancel',
|
||||
confirmText: 'com.affine.payment.upgrade',
|
||||
},
|
||||
},
|
||||
member: {
|
||||
both: {
|
||||
title: 'com.affine.payment.sync-paused.member.title',
|
||||
description: 'com.affine.payment.sync-paused.member.both.description',
|
||||
confirmText: 'com.affine.payment.sync-paused.member.member.confirm',
|
||||
},
|
||||
storage: {
|
||||
title: 'com.affine.payment.sync-paused.member.title',
|
||||
description: 'com.affine.payment.sync-paused.member.storage.description',
|
||||
confirmText: 'com.affine.payment.sync-paused.member.member.confirm',
|
||||
},
|
||||
member: {
|
||||
title: 'com.affine.payment.sync-paused.member.title',
|
||||
description: 'com.affine.payment.sync-paused.member.member.description',
|
||||
confirmText: 'com.affine.payment.sync-paused.member.member.confirm',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function getSyncPausedMessage(
|
||||
isOwner: boolean,
|
||||
isMemberOverflow: boolean,
|
||||
isStorageOverflow: boolean
|
||||
): Message {
|
||||
const userType = isOwner ? 'owner' : 'member';
|
||||
const condition =
|
||||
isStorageOverflow && isMemberOverflow
|
||||
? 'both'
|
||||
: isStorageOverflow
|
||||
? 'storage'
|
||||
: isMemberOverflow
|
||||
? 'member'
|
||||
: 'both';
|
||||
|
||||
return messages[userType][condition];
|
||||
}
|
||||
|
||||
const Tips = ({ tips }: { tips?: I18nString[] }) => {
|
||||
const t = useI18n();
|
||||
if (!tips || tips.length < 1) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className={styles.tipsStyle}>
|
||||
{tips.map(tip => (
|
||||
<div key={tip.toString()} className={styles.tipStyle}>
|
||||
<div className={styles.bullet} />
|
||||
{t.t(tip)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const tipsStyle = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '8px',
|
||||
});
|
||||
|
||||
export const tipStyle = style({
|
||||
display: 'flex',
|
||||
flexWrap: 'nowrap',
|
||||
});
|
||||
|
||||
export const bullet = style({
|
||||
backgroundColor: cssVarV2('icon/activated'),
|
||||
width: '6px',
|
||||
height: '6px',
|
||||
borderRadius: '50%',
|
||||
marginTop: '8px',
|
||||
marginLeft: '4px',
|
||||
marginRight: '12px',
|
||||
});
|
||||
|
||||
export const modalChildren = style({
|
||||
paddingLeft: '0',
|
||||
});
|
||||
Reference in New Issue
Block a user