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:
parent
7166409c75
commit
8c1738db83
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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';
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
|
@ -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 };
|
||||
}
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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() {}
|
||||
}
|
@ -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`,
|
||||
|
@ -10,6 +10,7 @@ export interface ThemeItem {
|
||||
optional: boolean;
|
||||
isBuiltIn?: boolean;
|
||||
uid?: string;
|
||||
default?: boolean; // 当前系统默认主题
|
||||
}
|
||||
|
||||
export type Theme = {
|
||||
|
Loading…
Reference in New Issue
Block a user