mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-07-01 18:52:20 +08:00
* feat: create custom-theme plugin * feat: add custom-theme * chore: add plugin name and description * chore: add deps * chore: optimize deps * refactor: rename * chore: add antd-token-previewer * chore: add deps in plugin * feat: add theme-editor * feat: add theme-editor * feat: add settings * feat: add theme collection * refactor: migration to the components folder * feat: add ThemeList * refactor: be better * feat: export createStyles * feat: implement ThemeCard (T-723) * style: optimize style * feat: add ThemeEditorProvider * feat: add ToEditTheme * chore: add isBuiltIn field * feat: implement WYSIWYG * refactor: migrate i18n * feat: support to save config * feat: add validation rule * refactor: remove useless code * refactor: optimize GlobalThemeProvider * feat: add CurrentUserSettingsMenuProvider * feat: support switching themes * refactor: migrate CurrentUserProvider to app root * feat: add InitializeTheme * fix: fix preview demo * fix: filter themes * fix: not valid when editing theme * fix: should restore the previous theme when closing theme editor * fix: fix algorithm * fix: the theme that was just saved should be applied * refactor: loacl antd-token-previewer * fix: should be based on the current theme when editing themes * feat: support to edit theme * perf: reduce executions * feat: add animation * fix: fix the type error of useRequest * feat: remove built-in themes * chore: reinstall deps * fix: fix version of deps * fix: delete client.d.ts to fix build error * chore: reinstall deps * fix: fix build * fix: fix build * fix: avoid build errors * fix: fix crashing * fix: use value instead of defaultValue * fix: avoid error * fix: avoid closure * fix: fix build * fix: fix style of login page * refactor(page): fix style * fix: fix style of PageHeader * refactor: fix style of Drawer * refactor: add FormDialog to loacl * refactor: fix style of SchemaSettings.ModalItem * refactor: fix style of pm/Card * fix: fix text color of pm/Marketplace * fix: fix table error * refactor: fix style of collection-manager/summary * refactor: fix style of fields drawer * chore: reinstall deps * fix: fix build * fix: fix build of custom-theme * fix: should export Plugin * refactor: fix style of GraphDrawPage * chore: upgrade plugin version * refactor: fix style of Modal by using antd App * refactor: fix style of FormDialog by using local version * refactor(workflow): refactor style using antd-style in workflow * fix(workflow): fix style of workflow * fix: fix size * refactor: add --nb-header-height * feat: remove theme configuration from system settings * refactor: migrate useUpdateThemeSettings to a new file * refactor: rename theme to themeId * feat: add updateSystemThemeSettings * refactor: migrate utils function * feat: use localStorage to avoid theme invalid in login page * fix: fix style of login page * fix: fix style of Drawer * feat: optimize style of theme card * fix: should use a empty object to reset theme * fix: fix test of Page * fix: fix test of Application * fix: change backgroundColor of login page * fix: fix all style of modal * fix: fix gap between blocks (T-896) * fix: fix color of font (T-905) * fix: fix build * fix: fix can not scroll in Drawer (T-897) * fix: fix width of built-in plugins page (T-900) * fix: fix style of import Modal (T-907) * fix: fix style of calendar (T-908) * fix: fix style of kanban (T-909) * fix: fix style of Gantt (T-910) * fix: fix style of Collapse (T-915) * fix: fix style of mobile (T-916) * fix: fix style of PageHeader (T-958) * fix: optimize background color of Drawer * fix: fix style of notification * fix: fix T-1000 * fix: fix style of LinkageHeader (T-1003) * fix: fix T-1004 * fix: fix style of scroll bar of chart (T-911) * fix: fix style of workflow drawer (T-997) * fix: fix T-922 * fix: fix T-924 * refactor: rename custom-theme to theme-editor * fix: fix T-999 * fix: fix T-1011 * fix: fix error * fix: optimize dark mode (T-921) * fix: fix style of markdown (T-1020) * fix: fix style of data template (T-1025) * fix: fix style of rich text (T-1026) * fix: fix style of a * fix: fix style of XButton (T-1028) * fix: fix T-1027 * fix: fix color of variable tag (T-1030) * chore: translation * feat: add a modal before create new theme (T-1024) * feat: highlight card when editing theme (T-1031) * feat: support to change theme name (T-1023) * fix: api.auth.getOption('theme') * fix: fix T-1032 * fix: fix feedback in feishu group * refactor: code review * fix: fix test * chore: rename * fix: useNotificationMiddleware * refactor: revert * fix: fix build * fix: notification * refactor: migrate CurrentUserProvider from Application to NocoBaseBuildInPlugin * fix: fix test * refactor: fix code review * chore: avoid test error --------- Co-authored-by: chenos <chenlinxh@gmail.com>
369 lines
9.4 KiB
TypeScript
369 lines
9.4 KiB
TypeScript
import { DeleteOutlined, SettingOutlined } from '@ant-design/icons';
|
|
import {
|
|
App,
|
|
Avatar,
|
|
Card,
|
|
Modal,
|
|
Popconfirm,
|
|
Spin,
|
|
Switch,
|
|
Tabs,
|
|
TabsProps,
|
|
Tag,
|
|
Tooltip,
|
|
Typography,
|
|
message,
|
|
} from 'antd';
|
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import type { IPluginData } from '.';
|
|
import { useAPIClient, useRequest } from '../api-client';
|
|
import { useStyles as useMarkdownStyles } from '../schema-component/antd/markdown/style';
|
|
import { useParseMarkdown } from '../schema-component/antd/markdown/util';
|
|
import { useStyles } from './style';
|
|
|
|
interface PluginDocumentProps {
|
|
path: string;
|
|
name: string;
|
|
}
|
|
|
|
interface ICommonCard {
|
|
onClick: () => void;
|
|
name: string;
|
|
description: string;
|
|
title: string;
|
|
displayName: string;
|
|
actions?: JSX.Element[];
|
|
}
|
|
|
|
interface IPluginDetail {
|
|
plugin: any;
|
|
onCancel: () => void;
|
|
items: TabsProps['items'];
|
|
}
|
|
|
|
/**
|
|
* get color by string
|
|
* TODO: real avatar
|
|
* @param str
|
|
*/
|
|
const stringToColor = function (str: string) {
|
|
let hash = 0;
|
|
for (let i = 0; i < str.length; i++) {
|
|
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
|
}
|
|
let color = '#';
|
|
for (let i = 0; i < 3; i++) {
|
|
const value = (hash >> (i * 8)) & 0xff;
|
|
color += ('00' + value.toString(16)).substr(-2);
|
|
}
|
|
return color;
|
|
};
|
|
|
|
const PluginDocument: React.FC<PluginDocumentProps> = (props) => {
|
|
const { styles } = useStyles();
|
|
const { componentCls, hashId } = useMarkdownStyles();
|
|
const [docLang, setDocLang] = useState('');
|
|
const { name, path } = props;
|
|
const { data, loading, error } = useRequest<{
|
|
data: {
|
|
content: string;
|
|
};
|
|
}>(
|
|
{
|
|
url: '/plugins:getTabInfo',
|
|
params: {
|
|
filterByTk: name,
|
|
path: path,
|
|
locale: docLang,
|
|
},
|
|
},
|
|
{
|
|
refreshDeps: [name, path, docLang],
|
|
},
|
|
);
|
|
const { html, loading: parseLoading } = useParseMarkdown(data?.data?.content);
|
|
|
|
const htmlWithOutRelativeDirect = useMemo(() => {
|
|
if (html) {
|
|
const pattern = /<a\s+href="\..*?\/([^/]+)"/g;
|
|
return html.replace(pattern, (match, $1) => match + `onclick="return false;"`); // prevent the default event of <a/>
|
|
}
|
|
}, [html]);
|
|
|
|
const handleSwitchDocLang = useCallback((e: MouseEvent) => {
|
|
const lang = (e.target as HTMLDivElement).innerHTML;
|
|
if (lang.trim() === '中文') {
|
|
setDocLang('zh-CN');
|
|
} else if (lang.trim() === 'English') {
|
|
setDocLang('en-US');
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const md = document.getElementById('pm-md-preview');
|
|
md.addEventListener('click', handleSwitchDocLang);
|
|
return () => {
|
|
removeEventListener('click', handleSwitchDocLang);
|
|
};
|
|
}, [handleSwitchDocLang]);
|
|
|
|
return (
|
|
<div className={styles.PluginDocument} id="pm-md-preview">
|
|
{loading || parseLoading ? (
|
|
<Spin />
|
|
) : (
|
|
<div
|
|
className={`${componentCls} ${hashId} nb-markdown nb-markdown-default nb-markdown-table`}
|
|
dangerouslySetInnerHTML={{ __html: error ? '' : htmlWithOutRelativeDirect }}
|
|
></div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
function PluginDetail(props: IPluginDetail) {
|
|
const { plugin, onCancel, items } = props;
|
|
const { styles } = useStyles();
|
|
|
|
return (
|
|
<Modal
|
|
footer={false}
|
|
className={styles.PluginDetail}
|
|
width="70%"
|
|
title={
|
|
<Typography.Title level={2} style={{ margin: 0 }}>
|
|
{plugin?.displayName || plugin?.name}
|
|
<Tag className={'version-tag'}>v{plugin?.version}</Tag>
|
|
</Typography.Title>
|
|
}
|
|
open={!!plugin}
|
|
onCancel={onCancel}
|
|
destroyOnClose
|
|
>
|
|
{plugin?.description && <div className={'plugin-desc'}>{plugin?.description}</div>}
|
|
<Tabs items={items} />
|
|
</Modal>
|
|
);
|
|
}
|
|
|
|
function CommonCard(props: ICommonCard) {
|
|
const { onClick, name, displayName, actions, description, title } = props;
|
|
const { styles } = useStyles();
|
|
|
|
return (
|
|
<Card
|
|
bordered={false}
|
|
className={styles.CommonCard}
|
|
onClick={onClick}
|
|
hoverable
|
|
// className={cls(css`
|
|
// &:hover {
|
|
// border: 1px solid var(--antd-wave-shadow-color);
|
|
// cursor: pointer;
|
|
// }
|
|
|
|
// border: 1px solid transparent;
|
|
// `)}
|
|
actions={actions}
|
|
// actions={[<a>Settings</a>, <a>Remove</a>, <Switch size={'small'} defaultChecked={true}></Switch>]}
|
|
>
|
|
<Card.Meta
|
|
className={styles.avatar}
|
|
avatar={<Avatar style={{ background: `${stringToColor(name)}` }}>{name?.[0]}</Avatar>}
|
|
description={
|
|
<Tooltip title={description} placement="bottom">
|
|
<div
|
|
style={{
|
|
overflow: 'hidden',
|
|
whiteSpace: 'nowrap',
|
|
textOverflow: 'ellipsis',
|
|
}}
|
|
>
|
|
{description || '-'}
|
|
</div>
|
|
</Tooltip>
|
|
}
|
|
title={
|
|
<span>
|
|
{displayName || name}
|
|
<span className={styles.version}>{title}</span>
|
|
</span>
|
|
}
|
|
/>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
export const PluginCard = (props: { data: IPluginData }) => {
|
|
const navigate = useNavigate();
|
|
const { data } = props;
|
|
const api = useAPIClient();
|
|
const { t } = useTranslation();
|
|
const { enabled, name, displayName, id, description, version } = data;
|
|
const [plugin, setPlugin] = useState<any>(null);
|
|
const { modal } = App.useApp();
|
|
const { data: tabsData, run } = useRequest<any>(
|
|
{
|
|
url: '/plugins:getTabs',
|
|
},
|
|
{
|
|
manual: true,
|
|
},
|
|
);
|
|
const items = useMemo<TabsProps['items']>(() => {
|
|
return tabsData?.data?.tabs.map((item) => {
|
|
return {
|
|
label: item.title,
|
|
key: item.path,
|
|
children: React.createElement(PluginDocument, {
|
|
name: tabsData?.data.filterByTk,
|
|
path: item.path,
|
|
}),
|
|
};
|
|
});
|
|
}, [tabsData?.data]);
|
|
|
|
const actions = useMemo(
|
|
() =>
|
|
[
|
|
enabled ? (
|
|
<SettingOutlined
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
navigate(`/admin/settings/${name}`);
|
|
}}
|
|
/>
|
|
) : null,
|
|
<Popconfirm
|
|
key={id}
|
|
title={t('Are you sure to delete this plugin?')}
|
|
onConfirm={async (e) => {
|
|
e.stopPropagation();
|
|
await api.request({
|
|
url: `pm:remove/${name}`,
|
|
});
|
|
message.success(t('插件删除成功'));
|
|
window.location.reload();
|
|
}}
|
|
onCancel={(e) => e.stopPropagation()}
|
|
okText={t('Yes')}
|
|
cancelText={t('No')}
|
|
>
|
|
<DeleteOutlined onClick={(e) => e.stopPropagation()} />
|
|
</Popconfirm>,
|
|
<Switch
|
|
key={id}
|
|
size={'small'}
|
|
onChange={async (checked, e) => {
|
|
e.stopPropagation();
|
|
modal.warning({
|
|
title: checked ? t('Plugin starting') : t('Plugin stopping'),
|
|
content: t('The application is reloading, please do not close the page.'),
|
|
okButtonProps: {
|
|
style: {
|
|
display: 'none',
|
|
},
|
|
},
|
|
});
|
|
await api.request({
|
|
url: `pm:${checked ? 'enable' : 'disable'}/${name}`,
|
|
});
|
|
window.location.reload();
|
|
// message.success(checked ? t('插件激活成功') : t('插件禁用成功'));
|
|
}}
|
|
defaultChecked={enabled}
|
|
></Switch>,
|
|
].filter(Boolean),
|
|
[api, enabled, navigate, id, name, t],
|
|
);
|
|
return (
|
|
<>
|
|
<PluginDetail plugin={plugin} onCancel={() => setPlugin(null)} items={items} />
|
|
<CommonCard
|
|
onClick={() => {
|
|
setPlugin(data);
|
|
run({
|
|
params: {
|
|
filterByTk: name,
|
|
},
|
|
});
|
|
}}
|
|
name={name}
|
|
description={description}
|
|
title={version}
|
|
actions={actions}
|
|
displayName={displayName}
|
|
/>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export const BuiltInPluginCard = (props: { data: IPluginData }) => {
|
|
const {
|
|
data: { description, name, version, displayName },
|
|
data,
|
|
} = props;
|
|
const navigate = useNavigate();
|
|
const [plugin, setPlugin] = useState<any>(null);
|
|
const { data: tabsData, run } = useRequest<{
|
|
data: {
|
|
tabs: {
|
|
title: string;
|
|
path: string;
|
|
}[];
|
|
filterByTk: string;
|
|
};
|
|
}>(
|
|
{
|
|
url: '/plugins:getTabs',
|
|
},
|
|
{
|
|
manual: true,
|
|
},
|
|
);
|
|
const items = useMemo(() => {
|
|
return tabsData?.data?.tabs.map((item) => {
|
|
return {
|
|
label: item.title,
|
|
key: item.path,
|
|
children: React.createElement(PluginDocument, {
|
|
name: tabsData?.data.filterByTk,
|
|
path: item.path,
|
|
}),
|
|
};
|
|
});
|
|
}, [tabsData?.data]);
|
|
|
|
return (
|
|
<>
|
|
<PluginDetail plugin={plugin} onCancel={() => setPlugin(null)} items={items} />
|
|
<CommonCard
|
|
onClick={() => {
|
|
setPlugin(data);
|
|
run({
|
|
params: {
|
|
filterByTk: name,
|
|
},
|
|
});
|
|
}}
|
|
name={name}
|
|
displayName={displayName}
|
|
description={description}
|
|
title={version}
|
|
actions={[
|
|
<div key="placeholder-comp"></div>,
|
|
<SettingOutlined
|
|
key="settings"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
navigate(`/admin/settings/${name}`);
|
|
}}
|
|
/>,
|
|
]}
|
|
/>
|
|
</>
|
|
);
|
|
};
|