fix(theme-editor): "No permission" error when updating default theme of system (#3171)

* fix: fix T-2703

* fix: bug

* fix: migration

* chore: switch type to radio

* fix: collection.sync

---------

Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
YANG QIA 2023-12-12 14:09:30 +08:00 committed by GitHub
parent 7166409c75
commit 8c1738db83
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 132 additions and 134 deletions

View File

@ -28,6 +28,7 @@
"@nocobase/database": "0.x", "@nocobase/database": "0.x",
"@nocobase/server": "0.x", "@nocobase/server": "0.x",
"@nocobase/test": "0.x", "@nocobase/test": "0.x",
"@nocobase/utils": "0.x" "@nocobase/utils": "0.x",
"@nocobase/actions": "0.x"
} }
} }

View File

@ -1,25 +1,28 @@
import { defaultTheme, useAPIClient, useCurrentUserContext, useGlobalTheme, useSystemSettings } from '@nocobase/client'; import { defaultTheme as presetTheme, useAPIClient, useCurrentUserContext, useGlobalTheme } from '@nocobase/client';
import { error } from '@nocobase/utils/client'; import { error } from '@nocobase/utils/client';
import { Spin } from 'antd'; import { Spin } from 'antd';
import React, { useEffect, useRef } from 'react'; import React, { useEffect, useMemo, useRef } from 'react';
import { changeAlgorithmFromFunctionToString } from '../utils/changeAlgorithmFromFunctionToString'; import { changeAlgorithmFromFunctionToString } from '../utils/changeAlgorithmFromFunctionToString';
import { changeAlgorithmFromStringToFunction } from '../utils/changeAlgorithmFromStringToFunction'; import { changeAlgorithmFromStringToFunction } from '../utils/changeAlgorithmFromStringToFunction';
import { useThemeListContext } from './ThemeListProvider'; import { useThemeListContext } from './ThemeListProvider';
const CurrentThemeIdContext = React.createContext<number>(null); const ThemeIdContext = React.createContext<{
currentThemeId: number;
defaultThemeId: number;
}>({} as any);
export const useCurrentThemeId = () => { export const useThemeId = () => {
return React.useContext(CurrentThemeIdContext); return React.useContext(ThemeIdContext);
}; };
/** /**
* *
*/ */
const InitializeTheme: React.FC = ({ children }) => { const InitializeTheme: React.FC = ({ children }) => {
const systemSettings = useSystemSettings();
const currentUser = useCurrentUserContext(); const currentUser = useCurrentUserContext();
const { setTheme } = useGlobalTheme(); const { setTheme } = useGlobalTheme();
const { run, data, loading } = useThemeListContext(); const { run, data, loading } = useThemeListContext();
const defaultTheme = useMemo(() => data?.find((item) => item.default), [data]);
const themeId = useRef<number>(null); const themeId = useRef<number>(null);
const api = useAPIClient(); const api = useAPIClient();
@ -39,40 +42,44 @@ const InitializeTheme: React.FC = ({ children }) => {
return run(); return run();
} }
themeId.current = const currentThemeId = currentUser?.data?.data?.systemSettings?.themeId;
// 这里不要使用 `===` 操作符 let theme: any;
currentUser?.data?.data?.systemSettings?.themeId == null if (currentThemeId !== null && currentThemeId !== undefined) {
? systemSettings?.data?.data?.options?.themeId // Use the theme from the current user's system settings
: currentUser?.data?.data?.systemSettings?.themeId; theme = data.find((item) => item.id === currentThemeId);
}
// 这里不要使用 `!==` 操作符 if (!theme) {
if (themeId.current != null) { // Use the default theme if there is not an available theme in user's system settings
const theme = data?.find((item) => item.id === themeId.current); theme = defaultTheme;
}
if (theme) { if (theme) {
themeId.current = theme.id;
setTheme(theme.config); setTheme(theme.config);
api.auth.setOption( api.auth.setOption(
'theme', 'theme',
JSON.stringify(Object.assign({ ...theme }, { config: changeAlgorithmFromFunctionToString(theme.config) })), JSON.stringify(Object.assign({ ...theme }, { config: changeAlgorithmFromFunctionToString(theme.config) })),
); );
}
} else { } else {
setTheme(defaultTheme); // Use the preset theme if the theme is not found
setTheme(presetTheme);
api.auth.setOption('theme', null); api.auth.setOption('theme', null);
} }
}, [ }, [api.auth, currentUser?.data?.data?.systemSettings?.themeId, data, run, setTheme, defaultTheme]);
api.auth,
currentUser?.data?.data?.systemSettings?.themeId,
data,
run,
setTheme,
systemSettings?.data?.data?.options?.themeId,
]);
if (loading && !data) { if (loading && !data) {
return <Spin />; return <Spin />;
} }
return <CurrentThemeIdContext.Provider value={themeId.current}>{children}</CurrentThemeIdContext.Provider>; return (
<ThemeIdContext.Provider
value={{
currentThemeId: themeId.current,
defaultThemeId: defaultTheme?.id,
}}
>
{children}
</ThemeIdContext.Provider>
);
}; };
InitializeTheme.displayName = 'InitializeTheme'; InitializeTheme.displayName = 'InitializeTheme';

View File

@ -1,12 +1,5 @@
import { DeleteOutlined, EditOutlined, EllipsisOutlined } from '@ant-design/icons'; import { DeleteOutlined, EditOutlined, EllipsisOutlined } from '@ant-design/icons';
import { import { compatOldTheme, useAPIClient, useCurrentUserContext, useGlobalTheme, useToken } from '@nocobase/client';
compatOldTheme,
useAPIClient,
useCurrentUserContext,
useGlobalTheme,
useSystemSettings,
useToken,
} from '@nocobase/client';
import { error } from '@nocobase/utils/client'; import { error } from '@nocobase/utils/client';
import { App, Card, ConfigProvider, Dropdown, Space, Switch, Tag, message } from 'antd'; import { App, Card, ConfigProvider, Dropdown, Space, Switch, Tag, message } from 'antd';
import React, { useCallback, useMemo } from 'react'; import React, { useCallback, useMemo } from 'react';
@ -14,7 +7,7 @@ import { ThemeConfig, ThemeItem } from '../../types';
import { Primary } from '../antd-token-previewer'; import { Primary } from '../antd-token-previewer';
import { useUpdateThemeSettings } from '../hooks/useUpdateThemeSettings'; import { useUpdateThemeSettings } from '../hooks/useUpdateThemeSettings';
import { useTranslation } from '../locale'; import { useTranslation } from '../locale';
import { useCurrentThemeId } from './InitializeTheme'; import { useThemeId } from './InitializeTheme';
import { useThemeEditorContext } from './ThemeEditorProvider'; import { useThemeEditorContext } from './ThemeEditorProvider';
enum HandleTypes { enum HandleTypes {
@ -69,17 +62,16 @@ const ThemeCard = (props: Props) => {
} = useGlobalTheme(); } = useGlobalTheme();
const { setOpen } = useThemeEditorContext(); const { setOpen } = useThemeEditorContext();
const currentUser = useCurrentUserContext(); const currentUser = useCurrentUserContext();
const systemSettings = useSystemSettings();
const { item, style = {}, onChange } = props; const { item, style = {}, onChange } = props;
const api = useAPIClient(); const api = useAPIClient();
const { updateUserThemeSettings, updateSystemThemeSettings } = useUpdateThemeSettings(); const { updateUserThemeSettings } = useUpdateThemeSettings();
const { modal } = App.useApp(); const { modal } = App.useApp();
const [loading, setLoading] = React.useState(false); const [loading, setLoading] = React.useState(false);
const currentThemeId = useCurrentThemeId(); const { currentThemeId, defaultThemeId } = useThemeId();
const { t } = useTranslation(); const { t } = useTranslation();
const { token } = useToken(); const { token } = useToken();
const isDefault = item.id === systemSettings?.data?.data?.options?.themeId; const isDefault = item.id === defaultThemeId;
const handleDelete = useCallback(() => { const handleDelete = useCallback(() => {
if (isDefault) { if (isDefault) {
@ -97,9 +89,6 @@ const ThemeCard = (props: Props) => {
if (item.id === currentUser?.data?.data?.systemSettings?.themeId) { if (item.id === currentUser?.data?.data?.systemSettings?.themeId) {
updateUserThemeSettings(null); updateUserThemeSettings(null);
} }
if (item.id === systemSettings?.data?.data?.options?.themeId) {
updateSystemThemeSettings(null);
}
message.success(t('Deleted successfully')); message.success(t('Deleted successfully'));
onChange?.({ type: HandleTypes.delete, item }); onChange?.({ type: HandleTypes.delete, item });
}, },
@ -111,9 +100,7 @@ const ThemeCard = (props: Props) => {
item, item,
modal, modal,
onChange, onChange,
systemSettings?.data?.data?.options?.themeId,
t, t,
updateSystemThemeSettings,
updateUserThemeSettings, updateUserThemeSettings,
]); ]);
const handleSwitchOptional = useCallback( const handleSwitchOptional = useCallback(
@ -127,12 +114,11 @@ const ThemeCard = (props: Props) => {
method: 'post', method: 'post',
data: { data: {
optional: checked, optional: checked,
default: false,
}, },
}), }),
// 如果用户把当前设置的主题设置为不可选,那么就需要把当前设置的主题设置为默认主题 // 如果用户把当前设置的主题设置为不可选,那么就需要把当前设置的主题设置为默认主题
item.id === currentThemeId && updateUserThemeSettings(null), item.id === currentUser?.data?.data?.systemSettings?.themeId && updateUserThemeSettings(null),
// 系统默认主题应该始终是可选的,所以当用户设置为不可选时,应该也同时把系统默认主题设置为空
item.id === systemSettings?.data?.data?.options?.themeId && updateSystemThemeSettings(null),
]); ]);
} else { } else {
await api.request({ await api.request({
@ -150,16 +136,7 @@ const ThemeCard = (props: Props) => {
message.success(t('Updated successfully')); message.success(t('Updated successfully'));
onChange?.({ type: HandleTypes.optional, item }); onChange?.({ type: HandleTypes.optional, item });
}, },
[ [api, currentUser?.data?.data?.systemSettings?.themeId, item, onChange, t, updateUserThemeSettings],
api,
currentThemeId,
item,
onChange,
systemSettings?.data?.data?.options?.themeId,
t,
updateSystemThemeSettings,
updateUserThemeSettings,
],
); );
const handleSwitchDefault = useCallback( const handleSwitchDefault = useCallback(
async (checked: boolean) => { async (checked: boolean) => {
@ -167,18 +144,24 @@ const ThemeCard = (props: Props) => {
try { try {
if (checked) { if (checked) {
await Promise.all([ await Promise.all([
updateSystemThemeSettings(item.id),
// 用户在设置该主题为默认主题时,肯定也希望该主题可被用户选择 // 用户在设置该主题为默认主题时,肯定也希望该主题可被用户选择
api.request({ api.request({
url: `themeConfig:update/${item.id}`, url: `themeConfig:update/${item.id}`,
method: 'post', method: 'post',
data: { data: {
optional: true, optional: true,
default: true,
}, },
}), }),
]); ]);
} else { } else {
await updateSystemThemeSettings(null); await api.request({
url: `themeConfig:update/${item.id}`,
method: 'post',
data: {
default: false,
},
});
} }
} catch (err) { } catch (err) {
error(err); error(err);
@ -187,7 +170,7 @@ const ThemeCard = (props: Props) => {
message.success(t('Updated successfully')); message.success(t('Updated successfully'));
onChange?.({ type: HandleTypes.optional, item }); onChange?.({ type: HandleTypes.optional, item });
}, },
[api, item, onChange, t, updateSystemThemeSettings], [api, item, onChange, t],
); );
const handleEdit = useCallback(() => { const handleEdit = useCallback(() => {
@ -269,13 +252,13 @@ const ThemeCard = (props: Props) => {
}, [handleDelete, handleEdit, isDefault, menu, token.colorTextDisabled]); }, [handleDelete, handleEdit, isDefault, menu, token.colorTextDisabled]);
const extra = useMemo(() => { const extra = useMemo(() => {
if (item.id !== systemSettings?.data?.data?.options?.themeId && !item.optional) { if (item.id !== defaultThemeId && !item.optional) {
return null; return null;
} }
const text = const text =
item.id === currentThemeId item.id === currentThemeId
? t('Current') ? t('Current')
: item.id === systemSettings?.data?.data?.options?.themeId : item.id === defaultThemeId
? t('Default') ? t('Default')
: item.optional : item.optional
? t('Optional') ? t('Optional')
@ -283,7 +266,7 @@ const ThemeCard = (props: Props) => {
const color = const color =
item.id === currentThemeId item.id === currentThemeId
? 'processing' ? 'processing'
: item.id === systemSettings?.data?.data?.options?.themeId : item.id === defaultThemeId
? 'default' ? 'default'
: item.optional : item.optional
? 'success' ? 'success'
@ -294,7 +277,7 @@ const ThemeCard = (props: Props) => {
{text} {text}
</Tag> </Tag>
); );
}, [currentThemeId, item.id, item.optional, systemSettings?.data?.data?.options?.themeId, t]); }, [currentThemeId, defaultThemeId, item.id, item.optional, t]);
const cardStyle = useMemo(() => { const cardStyle = useMemo(() => {
if (getCurrentEditingTheme()?.id === item.id) { if (getCurrentEditingTheme()?.id === item.id) {

View File

@ -2,7 +2,7 @@ import { SelectWithTitle, useAPIClient, useCurrentUserContext, useSystemSettings
import { error } from '@nocobase/utils/client'; import { error } from '@nocobase/utils/client';
import { MenuProps } from 'antd'; import { MenuProps } from 'antd';
import React, { useEffect, useMemo } from 'react'; import React, { useEffect, useMemo } from 'react';
import { useCurrentThemeId } from '../components/InitializeTheme'; import { useThemeId } from '../components/InitializeTheme';
import { useThemeListContext } from '../components/ThemeListProvider'; import { useThemeListContext } from '../components/ThemeListProvider';
import { useTranslation } from '../locale'; import { useTranslation } from '../locale';
import { useUpdateThemeSettings } from './useUpdateThemeSettings'; import { useUpdateThemeSettings } from './useUpdateThemeSettings';
@ -21,11 +21,9 @@ function Label() {
const { t } = useTranslation(); const { t } = useTranslation();
const currentUser = useCurrentUserContext(); const currentUser = useCurrentUserContext();
const systemSettings = useSystemSettings(); const systemSettings = useSystemSettings();
const { run, error: err, data, refresh } = useThemeListContext(); const { run, error: err, data } = useThemeListContext();
const { updateUserThemeSettings, updateSystemThemeSettings } = useUpdateThemeSettings(); const { updateUserThemeSettings } = useUpdateThemeSettings();
const currentThemeId = useCurrentThemeId(); const { currentThemeId } = useThemeId();
const themeId = useCurrentThemeId();
const api = useAPIClient();
const options = useMemo(() => { const options = useMemo(() => {
return data return data
@ -44,25 +42,6 @@ function Label() {
} }
}, []); }, []);
useEffect(() => {
const init = async () => {
// 当 themeId 为空时表示插件是第一次被启用
if (themeId == null && data) {
const firstTheme = data[0];
try {
// 避免并发请求,在本地存储中容易出问题
await updateSystemThemeSettings(firstTheme.id);
await updateUserThemeSettings(firstTheme.id);
} catch (err) {
error(err);
}
}
};
init();
}, [themeId, updateSystemThemeSettings, updateUserThemeSettings, data, api, refresh]);
if (err) { if (err) {
error(err); error(err);
return null; return null;

View File

@ -37,33 +37,5 @@ export function useUpdateThemeSettings() {
[api, currentUser], [api, currentUser],
); );
const updateSystemThemeSettings = useCallback( return { updateUserThemeSettings };
async (themeId: number | null) => {
if (themeId === systemSettings.data.data.options?.themeId) {
return;
}
await api.request({
url: 'systemSettings:update/1',
method: 'post',
data: {
options: {
...(systemSettings.data.data.options || {}),
themeId,
},
},
});
systemSettings.mutate({
data: {
...systemSettings.data.data,
options: {
...(systemSettings.data.data.options || {}),
themeId,
},
},
});
},
[api, systemSettings],
);
return { updateUserThemeSettings, updateSystemThemeSettings };
} }

View File

@ -8,6 +8,7 @@ export const defaultTheme: Omit<ThemeItem, 'id'> = {
optional: true, optional: true,
isBuiltIn: true, isBuiltIn: true,
uid: 'default', uid: 'default',
default: true,
}; };
export const dark: Omit<ThemeItem, 'id'> = { export const dark: Omit<ThemeItem, 'id'> = {
@ -19,6 +20,7 @@ export const dark: Omit<ThemeItem, 'id'> = {
optional: true, optional: true,
isBuiltIn: true, isBuiltIn: true,
uid: 'dark', uid: 'dark',
default: false,
}; };
export const compact: Omit<ThemeItem, 'id'> = { export const compact: Omit<ThemeItem, 'id'> = {
@ -30,6 +32,7 @@ export const compact: Omit<ThemeItem, 'id'> = {
optional: true, optional: true,
isBuiltIn: true, isBuiltIn: true,
uid: 'compact', uid: 'compact',
default: false,
}; };
/** 同时包含 `紧凑` 和 `暗黑` 两种模式 */ /** 同时包含 `紧凑` 和 `暗黑` 两种模式 */
@ -42,4 +45,5 @@ export const compactDark: Omit<ThemeItem, 'id'> = {
optional: true, optional: true,
isBuiltIn: true, isBuiltIn: true,
uid: 'compact_dark', uid: 'compact_dark',
default: false,
}; };

View File

@ -0,0 +1,46 @@
import { Migration } from '@nocobase/server';
export default class ThemeEditorMigration extends Migration {
async up() {
const result = await this.app.version.satisfies('<0.17.0-alpha.5');
if (!result) {
return;
}
const repository = this.db.getRepository('themeConfig');
if (!repository) {
return;
}
await repository.collection.sync();
const systemSettings = await this.db.getRepository('systemSettings').findOne();
const defaultThemeId = systemSettings.options?.themeId;
if (!defaultThemeId) {
return;
}
await this.db.sequelize.transaction(async (t) => {
await repository.update({
values: {
default: false,
},
filter: {
default: true,
},
transaction: t,
});
await repository.update({
values: {
default: true,
optional: true,
},
filterByTk: defaultThemeId,
transaction: t,
});
});
}
async down() {}
}

View File

@ -7,15 +7,7 @@ export class ThemeEditorPlugin extends Plugin {
afterAdd() {} afterAdd() {}
async beforeLoad() { async beforeLoad() {}
this.db.addMigrations({
namespace: 'theme-editor',
directory: resolve(__dirname, './migrations'),
context: {
plugin: this,
},
});
}
async load() { async load() {
this.db.collection({ this.db.collection({
@ -39,8 +31,21 @@ export class ThemeEditorPlugin extends Plugin {
type: 'uid', type: 'uid',
name: 'uid', name: 'uid',
}, },
{
type: 'radio',
name: 'default',
defaultValue: false,
},
], ],
}); });
this.db.addMigrations({
namespace: 'theme-editor',
directory: resolve(__dirname, './migrations'),
context: {
plugin: this,
},
});
this.app.acl.allow('themeConfig', 'list', 'public'); this.app.acl.allow('themeConfig', 'list', 'public');
this.app.acl.registerSnippet({ this.app.acl.registerSnippet({
name: `pm.${this.name}.themeConfig`, name: `pm.${this.name}.themeConfig`,

View File

@ -10,6 +10,7 @@ export interface ThemeItem {
optional: boolean; optional: boolean;
isBuiltIn?: boolean; isBuiltIn?: boolean;
uid?: string; uid?: string;
default?: boolean; // 当前系统默认主题
} }
export type Theme = { export type Theme = {