refactor(core): move setting dialog to workspace scope (#9706)

This commit is contained in:
pengx17
2025-01-15 13:00:06 +00:00
parent 9df338274d
commit b1896746f9
32 changed files with 248 additions and 277 deletions
@@ -27,7 +27,6 @@ const GLOBAL_DIALOGS = {
'create-workspace': CreateWorkspaceDialog,
'import-workspace': ImportWorkspaceDialog,
'import-template': ImportTemplateDialog,
setting: SettingDialog,
'sign-in': SignInDialog,
'change-password': ChangePasswordDialog,
'verify-email': VerifyEmailDialog,
@@ -45,6 +44,7 @@ const WORKSPACE_DIALOGS = {
'doc-selector': DocSelectorDialog,
'collection-selector': CollectionSelectorDialog,
'date-selector': DateSelectorDialog,
setting: SettingDialog,
import: ImportDialog,
} satisfies {
[key in keyof WORKSPACE_DIALOG_SCHEMA]?: React.FC<
@@ -10,11 +10,10 @@ import {
PenIcon,
} from '@blocksuite/icons/rc';
import { useLiveData, useServices } from '@toeverything/infra';
import type { ReactNode } from 'react';
import { useEffect } from 'react';
import { AuthService, ServerService } from '../../../../modules/cloud';
import type { SettingState } from '../types';
import type { SettingSidebarItem, SettingState } from '../types';
import { AboutAffine } from './about';
import { AppearanceSettings } from './appearance';
import { BillingSettings } from './billing';
@@ -24,14 +23,7 @@ import { PaymentIcon, UpgradeIcon } from './icons';
import { AFFiNEPricingPlans } from './plans';
import { Shortcuts } from './shortcuts';
interface GeneralSettingListItem {
key: SettingTab;
title: string;
icon: ReactNode;
testId: string;
}
export type GeneralSettingList = GeneralSettingListItem[];
export type GeneralSettingList = SettingSidebarItem[];
export const useGeneralSettingList = (): GeneralSettingList => {
const t = useI18n();
@@ -54,7 +46,7 @@ export const useGeneralSettingList = (): GeneralSettingList => {
userFeatureService.userFeature.revalidate();
}, [userFeatureService]);
const settings: GeneralSettingListItem[] = [
const settings: GeneralSettingList = [
{
key: 'appearance',
title: t['com.affine.settings.appearance'](),
@@ -7,11 +7,11 @@ import {
DefaultServerService,
ServersService,
} from '@affine/core/modules/cloud';
import type { DialogComponentProps } from '@affine/core/modules/dialogs';
import type {
DialogComponentProps,
GLOBAL_DIALOG_SCHEMA,
} from '@affine/core/modules/dialogs';
import type { SettingTab } from '@affine/core/modules/dialogs/constant';
SettingTab,
WORKSPACE_DIALOG_SCHEMA,
} from '@affine/core/modules/dialogs/constant';
import { GlobalContextService } from '@affine/core/modules/global-context';
import { Trans } from '@affine/i18n';
import { ContactWithUsIcon } from '@blocksuite/icons/rc';
@@ -207,7 +207,7 @@ const SettingModalInner = ({
export const SettingDialog = ({
close,
activeTab,
}: DialogComponentProps<GLOBAL_DIALOG_SCHEMA['setting']>) => {
}: DialogComponentProps<WORKSPACE_DIALOG_SCHEMA['setting']>) => {
return (
<Modal
width={1280}
@@ -1,38 +1,25 @@
import { WorkspaceListSkeleton } from '@affine/component/setting-components';
import { Avatar } from '@affine/component/ui/avatar';
import { UserPlanButton } from '@affine/core/components/affine/auth/user-plan-button';
import { useCatchEventCallback } from '@affine/core/components/hooks/use-catch-event-hook';
import { useWorkspaceInfo } from '@affine/core/components/hooks/use-workspace-info';
import { CurrentWorkspaceScopeProvider } from '@affine/core/components/providers/current-workspace-scope';
import { AuthService } from '@affine/core/modules/cloud';
import { UserFeatureService } from '@affine/core/modules/cloud/services/user-feature';
import { GlobalDialogService } from '@affine/core/modules/dialogs';
import type { SettingTab } from '@affine/core/modules/dialogs/constant';
import {
type WorkspaceMetadata,
WorkspaceService,
} from '@affine/core/modules/workspace';
import { type WorkspaceMetadata } from '@affine/core/modules/workspace';
import { useI18n } from '@affine/i18n';
import { track } from '@affine/track';
import {
Logo1Icon,
PaymentIcon,
PropertyIcon,
SettingsIcon,
} from '@blocksuite/icons/rc';
import { useLiveData, useService, useServices } from '@toeverything/infra';
import { Logo1Icon } from '@blocksuite/icons/rc';
import { useLiveData, useService } from '@toeverything/infra';
import clsx from 'clsx';
import {
type HTMLAttributes,
type MouseEvent,
type ReactNode,
Suspense,
useCallback,
useEffect,
useMemo,
} from 'react';
import { useGeneralSettingList } from '../general-setting';
import { useWorkspaceSettingList } from '../workspace-setting';
import * as style from './style.css';
export type UserInfoProps = {
@@ -118,26 +105,51 @@ export const SignInButton = () => {
);
};
type SettingSidebarItemProps = {
isActive: boolean;
icon: ReactNode;
title: string;
key: string;
testId?: string;
} & HTMLAttributes<HTMLDivElement>;
const SettingSidebarItem = ({
isActive,
icon,
label,
title,
testId,
...props
}: {
isActive: boolean;
label: string;
icon: ReactNode;
} & HTMLAttributes<HTMLDivElement>) => {
}: SettingSidebarItemProps) => {
return (
<div
{...props}
title={label}
title={title}
data-testid={testId}
className={clsx(style.sidebarSelectItem, {
active: isActive,
})}
>
<div className={style.sidebarSelectItemIcon}>{icon}</div>
<div className={style.sidebarSelectItemName}>{label}</div>
<div className={style.sidebarSelectItemName}>{title}</div>
</div>
);
};
const SettingSidebarGroup = ({
title,
items,
}: {
title: string;
items: SettingSidebarItemProps[];
}) => {
return (
<div className={style.sidebarGroup}>
<div className={style.sidebarSubtitle}>{title}</div>
<div className={style.sidebarItemsWrapper}>
{items.map(({ key, ...props }) => (
<SettingSidebarItem key={key} {...props} />
))}
</div>
</div>
);
};
@@ -152,12 +164,11 @@ export const SettingSidebar = ({
const t = useI18n();
const loginStatus = useLiveData(useService(AuthService).session.status$);
const generalList = useGeneralSettingList();
const workspaceSettingList = useWorkspaceSettingList();
const gotoTab = useCallback(
(e: MouseEvent<HTMLDivElement>) => {
const tab = e.currentTarget.dataset.eventArg;
if (!tab) return;
(tab: SettingTab) => {
track.$.settingsPanel.menu.openSettings({ to: tab });
onTabChange(tab as SettingTab);
onTabChange(tab);
},
[onTabChange]
);
@@ -165,16 +176,34 @@ export const SettingSidebar = ({
track.$.settingsPanel.menu.openSettings({ to: 'account' });
onTabChange('account');
}, [onTabChange]);
const onWorkspaceSettingClick = useCallback(
(tab: SettingTab) => {
track.$.settingsPanel.menu.openSettings({
to: 'workspace',
control: tab,
});
onTabChange(tab);
},
[onTabChange]
);
const groups = useMemo(() => {
const res = [
{
key: 'setting:general',
title: t['com.affine.settingSidebar.settings.general'](),
items: generalList,
},
{
key: 'setting:workspace',
title: t['com.affine.settingSidebar.settings.workspace'](),
items: workspaceSettingList,
},
].map(group => {
return {
...group,
items: group.items.map(item => {
return {
...item,
isActive: item.key === activeTab,
'data-event-arg': item.key,
onClick: () => gotoTab(item.key),
};
}),
};
});
return res;
}, [activeTab, generalList, gotoTab, t, workspaceSettingList]);
return (
<div className={style.settingSlideBar} data-testid="settings-sidebar">
@@ -193,117 +222,13 @@ export const SettingSidebar = ({
</Suspense>
) : null}
<div className={style.sidebarGroup}>
<div className={style.sidebarSubtitle}>
{t['com.affine.settingSidebar.settings.general']()}
</div>
<div className={style.sidebarItemsWrapper}>
{generalList.map(({ title, icon, key, testId }) => {
return (
<SettingSidebarItem
isActive={key === activeTab}
key={key}
label={title}
data-event-arg={key}
onClick={gotoTab}
data-testid={testId}
icon={icon}
/>
);
})}
</div>
</div>
<div className={style.sidebarGroup}>
<div className={style.sidebarSubtitle}>
{t['com.affine.settingSidebar.settings.workspace']()}
</div>
<div className={style.sidebarItemsWrapper}>
<Suspense fallback={<WorkspaceListSkeleton />}>
<CurrentWorkspaceScopeProvider>
<WorkspaceSettingItems
onWorkspaceSettingClick={onWorkspaceSettingClick}
activeTab={activeTab}
/>
</CurrentWorkspaceScopeProvider>
</Suspense>
</div>
</div>
</div>
);
};
const WorkspaceSettingItems = ({
onWorkspaceSettingClick,
activeTab,
}: {
onWorkspaceSettingClick: (activeTab: SettingTab) => void;
activeTab: SettingTab;
}) => {
const { userFeatureService } = useServices({
UserFeatureService,
});
const workspaceService = useService(WorkspaceService);
const information = useWorkspaceInfo(workspaceService.workspace);
const t = useI18n();
useEffect(() => {
userFeatureService.userFeature.revalidate();
}, [userFeatureService]);
const showBilling = information?.isTeam && information?.isOwner;
const subTabs = useMemo(() => {
const subTabConfigs = [
{
key: 'workspace:preference',
title: 'com.affine.settings.workspace.preferences',
icon: <SettingsIcon />,
},
{
key: 'workspace:properties',
title: 'com.affine.settings.workspace.properties',
icon: <PropertyIcon />,
},
...(showBilling
? [
{
key: 'workspace:billing' as SettingTab,
title: 'com.affine.settings.workspace.billing',
icon: <PaymentIcon />,
},
]
: []),
] satisfies {
key: SettingTab;
title: keyof ReturnType<typeof useI18n>;
icon: ReactNode;
}[];
return subTabConfigs.map(({ key, title, icon }) => {
return (
<SettingSidebarItem
isActive={activeTab === key}
label={t[title]()}
icon={icon}
data-testid={`workspace-list-item-${key}`}
onClick={() => {
onWorkspaceSettingClick(key);
}}
className={clsx(style.sidebarSelectItem, {
active: activeTab === key,
})}
key={key}
{groups.map(group => (
<SettingSidebarGroup
key={group.key}
title={group.title}
items={group.items}
/>
);
});
}, [activeTab, onWorkspaceSettingClick, showBilling, t]);
return (
<div className={style.sidebarItemsWrapper}>
{/* TODO: remove the suspense? */}
<Suspense fallback={<WorkspaceListSkeleton />}>{subTabs}</Suspense>
))}
</div>
);
};
@@ -1,6 +1,14 @@
import type { SettingTab } from '@affine/core/modules/dialogs/constant';
import type { ReactNode } from 'react';
export interface SettingState {
activeTab: SettingTab;
scrollAnchor?: string;
}
export interface SettingSidebarItem {
key: SettingTab;
title: string;
icon: ReactNode;
testId: string;
}
@@ -1,8 +1,12 @@
import { CurrentWorkspaceScopeProvider } from '@affine/core/components/providers/current-workspace-scope';
import { useWorkspaceInfo } from '@affine/core/components/hooks/use-workspace-info';
import type { SettingTab } from '@affine/core/modules/dialogs/constant';
import { WorkspaceService } from '@affine/core/modules/workspace';
import { useI18n } from '@affine/i18n';
import { PaymentIcon, PropertyIcon, SettingsIcon } from '@blocksuite/icons/rc';
import { useService } from '@toeverything/infra';
import { useMemo } from 'react';
import type { SettingState } from '../types';
import type { SettingSidebarItem, SettingState } from '../types';
import { WorkspaceSettingBilling } from './billing';
import { WorkspaceSettingDetail } from './new-workspace-setting-detail';
import { WorkspaceSettingProperties } from './properties';
@@ -16,24 +20,56 @@ export const WorkspaceSetting = ({
onCloseSetting: () => void;
onChangeSettingState: (settingState: SettingState) => void;
}) => {
const element = useMemo(() => {
switch (activeTab) {
case 'workspace:preference':
return (
<WorkspaceSettingDetail
onCloseSetting={onCloseSetting}
onChangeSettingState={onChangeSettingState}
/>
);
case 'workspace:properties':
return <WorkspaceSettingProperties />;
case 'workspace:billing':
return <WorkspaceSettingBilling />;
default:
return null;
}
}, [activeTab, onCloseSetting, onChangeSettingState]);
return (
<CurrentWorkspaceScopeProvider>{element}</CurrentWorkspaceScopeProvider>
);
switch (activeTab) {
case 'workspace:preference':
return (
<WorkspaceSettingDetail
onCloseSetting={onCloseSetting}
onChangeSettingState={onChangeSettingState}
/>
);
case 'workspace:properties':
return <WorkspaceSettingProperties />;
case 'workspace:billing':
return <WorkspaceSettingBilling />;
default:
return null;
}
};
export const useWorkspaceSettingList = (): SettingSidebarItem[] => {
const workspaceService = useService(WorkspaceService);
const information = useWorkspaceInfo(workspaceService.workspace);
const t = useI18n();
const showBilling = information?.isTeam && information?.isOwner;
const items = useMemo<SettingSidebarItem[]>(() => {
return [
{
key: 'workspace:preference',
title: t['com.affine.settings.workspace.preferences'](),
icon: <SettingsIcon />,
testId: 'workspace-setting:preference',
},
{
key: 'workspace:properties',
title: t['com.affine.settings.workspace.properties'](),
icon: <PropertyIcon />,
testId: 'workspace-setting:properties',
},
...(showBilling
? [
{
key: 'workspace:billing' as SettingTab,
title: t['com.affine.settings.workspace.billing'](),
icon: <PaymentIcon />,
testId: 'workspace-setting:billing',
},
]
: []),
];
}, [showBilling, t]);
return items;
};