被雨水过滤的空气-Rain 4812cc5692
feat: support custome themes (#2228)
* 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>
2023-07-16 12:46:25 +08:00

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}`);
}}
/>,
]}
/>
</>
);
};