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

View File

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

View File

@ -2,7 +2,7 @@ import { SelectWithTitle, useAPIClient, useCurrentUserContext, useSystemSettings
import { error } from '@nocobase/utils/client';
import { MenuProps } from 'antd';
import React, { useEffect, useMemo } from 'react';
import { useCurrentThemeId } from '../components/InitializeTheme';
import { useThemeId } from '../components/InitializeTheme';
import { useThemeListContext } from '../components/ThemeListProvider';
import { useTranslation } from '../locale';
import { useUpdateThemeSettings } from './useUpdateThemeSettings';
@ -21,11 +21,9 @@ function Label() {
const { t } = useTranslation();
const currentUser = useCurrentUserContext();
const systemSettings = useSystemSettings();
const { run, error: err, data, refresh } = useThemeListContext();
const { updateUserThemeSettings, updateSystemThemeSettings } = useUpdateThemeSettings();
const currentThemeId = useCurrentThemeId();
const themeId = useCurrentThemeId();
const api = useAPIClient();
const { run, error: err, data } = useThemeListContext();
const { updateUserThemeSettings } = useUpdateThemeSettings();
const { currentThemeId } = useThemeId();
const options = useMemo(() => {
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) {
error(err);
return null;

View File

@ -37,33 +37,5 @@ export function useUpdateThemeSettings() {
[api, currentUser],
);
const updateSystemThemeSettings = useCallback(
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 };
return { updateUserThemeSettings };
}

View File

@ -8,6 +8,7 @@ export const defaultTheme: Omit<ThemeItem, 'id'> = {
optional: true,
isBuiltIn: true,
uid: 'default',
default: true,
};
export const dark: Omit<ThemeItem, 'id'> = {
@ -19,6 +20,7 @@ export const dark: Omit<ThemeItem, 'id'> = {
optional: true,
isBuiltIn: true,
uid: 'dark',
default: false,
};
export const compact: Omit<ThemeItem, 'id'> = {
@ -30,6 +32,7 @@ export const compact: Omit<ThemeItem, 'id'> = {
optional: true,
isBuiltIn: true,
uid: 'compact',
default: false,
};
/** 同时包含 `紧凑` 和 `暗黑` 两种模式 */
@ -42,4 +45,5 @@ export const compactDark: Omit<ThemeItem, 'id'> = {
optional: true,
isBuiltIn: true,
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() {}
async beforeLoad() {
this.db.addMigrations({
namespace: 'theme-editor',
directory: resolve(__dirname, './migrations'),
context: {
plugin: this,
},
});
}
async beforeLoad() {}
async load() {
this.db.collection({
@ -39,8 +31,21 @@ export class ThemeEditorPlugin extends Plugin {
type: '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.registerSnippet({
name: `pm.${this.name}.themeConfig`,

View File

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