feat: block templates (#240)
* feat: block templates * feat: improve code * feat: improve code * feat: schema settings for block templates * feat: improve code * feat: transition * feat: transition
This commit is contained in:
parent
5c92d3ba46
commit
2fb019c392
packages
app/src/pages
client/src
index.tsxSchemaInitializer.tsxtypes.ts
locale
route-switch
schema-component
antd
calendar
form
kanban
table
hooks
schema-initializer
Initializers
Items
CalendarBlockInitializer.tsxFormBlockInitializer.tsxKanbanBlockInitializer.tsxTableBlockInitializer.tsx
utils.tsschema-settings
schema-templates
plugin-ui-routes-storage/src
plugin-ui-schema-storage/src/collections
@ -5,6 +5,8 @@ import {
|
||||
AntdSchemaComponentProvider,
|
||||
APIClientProvider,
|
||||
AuthLayout,
|
||||
BlockTemplateDetails,
|
||||
BlockTemplatePage,
|
||||
ChinaRegionProvider,
|
||||
CollectionManagerShortcut,
|
||||
compose,
|
||||
@ -13,19 +15,20 @@ import {
|
||||
i18n,
|
||||
MenuItemInitializers,
|
||||
PluginManagerProvider,
|
||||
RemoteRouteSwitchProvider,
|
||||
// RemoteCollectionManagerProvider,
|
||||
RouteSchemaComponent,
|
||||
RouteSwitch,
|
||||
RouteSwitchProvider,
|
||||
SchemaComponentProvider,
|
||||
SchemaInitializerProvider,
|
||||
SchemaTemplateShortcut,
|
||||
SigninPage,
|
||||
SignupPage,
|
||||
SystemSettingsProvider,
|
||||
SystemSettingsShortcut,
|
||||
useRequest
|
||||
useRoutes
|
||||
} from '@nocobase/client';
|
||||
import { notification, Spin } from 'antd';
|
||||
import { notification } from 'antd';
|
||||
import 'antd/dist/antd.css';
|
||||
import React from 'react';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
@ -50,6 +53,20 @@ const providers = [
|
||||
[APIClientProvider, { apiClient }],
|
||||
[I18nextProvider, { i18n }],
|
||||
[AntdConfigProvider, { remoteLocale: true }],
|
||||
[
|
||||
RemoteRouteSwitchProvider,
|
||||
{
|
||||
components: {
|
||||
AuthLayout,
|
||||
AdminLayout,
|
||||
RouteSchemaComponent,
|
||||
SigninPage,
|
||||
SignupPage,
|
||||
BlockTemplatePage,
|
||||
BlockTemplateDetails,
|
||||
},
|
||||
},
|
||||
],
|
||||
SystemSettingsProvider,
|
||||
[
|
||||
PluginManagerProvider,
|
||||
@ -59,39 +76,30 @@ const providers = [
|
||||
DesignableSwitch,
|
||||
CollectionManagerShortcut,
|
||||
SystemSettingsShortcut,
|
||||
SchemaTemplateShortcut,
|
||||
},
|
||||
},
|
||||
],
|
||||
[SchemaComponentProvider, { components: { Link, NavLink } }],
|
||||
// RemoteCollectionManagerProvider,
|
||||
[SchemaInitializerProvider, { initializers: { MenuItemInitializers } }],
|
||||
AntdSchemaComponentProvider,
|
||||
ChinaRegionProvider,
|
||||
[DocumentTitleProvider, { addonAfter: 'NocoBase' }],
|
||||
[
|
||||
RouteSwitchProvider,
|
||||
SchemaInitializerProvider,
|
||||
{
|
||||
components: {
|
||||
AuthLayout,
|
||||
AdminLayout,
|
||||
RouteSchemaComponent,
|
||||
SigninPage,
|
||||
SignupPage,
|
||||
initializers: {
|
||||
MenuItemInitializers,
|
||||
},
|
||||
},
|
||||
],
|
||||
AntdSchemaComponentProvider,
|
||||
ChinaRegionProvider,
|
||||
[DocumentTitleProvider, { addonAfter: 'NocoBase' }],
|
||||
];
|
||||
|
||||
const App = compose(...providers)(() => {
|
||||
const { data, loading } = useRequest({
|
||||
url: 'uiRoutes:getAccessible',
|
||||
});
|
||||
if (loading) {
|
||||
return <Spin />;
|
||||
}
|
||||
const routes = useRoutes();
|
||||
return (
|
||||
<div>
|
||||
<RouteSwitch routes={data?.data || []} />
|
||||
<RouteSwitch routes={routes} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
@ -19,6 +19,7 @@ export * from './route-switch';
|
||||
export * from './schema-component';
|
||||
export * from './schema-initializer';
|
||||
export * from './schema-settings';
|
||||
export * from './schema-templates';
|
||||
export * from './settings-form';
|
||||
export * from './system-settings';
|
||||
export * from './user';
|
||||
|
@ -279,5 +279,10 @@
|
||||
|
||||
"Add card": "添加卡片",
|
||||
"edit title": "修改标题",
|
||||
"turn page": "翻页"
|
||||
"turn page": "翻页",
|
||||
"Save as template": "保存为模板",
|
||||
"Block templates": "区块模板",
|
||||
"Convert reference to duplicate": "模板引用转为复制",
|
||||
"Template name": "模板名称",
|
||||
"Block type": "区块类型"
|
||||
}
|
||||
|
@ -1,8 +1,20 @@
|
||||
import { Spin } from 'antd';
|
||||
import React from 'react';
|
||||
import { useRequest } from '../api-client';
|
||||
import { RouteSwitchContext } from './context';
|
||||
import { RouteSwitchProviderProps } from './types';
|
||||
|
||||
export function RouteSwitchProvider(props: RouteSwitchProviderProps) {
|
||||
const { children, components } = props;
|
||||
return <RouteSwitchContext.Provider value={{ components }}>{children}</RouteSwitchContext.Provider>;
|
||||
const { children, components, routes } = props;
|
||||
return <RouteSwitchContext.Provider value={{ routes, components }}>{children}</RouteSwitchContext.Provider>;
|
||||
}
|
||||
|
||||
export function RemoteRouteSwitchProvider(props: RouteSwitchProviderProps) {
|
||||
const { data, loading } = useRequest({
|
||||
url: 'uiRoutes:getAccessible',
|
||||
});
|
||||
if (loading) {
|
||||
return <Spin />;
|
||||
}
|
||||
return <RouteSwitchProvider {...props} routes={data?.data || []} />;
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
PluginManager,
|
||||
RemoteCollectionManagerProvider,
|
||||
RemoteSchemaComponent,
|
||||
RemoteSchemaTemplateManagerProvider,
|
||||
useDocumentTitle,
|
||||
useRoute,
|
||||
useSystemSettings
|
||||
@ -94,6 +95,7 @@ const InternalAdminLayout = (props: any) => {
|
||||
{ component: 'DesignableSwitch', pin: true },
|
||||
{ component: 'CollectionManagerShortcut', pin: true },
|
||||
{ component: 'ACLShortcut', pin: true },
|
||||
{ component: 'SchemaTemplateShortcut', pin: true },
|
||||
{ component: 'SystemSettingsShortcut' },
|
||||
]}
|
||||
/>
|
||||
@ -110,11 +112,13 @@ const InternalAdminLayout = (props: any) => {
|
||||
|
||||
export const AdminLayout = (props) => {
|
||||
return (
|
||||
<RemoteCollectionManagerProvider>
|
||||
<CurrentUserProvider>
|
||||
<InternalAdminLayout {...props} />
|
||||
</CurrentUserProvider>
|
||||
</RemoteCollectionManagerProvider>
|
||||
<RemoteSchemaTemplateManagerProvider>
|
||||
<RemoteCollectionManagerProvider>
|
||||
<CurrentUserProvider>
|
||||
<InternalAdminLayout {...props} />
|
||||
</CurrentUserProvider>
|
||||
</RemoteCollectionManagerProvider>
|
||||
</RemoteSchemaTemplateManagerProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -3,6 +3,7 @@ import { RouteProps } from './types';
|
||||
|
||||
export const RouteSwitchContext = createContext({
|
||||
components: {},
|
||||
routes: [],
|
||||
});
|
||||
|
||||
export const RouteContext = createContext<RouteProps>(null);
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { useContext } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useContext } from 'react';
|
||||
import { RouteContext, RouteSwitchContext } from './context';
|
||||
|
||||
export function useRouteComponent(name?: string) {
|
||||
@ -13,3 +13,8 @@ export function useRouteComponent(name?: string) {
|
||||
export function useRoute() {
|
||||
return useContext(RouteContext);
|
||||
}
|
||||
|
||||
export function useRoutes() {
|
||||
const { routes } = useContext(RouteSwitchContext);
|
||||
return routes || [];
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ export interface RouteProps {
|
||||
export interface RouteSwitchProviderProps {
|
||||
components?: any;
|
||||
children?: any;
|
||||
routes?: RouteRedirectProps[];
|
||||
}
|
||||
|
||||
export type RouteRedirectProps = RedirectProps | RouteProps;
|
||||
|
@ -4,6 +4,7 @@ import { useCompile, useDesignable } from '../..';
|
||||
import { useCollection, useResourceActionContext } from '../../../collection-manager';
|
||||
import { useCollectionFilterOptions } from '../../../collection-manager/action-hooks';
|
||||
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
|
||||
import { useSchemaTemplate } from '../../../schema-templates';
|
||||
|
||||
export const CalendarDesigner = () => {
|
||||
const field = useField();
|
||||
@ -13,6 +14,7 @@ export const CalendarDesigner = () => {
|
||||
const ctx = useResourceActionContext();
|
||||
const { dn } = useDesignable();
|
||||
const compile = useCompile();
|
||||
const template = useSchemaTemplate();
|
||||
const defaultFilter = fieldSchema?.['x-decorator-props']?.request?.params?.filter || {};
|
||||
const options = fields?.map((field) => {
|
||||
return {
|
||||
@ -23,7 +25,7 @@ export const CalendarDesigner = () => {
|
||||
const calendarSchema = fieldSchema.properties.calendar;
|
||||
const fieldNames = calendarSchema?.['x-component-props']?.['fieldNames'] || {};
|
||||
return (
|
||||
<GeneralSchemaDesigner title={title || name}>
|
||||
<GeneralSchemaDesigner template={template} title={title || name}>
|
||||
<SchemaSettings.SelectItem
|
||||
title={'标题字段'}
|
||||
value={fieldNames.title}
|
||||
@ -128,6 +130,8 @@ export const CalendarDesigner = () => {
|
||||
}}
|
||||
/>
|
||||
<SchemaSettings.Divider />
|
||||
<SchemaSettings.Template componentName={'Calendar'} collectionName={name} />
|
||||
<SchemaSettings.Divider />
|
||||
<SchemaSettings.Remove
|
||||
removeParentsIfNoChildren
|
||||
breakRemoveOn={{
|
||||
|
@ -6,6 +6,8 @@ export const FormDesigner = () => {
|
||||
const { name, title } = useCollection();
|
||||
return (
|
||||
<GeneralSchemaDesigner title={title || name}>
|
||||
<SchemaSettings.Template componentName={'Form'} collectionName={name} />
|
||||
<SchemaSettings.Divider />
|
||||
<SchemaSettings.Remove
|
||||
removeParentsIfNoChildren
|
||||
breakRemoveOn={{
|
||||
|
@ -8,6 +8,7 @@ import { useAttach, useComponent } from '../..';
|
||||
import { useRequest } from '../../../api-client';
|
||||
import { useCollection } from '../../../collection-manager';
|
||||
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
|
||||
import { useSchemaTemplate } from '../../../schema-templates';
|
||||
|
||||
type Opts = Options<any, any> & { uid?: string };
|
||||
|
||||
@ -113,8 +114,11 @@ export const Form: React.FC<FormProps> & { Designer?: any } = observer((props) =
|
||||
|
||||
Form.Designer = () => {
|
||||
const { name, title } = useCollection();
|
||||
const template = useSchemaTemplate();
|
||||
return (
|
||||
<GeneralSchemaDesigner title={title || name}>
|
||||
<GeneralSchemaDesigner template={template} title={title || name}>
|
||||
<SchemaSettings.Template componentName={'Form'} collectionName={name} />
|
||||
<SchemaSettings.Divider />
|
||||
<SchemaSettings.Remove
|
||||
removeParentsIfNoChildren
|
||||
breakRemoveOn={{
|
||||
|
@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useCollection, useResourceActionContext } from '../../../collection-manager';
|
||||
import { useCollectionFilterOptions } from '../../../collection-manager/action-hooks';
|
||||
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
|
||||
import { useSchemaTemplate } from '../../../schema-templates';
|
||||
import { useDesignable } from '../../hooks';
|
||||
|
||||
export const KanbanDesigner = () => {
|
||||
@ -15,8 +16,9 @@ export const KanbanDesigner = () => {
|
||||
const { t } = useTranslation();
|
||||
const { dn } = useDesignable();
|
||||
const defaultFilter = fieldSchema?.['x-decorator-props']?.request?.params?.filter || {};
|
||||
const template = useSchemaTemplate();
|
||||
return (
|
||||
<GeneralSchemaDesigner title={title || name}>
|
||||
<GeneralSchemaDesigner template={template} title={title || name}>
|
||||
<SchemaSettings.ModalItem
|
||||
title={'设置数据范围'}
|
||||
schema={
|
||||
@ -49,6 +51,8 @@ export const KanbanDesigner = () => {
|
||||
}}
|
||||
/>
|
||||
<SchemaSettings.Divider />
|
||||
<SchemaSettings.Template componentName={'Kanban'} collectionName={name} />
|
||||
<SchemaSettings.Divider />
|
||||
<SchemaSettings.Remove
|
||||
removeParentsIfNoChildren
|
||||
breakRemoveOn={{
|
||||
|
@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useCollection, useResourceActionContext } from '../../../collection-manager';
|
||||
import { useCollectionFilterOptions, useSortFields } from '../../../collection-manager/action-hooks';
|
||||
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
|
||||
import { useSchemaTemplate } from '../../../schema-templates';
|
||||
import { useDesignable } from '../../hooks';
|
||||
|
||||
export const TableVoidDesigner = () => {
|
||||
@ -29,8 +30,9 @@ export const TableVoidDesigner = () => {
|
||||
direction: 'asc',
|
||||
};
|
||||
});
|
||||
const template = useSchemaTemplate();
|
||||
return (
|
||||
<GeneralSchemaDesigner title={title || name}>
|
||||
<GeneralSchemaDesigner template={template} title={title || name}>
|
||||
<SchemaSettings.SwitchItem
|
||||
title={t('Enable drag and drop sorting')}
|
||||
checked={field.decoratorProps.dragSort}
|
||||
@ -193,6 +195,8 @@ export const TableVoidDesigner = () => {
|
||||
}}
|
||||
/>
|
||||
<SchemaSettings.Divider />
|
||||
<SchemaSettings.Template componentName={'Table'} collectionName={name}/>
|
||||
<SchemaSettings.Divider />
|
||||
<SchemaSettings.Remove
|
||||
removeParentsIfNoChildren
|
||||
breakRemoveOn={{
|
||||
|
@ -11,6 +11,7 @@ interface CreateDesignableProps {
|
||||
current: Schema;
|
||||
api?: APIClient;
|
||||
refresh?: () => void;
|
||||
onSuccess?: any;
|
||||
}
|
||||
|
||||
export function createDesignable(options: CreateDesignableProps) {
|
||||
@ -26,6 +27,7 @@ interface InsertAdjacentOptions {
|
||||
wrap?: (s: ISchema) => ISchema;
|
||||
removeParentsIfNoChildren?: boolean;
|
||||
breakRemoveOn?: ISchema | BreakFn;
|
||||
onSuccess?: any;
|
||||
}
|
||||
|
||||
type BreakFn = (s: ISchema) => boolean;
|
||||
@ -80,7 +82,7 @@ export class Designable {
|
||||
if (!api) {
|
||||
return;
|
||||
}
|
||||
this.on('insertAdjacent', async ({ current, position, schema, removed }) => {
|
||||
this.on('insertAdjacent', async ({ onSuccess, current, position, schema, removed }) => {
|
||||
refresh();
|
||||
await api.request({
|
||||
url: `/uiSchemas:insertAdjacent/${current['x-uid']}?position=${position}`,
|
||||
@ -93,6 +95,7 @@ export class Designable {
|
||||
method: 'post',
|
||||
});
|
||||
}
|
||||
onSuccess?.();
|
||||
message.success('配置保存成功!', 0.2);
|
||||
});
|
||||
this.on('patch', async ({ schema }) => {
|
||||
@ -229,6 +232,19 @@ export class Designable {
|
||||
this.emit('remove', { removed });
|
||||
}
|
||||
|
||||
removeWithoutEmit(schema?: Schema, options: RemoveOptions = {}) {
|
||||
const { breakRemoveOn, removeParentsIfNoChildren } = options;
|
||||
let s = schema || this.current;
|
||||
let removed = s.parent.removeProperty(s.name);
|
||||
if (removeParentsIfNoChildren) {
|
||||
const parent = this.recursiveRemoveIfNoChildren(s.parent, { breakRemoveOn });
|
||||
if (parent) {
|
||||
removed = parent;
|
||||
}
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
insertBeforeBeginOrAfterEnd(schema: ISchema, options: InsertAdjacentOptions = {}) {
|
||||
if (!Schema.isSchemaInstance(this.current)) {
|
||||
return;
|
||||
@ -393,7 +409,7 @@ export class Designable {
|
||||
if (!Schema.isSchemaInstance(this.current)) {
|
||||
return;
|
||||
}
|
||||
const opts = {};
|
||||
const opts = { onSuccess: options?.onSuccess };
|
||||
const { wrap = defaultWrap, breakRemoveOn, removeParentsIfNoChildren } = options;
|
||||
if (Schema.isSchemaInstance(schema)) {
|
||||
if (this.parentsIn(schema)) {
|
||||
|
@ -5,7 +5,9 @@ import React, { useContext } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useCollectionManager } from '../../../collection-manager';
|
||||
import { SchemaComponent, SchemaComponentOptions } from '../../../schema-component';
|
||||
import { useSchemaTemplateManager } from '../../../schema-templates';
|
||||
import { SchemaInitializer } from '../../SchemaInitializer';
|
||||
import { useCollectionDataSourceItems } from '../utils';
|
||||
|
||||
const createSchema = (collectionName, { title, start, end }) => {
|
||||
const schema: ISchema = {
|
||||
@ -102,11 +104,17 @@ export const CalendarBlockInitializer = (props) => {
|
||||
const { collections, getCollection } = useCollectionManager();
|
||||
const { t } = useTranslation();
|
||||
const options = useContext(SchemaOptionsContext);
|
||||
const { getTemplateSchemaByMode } = useSchemaTemplateManager();
|
||||
return (
|
||||
<SchemaInitializer.Item
|
||||
{...props}
|
||||
icon={<FormOutlined />}
|
||||
onClick={async ({ item }) => {
|
||||
if (item.template) {
|
||||
const s = await getTemplateSchemaByMode(item);
|
||||
insert(s);
|
||||
return;
|
||||
}
|
||||
const collection = getCollection(item.name);
|
||||
const stringFields = collection?.fields
|
||||
?.filter((field) => field.type === 'string')
|
||||
@ -161,23 +169,10 @@ export const CalendarBlockInitializer = (props) => {
|
||||
}).open({
|
||||
initialValues: {},
|
||||
});
|
||||
|
||||
insert(createSchema(item.name, values));
|
||||
}}
|
||||
items={[
|
||||
{
|
||||
type: 'itemGroup',
|
||||
title: t('Select data source'),
|
||||
children: collections
|
||||
?.filter((item) => !item.inherit)
|
||||
?.map((item) => {
|
||||
return {
|
||||
type: 'item',
|
||||
name: item.name,
|
||||
title: item.title,
|
||||
};
|
||||
}),
|
||||
},
|
||||
]}
|
||||
items={useCollectionDataSourceItems('Calendar')}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -4,6 +4,8 @@ import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SchemaInitializer } from '../..';
|
||||
import { useCollectionManager } from '../../../collection-manager';
|
||||
import { useSchemaTemplateManager } from '../../../schema-templates';
|
||||
import { useCollectionDataSourceItems } from '../utils';
|
||||
|
||||
const createSchema = (collectionName) => {
|
||||
const schema: ISchema = {
|
||||
@ -55,28 +57,20 @@ export const FormBlockInitializer = (props) => {
|
||||
const { insert } = props;
|
||||
const { collections } = useCollectionManager();
|
||||
const { t } = useTranslation();
|
||||
const { getTemplateSchemaByMode } = useSchemaTemplateManager();
|
||||
return (
|
||||
<SchemaInitializer.Item
|
||||
{...props}
|
||||
icon={<FormOutlined />}
|
||||
onClick={({ item }) => {
|
||||
insert(createSchema(item.name));
|
||||
onClick={async ({ item }) => {
|
||||
if (item.template) {
|
||||
const s = await getTemplateSchemaByMode(item);
|
||||
insert(s);
|
||||
} else {
|
||||
insert(createSchema(item.name));
|
||||
}
|
||||
}}
|
||||
items={[
|
||||
{
|
||||
type: 'itemGroup',
|
||||
title: t('Select data source'),
|
||||
children: collections
|
||||
?.filter((item) => !item.inherit)
|
||||
?.map((item) => {
|
||||
return {
|
||||
type: 'item',
|
||||
name: item.name,
|
||||
title: item.title,
|
||||
};
|
||||
}),
|
||||
},
|
||||
]}
|
||||
items={useCollectionDataSourceItems('Form')}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -6,7 +6,9 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useAPIClient } from '../../../api-client';
|
||||
import { useCollectionManager } from '../../../collection-manager';
|
||||
import { SchemaComponent, SchemaComponentOptions } from '../../../schema-component';
|
||||
import { useSchemaTemplateManager } from '../../../schema-templates';
|
||||
import { SchemaInitializer } from '../../SchemaInitializer';
|
||||
import { useCollectionDataSourceItems } from '../utils';
|
||||
|
||||
const createSchema = (collectionName, { groupField, sortName }) => {
|
||||
const schema: ISchema = {
|
||||
@ -158,6 +160,7 @@ export const KanbanBlockInitializer = (props) => {
|
||||
const { insert } = props;
|
||||
const { collections, getCollection } = useCollectionManager();
|
||||
const { t } = useTranslation();
|
||||
const { getTemplateSchemaByMode } = useSchemaTemplateManager();
|
||||
const options = useContext(SchemaOptionsContext);
|
||||
const api = useAPIClient();
|
||||
return (
|
||||
@ -165,6 +168,11 @@ export const KanbanBlockInitializer = (props) => {
|
||||
{...props}
|
||||
icon={<FormOutlined />}
|
||||
onClick={async ({ item }) => {
|
||||
if (item.template) {
|
||||
const s = await getTemplateSchemaByMode(item);
|
||||
insert(s);
|
||||
return;
|
||||
}
|
||||
const collection = getCollection(item.name);
|
||||
const fields = collection?.fields
|
||||
?.filter((field) => ['select', 'radioGroup'].includes(field.interface))
|
||||
@ -219,21 +227,7 @@ export const KanbanBlockInitializer = (props) => {
|
||||
}
|
||||
insert(createSchema(item.name, { ...values, sortName }));
|
||||
}}
|
||||
items={[
|
||||
{
|
||||
type: 'itemGroup',
|
||||
title: t('Select data source'),
|
||||
children: collections
|
||||
?.filter((item) => !item.inherit)
|
||||
?.map((item) => {
|
||||
return {
|
||||
type: 'item',
|
||||
name: item.name,
|
||||
title: item.title,
|
||||
};
|
||||
}),
|
||||
},
|
||||
]}
|
||||
items={useCollectionDataSourceItems('Kanban')}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { TableOutlined } from '@ant-design/icons';
|
||||
import { ISchema } from '@formily/react';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SchemaInitializer } from '../..';
|
||||
import { useCollectionManager } from '../../../collection-manager';
|
||||
import { useSchemaTemplateManager } from '../../../schema-templates';
|
||||
import { useCollectionDataSourceItems } from '../utils';
|
||||
|
||||
const createSchema = (collectionName) => {
|
||||
export const createTableBlockSchema = (collectionName) => {
|
||||
const schema: ISchema = {
|
||||
type: 'void',
|
||||
'x-collection': 'collections',
|
||||
@ -79,30 +79,20 @@ const createSchema = (collectionName) => {
|
||||
|
||||
export const TableBlockInitializer = (props) => {
|
||||
const { insert } = props;
|
||||
const { collections } = useCollectionManager();
|
||||
const { t } = useTranslation();
|
||||
const { getTemplateSchemaByMode } = useSchemaTemplateManager();
|
||||
return (
|
||||
<SchemaInitializer.Item
|
||||
{...props}
|
||||
icon={<TableOutlined />}
|
||||
onClick={({ item }) => {
|
||||
insert(createSchema(item.name));
|
||||
onClick={async ({ item }) => {
|
||||
if (item.template) {
|
||||
const s = await getTemplateSchemaByMode(item);
|
||||
insert(s);
|
||||
} else {
|
||||
insert(createTableBlockSchema(item.name));
|
||||
}
|
||||
}}
|
||||
items={[
|
||||
{
|
||||
type: 'itemGroup',
|
||||
title: t('Select data source'),
|
||||
children: collections
|
||||
?.filter((item) => !item.inherit)
|
||||
?.map((item) => {
|
||||
return {
|
||||
type: 'item',
|
||||
name: item.name,
|
||||
title: item.title,
|
||||
};
|
||||
}),
|
||||
},
|
||||
]}
|
||||
items={useCollectionDataSourceItems('Table')}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -1,8 +1,10 @@
|
||||
import { ISchema, Schema, useFieldSchema } from '@formily/react';
|
||||
import { uid } from '@formily/shared';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SchemaInitializerItemOptions } from '../';
|
||||
import { useCollection } from '../../collection-manager';
|
||||
import { useCollection, useCollectionManager } from '../../collection-manager';
|
||||
import { useDesignable } from '../../schema-component';
|
||||
import { useSchemaTemplateManager } from '../../schema-templates';
|
||||
|
||||
export const gridRowColWrap = (schema: ISchema) => {
|
||||
return {
|
||||
@ -203,3 +205,76 @@ export const useCurrentSchema = (action: string, key: string, find = findSchema,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const useCollectionDataSourceItems = (componentName) => {
|
||||
const { t } = useTranslation();
|
||||
const { collections } = useCollectionManager();
|
||||
const { getTemplatesByCollection } = useSchemaTemplateManager();
|
||||
return [
|
||||
{
|
||||
key: 'tableBlock',
|
||||
type: 'itemGroup',
|
||||
title: t('Select data source'),
|
||||
children: collections
|
||||
?.filter((item) => !item.inherit)
|
||||
?.map((item, index) => {
|
||||
const templates = getTemplatesByCollection(item.name).filter((template) => {
|
||||
return componentName && template.componentName === componentName;
|
||||
});
|
||||
if (!templates.length) {
|
||||
return {
|
||||
type: 'item',
|
||||
name: item.name,
|
||||
title: item.title,
|
||||
};
|
||||
}
|
||||
return {
|
||||
key: `${componentName}_table_subMenu_${index}`,
|
||||
type: 'subMenu',
|
||||
name: `${item.name}_${index}`,
|
||||
title: item.title,
|
||||
children: [
|
||||
{
|
||||
type: 'item',
|
||||
name: item.name,
|
||||
title: '空白区块',
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
key: `${componentName}_table_subMenu_${index}_copy`,
|
||||
type: 'subMenu',
|
||||
name: 'copy',
|
||||
title: '复制模板',
|
||||
children: templates.map((template) => {
|
||||
return {
|
||||
type: 'item',
|
||||
mode: 'copy',
|
||||
name: item.name,
|
||||
template,
|
||||
title: template.name || '未命名',
|
||||
};
|
||||
}),
|
||||
},
|
||||
{
|
||||
key: `${componentName}_table_subMenu_${index}_ref`,
|
||||
type: 'subMenu',
|
||||
name: 'ref',
|
||||
title: '引用模板',
|
||||
children: templates.map((template) => {
|
||||
return {
|
||||
type: 'item',
|
||||
mode: 'reference',
|
||||
name: item.name,
|
||||
template,
|
||||
title: template.name || '未命名',
|
||||
};
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
}),
|
||||
},
|
||||
];
|
||||
};
|
||||
|
@ -42,76 +42,56 @@ SchemaInitializer.Button = observer((props: SchemaInitializerButtonProps) => {
|
||||
insertAdjacent(insertPosition, wrap(schema));
|
||||
}
|
||||
};
|
||||
const menu = (
|
||||
<Menu>
|
||||
{items?.map((item, indexA) => {
|
||||
if (item.type === 'divider') {
|
||||
return <Menu.Divider key={`item-${indexA}`} />;
|
||||
}
|
||||
if (item.type === 'item' && item.component) {
|
||||
const Component = findComponent(item.component);
|
||||
return (
|
||||
Component && (
|
||||
<SchemaInitializerItemContext.Provider
|
||||
key={`item-${indexA}`}
|
||||
value={{
|
||||
index: indexA,
|
||||
item,
|
||||
info: item,
|
||||
insert: insertSchema,
|
||||
}}
|
||||
>
|
||||
<Component
|
||||
{...item}
|
||||
item={{
|
||||
...item,
|
||||
title: compile(item.title),
|
||||
}}
|
||||
insert={insertSchema}
|
||||
/>
|
||||
</SchemaInitializerItemContext.Provider>
|
||||
)
|
||||
);
|
||||
}
|
||||
const renderItems = (items: any) => {
|
||||
return items?.map((item, indexA) => {
|
||||
if (item.type === 'divider') {
|
||||
return <Menu.Divider key={item.key || `item-${indexA}`} />;
|
||||
}
|
||||
if (item.type === 'item' && item.component) {
|
||||
const Component = findComponent(item.component);
|
||||
return (
|
||||
item?.children?.length > 0 && (
|
||||
<Menu.ItemGroup key={`item-group-${indexA}`} title={compile(item.title)}>
|
||||
{item?.children?.map((child, indexB) => {
|
||||
if (!child.component) {
|
||||
return null;
|
||||
}
|
||||
const Component = findComponent(child.component);
|
||||
return (
|
||||
Component && (
|
||||
<SchemaInitializerItemContext.Provider
|
||||
key={`item-${indexB}`}
|
||||
value={{
|
||||
index: indexB,
|
||||
item: child,
|
||||
info: child,
|
||||
insert: insertSchema,
|
||||
}}
|
||||
>
|
||||
<Component
|
||||
{...child}
|
||||
item={{
|
||||
...child,
|
||||
title: compile(child.title),
|
||||
}}
|
||||
insert={insertSchema}
|
||||
/>
|
||||
</SchemaInitializerItemContext.Provider>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</Menu.ItemGroup>
|
||||
Component && (
|
||||
<SchemaInitializerItemContext.Provider
|
||||
key={`item-${indexA}`}
|
||||
value={{
|
||||
index: indexA,
|
||||
item,
|
||||
info: item,
|
||||
insert: insertSchema,
|
||||
}}
|
||||
>
|
||||
<Component
|
||||
{...item}
|
||||
item={{
|
||||
...item,
|
||||
title: compile(item.title),
|
||||
}}
|
||||
insert={insertSchema}
|
||||
/>
|
||||
</SchemaInitializerItemContext.Provider>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
if (item.type === 'itemGroup') {
|
||||
return (
|
||||
<Menu.ItemGroup key={item.key || `item-group-${indexA}`} title={compile(item.title)}>
|
||||
{renderItems(item.children)}
|
||||
</Menu.ItemGroup>
|
||||
);
|
||||
}
|
||||
if (item.type === 'subMenu') {
|
||||
return (
|
||||
<Menu.SubMenu key={item.key || `item-group-${indexA}`} title={compile(item.title)}>
|
||||
{renderItems(item.children)}
|
||||
</Menu.SubMenu>
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (!designable) {
|
||||
const menu = <Menu>{renderItems(items)}</Menu>;
|
||||
|
||||
if (!designable && props.designable !== true) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -124,15 +104,17 @@ SchemaInitializer.Button = observer((props: SchemaInitializerButtonProps) => {
|
||||
{...dropdown}
|
||||
overlay={menu}
|
||||
>
|
||||
{component ? component : (
|
||||
{component ? (
|
||||
component
|
||||
) : (
|
||||
<Button
|
||||
{...others}
|
||||
type={'dashed'}
|
||||
style={{
|
||||
...style,
|
||||
borderColor: '#f18b62',
|
||||
color: '#f18b62',
|
||||
...style,
|
||||
}}
|
||||
{...others}
|
||||
>
|
||||
{compile(props.children || props.title)}
|
||||
</Button>
|
||||
@ -156,16 +138,24 @@ SchemaInitializer.Item = (props: SchemaInitializerItemProps) => {
|
||||
}
|
||||
if (item.type === 'itemGroup') {
|
||||
return (
|
||||
// @ts-ignore
|
||||
<Menu.ItemGroup eventKey={`item-group-${indexA}`} key={`item-group-${indexA}`} title={compile(item.title)}>
|
||||
<Menu.ItemGroup
|
||||
// @ts-ignore
|
||||
eventKey={item.key || `item-group-${indexA}`}
|
||||
key={item.key || `item-group-${indexA}`}
|
||||
title={compile(item.title)}
|
||||
>
|
||||
{renderMenuItem(item.children)}
|
||||
</Menu.ItemGroup>
|
||||
);
|
||||
}
|
||||
if (item.type === 'subMenu') {
|
||||
return (
|
||||
// @ts-ignore
|
||||
<Menu.SubMenu eventKey={`sub-menu-${indexA}`} key={`sub-menu-${indexA}`} title={compile(item.title)}>
|
||||
<Menu.SubMenu
|
||||
// @ts-ignore
|
||||
eventKey={item.key || `sub-menu-${indexA}`}
|
||||
key={item.key || `sub-menu-${indexA}`}
|
||||
title={compile(item.title)}
|
||||
>
|
||||
{renderMenuItem(item.children)}
|
||||
</Menu.SubMenu>
|
||||
);
|
||||
|
@ -8,12 +8,14 @@ export interface SchemaInitializerButtonProps extends ButtonProps {
|
||||
items?: SchemaInitializerItemOptions[];
|
||||
dropdown?: DropDownProps;
|
||||
component?: any;
|
||||
designable?: boolean;
|
||||
}
|
||||
|
||||
export type SchemaInitializerItemOptions = ItemGroupOptions | SubMenuOptions | ItemOptions | DividerOptions;
|
||||
|
||||
interface ItemCommonOptions {
|
||||
title?: any;
|
||||
key?: string;
|
||||
}
|
||||
|
||||
interface ItemGroupOptions extends ItemCommonOptions {
|
||||
|
@ -11,19 +11,26 @@ const titleCss = css`
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
font-size: 12px;
|
||||
background: #f18b62;
|
||||
color: #fff;
|
||||
padding: 0 5px;
|
||||
/* background: #f18b62;
|
||||
color: #fff; */
|
||||
padding: 0;
|
||||
line-height: 16px;
|
||||
height: 16px;
|
||||
border-bottom-right-radius: 2px;
|
||||
border-radius: 2px;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
.title-tag {
|
||||
padding: 0 3px;
|
||||
border-radius: 2px;
|
||||
background: #f18b62;
|
||||
color: #fff;
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
|
||||
export const GeneralSchemaDesigner = (props: any) => {
|
||||
const { title, draggable = true } = props;
|
||||
const { title, template, draggable = true } = props;
|
||||
const { dn, designable } = useDesignable();
|
||||
const field = useField();
|
||||
const fieldSchema = useFieldSchema();
|
||||
@ -38,7 +45,14 @@ export const GeneralSchemaDesigner = (props: any) => {
|
||||
}
|
||||
return (
|
||||
<div className={'general-schema-designer'}>
|
||||
{title && <div className={classNames('general-schema-designer-title', titleCss)}>{compile(title)}</div>}
|
||||
{title && (
|
||||
<div className={classNames('general-schema-designer-title', titleCss)}>
|
||||
<Space size={2}>
|
||||
<span className={'title-tag'}>{compile(title)}</span>
|
||||
{template && <span className={'title-tag'}>引用模板: {template?.name || '未命名'}</span>}
|
||||
</Space>
|
||||
</div>
|
||||
)}
|
||||
<div className={'general-schema-designer-icons'}>
|
||||
<Space size={2} align={'center'}>
|
||||
{draggable && (
|
||||
|
@ -1,11 +1,22 @@
|
||||
import { FormDialog, FormLayout } from '@formily/antd';
|
||||
import { FormDialog, FormItem, FormLayout, Input } from '@formily/antd';
|
||||
import { GeneralField } from '@formily/core';
|
||||
import { ISchema, Schema, SchemaOptionsContext } from '@formily/react';
|
||||
import { uid } from '@formily/shared';
|
||||
import { Dropdown, Menu, MenuItemProps, Modal, Select, Switch } from 'antd';
|
||||
import React, { createContext, useContext, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ActionContext, Designable, SchemaComponent, SchemaComponentOptions, useActionContext } from '..';
|
||||
import {
|
||||
ActionContext,
|
||||
createDesignable,
|
||||
Designable,
|
||||
SchemaComponent,
|
||||
SchemaComponentOptions,
|
||||
useActionContext,
|
||||
useAPIClient,
|
||||
useCollection
|
||||
} from '..';
|
||||
import { useSchemaTemplateManager } from '../schema-templates';
|
||||
import { useBlockTemplateContext } from '../schema-templates/BlockTemplate';
|
||||
|
||||
interface SchemaSettingsProps {
|
||||
title?: any;
|
||||
@ -20,6 +31,8 @@ interface SchemaSettingsContextProps {
|
||||
fieldSchema?: Schema;
|
||||
setVisible?: any;
|
||||
visible?: any;
|
||||
template?: any;
|
||||
collectionName?: any;
|
||||
}
|
||||
|
||||
const SchemaSettingsContext = createContext<SchemaSettingsContextProps>(null);
|
||||
@ -48,19 +61,24 @@ interface SchemaSettingsProviderProps {
|
||||
fieldSchema?: Schema;
|
||||
setVisible?: any;
|
||||
visible?: any;
|
||||
template?: any;
|
||||
collectionName?: any;
|
||||
}
|
||||
|
||||
export const SchemaSettingsProvider: React.FC<SchemaSettingsProviderProps> = (props) => {
|
||||
const { visible, setVisible, dn, field, fieldSchema, children } = props;
|
||||
const { children, fieldSchema, ...others } = props;
|
||||
const { getTemplateBySchemaId } = useSchemaTemplateManager();
|
||||
const { name } = useCollection();
|
||||
const template = getTemplateBySchemaId(fieldSchema['x-uid']);
|
||||
return (
|
||||
<SchemaSettingsContext.Provider value={{ visible, setVisible, dn, field, fieldSchema }}>
|
||||
<SchemaSettingsContext.Provider value={{ collectionName: name, template, fieldSchema, ...others }}>
|
||||
{children}
|
||||
</SchemaSettingsContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const SchemaSettings: React.FC<SchemaSettingsProps> & SchemaSettingsNested = (props) => {
|
||||
const { title, dn, field, fieldSchema } = props;
|
||||
const { title, dn, ...others } = props;
|
||||
const [visible, setVisible] = useState(false);
|
||||
const DropdownMenu = (
|
||||
<Dropdown
|
||||
@ -75,7 +93,7 @@ export const SchemaSettings: React.FC<SchemaSettingsProps> & SchemaSettingsNeste
|
||||
);
|
||||
if (dn) {
|
||||
return (
|
||||
<SchemaSettingsProvider visible={visible} setVisible={setVisible} dn={dn} field={field} fieldSchema={fieldSchema}>
|
||||
<SchemaSettingsProvider visible={visible} setVisible={setVisible} dn={dn} {...others}>
|
||||
{DropdownMenu}
|
||||
</SchemaSettingsProvider>
|
||||
);
|
||||
@ -83,6 +101,86 @@ export const SchemaSettings: React.FC<SchemaSettingsProps> & SchemaSettingsNeste
|
||||
return DropdownMenu;
|
||||
};
|
||||
|
||||
SchemaSettings.Template = (props) => {
|
||||
const { componentName, collectionName } = props;
|
||||
const { t } = useTranslation();
|
||||
const { dn, setVisible, template, fieldSchema } = useSchemaSettings();
|
||||
const api = useAPIClient();
|
||||
const { dn: tdn } = useBlockTemplateContext();
|
||||
const { saveAsTemplate, copyTemplateSchema } = useSchemaTemplateManager();
|
||||
if (!collectionName) {
|
||||
return null;
|
||||
}
|
||||
if (template) {
|
||||
return (
|
||||
<SchemaSettings.Item
|
||||
onClick={async () => {
|
||||
const schema = await copyTemplateSchema(template);
|
||||
const removed = tdn.removeWithoutEmit();
|
||||
tdn.insertAfterEnd(schema, {
|
||||
async onSuccess() {
|
||||
await api.request({
|
||||
url: `/uiSchemas:remove/${removed['x-uid']}`,
|
||||
});
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t('Convert reference to duplicate')}
|
||||
</SchemaSettings.Item>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<SchemaSettings.Item
|
||||
onClick={async () => {
|
||||
setVisible(false);
|
||||
const values = await FormDialog('Save as template', () => {
|
||||
return (
|
||||
<FormLayout layout={'vertical'}>
|
||||
<SchemaComponent
|
||||
components={{ Input, FormItem }}
|
||||
schema={{
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {
|
||||
title: '模板名称',
|
||||
required: true,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input',
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</FormLayout>
|
||||
);
|
||||
}).open({});
|
||||
const sdn = createDesignable({
|
||||
api,
|
||||
refresh: dn.refresh.bind(dn),
|
||||
current: fieldSchema.parent,
|
||||
});
|
||||
sdn.loadAPIClientEvents();
|
||||
const { key } = await saveAsTemplate({
|
||||
collectionName,
|
||||
componentName,
|
||||
name: values.name,
|
||||
uid: fieldSchema['x-uid'],
|
||||
});
|
||||
sdn.removeWithoutEmit(fieldSchema);
|
||||
sdn.insertBeforeEnd({
|
||||
type: 'void',
|
||||
'x-component': 'BlockTemplate',
|
||||
'x-component-props': {
|
||||
templateId: key,
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t('Save as template')}
|
||||
</SchemaSettings.Item>
|
||||
);
|
||||
};
|
||||
|
||||
SchemaSettings.Item = (props) => {
|
||||
const { eventKey } = props;
|
||||
return (
|
||||
@ -116,8 +214,9 @@ SchemaSettings.Divider = (props) => {
|
||||
|
||||
SchemaSettings.Remove = (props: any) => {
|
||||
const { confirm, removeParentsIfNoChildren, breakRemoveOn } = props;
|
||||
const { dn } = useSchemaSettings();
|
||||
const { dn, template } = useSchemaSettings();
|
||||
const { t } = useTranslation();
|
||||
const ctx = useBlockTemplateContext();
|
||||
return (
|
||||
<SchemaSettings.Item
|
||||
onClick={() => {
|
||||
@ -126,10 +225,15 @@ SchemaSettings.Remove = (props: any) => {
|
||||
content: t('Are you sure you want to delete it?'),
|
||||
...confirm,
|
||||
onOk() {
|
||||
dn.remove(null, {
|
||||
const options = {
|
||||
removeParentsIfNoChildren,
|
||||
breakRemoveOn,
|
||||
});
|
||||
};
|
||||
if (template && ctx?.dn) {
|
||||
ctx?.dn.remove(null, options);
|
||||
} else {
|
||||
dn.remove(null, options);
|
||||
}
|
||||
},
|
||||
});
|
||||
}}
|
||||
|
59
packages/client/src/schema-templates/AddBlockTemplate.tsx
Normal file
59
packages/client/src/schema-templates/AddBlockTemplate.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import { uid } from '@formily/shared';
|
||||
import { Button, Dropdown, Menu } from 'antd';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useAPIClient, useCollectionManager, useCompile, useSchemaTemplateManager } from '..';
|
||||
import { createTableBlockSchema } from '../schema-initializer/Initializers/Items';
|
||||
|
||||
export const AddBlockTemplate = (props: any) => {
|
||||
const { t } = useTranslation();
|
||||
const history = useHistory();
|
||||
const api = useAPIClient();
|
||||
const { refresh } = useSchemaTemplateManager();
|
||||
const { collections } = useCollectionManager();
|
||||
const compile = useCompile();
|
||||
const insert = ({ uiSchema, collectionName }) => {
|
||||
const key = uid();
|
||||
api
|
||||
.resource('uiSchemaTemplates')
|
||||
.create({
|
||||
values: {
|
||||
key,
|
||||
collectionName,
|
||||
uiSchema,
|
||||
},
|
||||
})
|
||||
.then((res) => {
|
||||
refresh();
|
||||
history.push(`/admin/block-templates/${key}`);
|
||||
});
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Dropdown
|
||||
overlay={
|
||||
<Menu>
|
||||
<Menu.SubMenu title={'Table'}>
|
||||
{collections?.map((collection) => {
|
||||
return (
|
||||
<Menu.Item
|
||||
key={collection.name}
|
||||
onClick={() => {
|
||||
const uiSchema = createTableBlockSchema(collection.name);
|
||||
insert({ uiSchema, collectionName: collection.name });
|
||||
}}
|
||||
>
|
||||
{compile(collection.title)}
|
||||
</Menu.Item>
|
||||
);
|
||||
})}
|
||||
</Menu.SubMenu>
|
||||
</Menu>
|
||||
}
|
||||
>
|
||||
<Button type={'primary'}>{t('Add block template')}</Button>
|
||||
</Dropdown>
|
||||
</>
|
||||
);
|
||||
};
|
26
packages/client/src/schema-templates/BlockTemplate.tsx
Normal file
26
packages/client/src/schema-templates/BlockTemplate.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import { observer, useField, useFieldSchema } from '@formily/react';
|
||||
import React, { createContext, useContext, useMemo } from 'react';
|
||||
import { RemoteSchemaComponent, useDesignable } from '..';
|
||||
import { useSchemaTemplateManager } from './SchemaTemplateManagerProvider';
|
||||
|
||||
const BlockTemplateContext = createContext<any>({});
|
||||
|
||||
export const useBlockTemplateContext = () => {
|
||||
return useContext(BlockTemplateContext);
|
||||
};
|
||||
|
||||
export const BlockTemplate = observer((props: any) => {
|
||||
const { templateId } = props;
|
||||
const { getTemplateById } = useSchemaTemplateManager();
|
||||
const field = useField();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const { dn } = useDesignable();
|
||||
const template = useMemo(() => getTemplateById(templateId), [templateId]);
|
||||
return (
|
||||
<div>
|
||||
<BlockTemplateContext.Provider value={{ dn, field, fieldSchema, template }}>
|
||||
<RemoteSchemaComponent uid={template?.uid} />
|
||||
</BlockTemplateContext.Provider>
|
||||
</div>
|
||||
);
|
||||
});
|
@ -0,0 +1,91 @@
|
||||
import { Input, PageHeader as AntdPageHeader, Spin } from 'antd';
|
||||
import React, { useContext, useState } from 'react';
|
||||
import { useHistory, useRouteMatch } from 'react-router-dom';
|
||||
import { useAPIClient, useRequest, useSchemaTemplateManager } from '..';
|
||||
import { RemoteSchemaComponent, SchemaComponentContext } from '../schema-component';
|
||||
|
||||
const EditableTitle = (props) => {
|
||||
const [title, setTitle] = useState(props.title);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const { refresh } = useSchemaTemplateManager();
|
||||
const api = useAPIClient();
|
||||
const { run } = useRequest(
|
||||
{
|
||||
resource: 'uiSchemaTemplates',
|
||||
action: 'update',
|
||||
params: {
|
||||
filterByTk: props.filterByTk,
|
||||
},
|
||||
},
|
||||
{
|
||||
manual: true,
|
||||
debounceWait: 500,
|
||||
onSuccess() {
|
||||
refresh();
|
||||
},
|
||||
},
|
||||
);
|
||||
return (
|
||||
<div>
|
||||
{visible ? (
|
||||
<Input
|
||||
defaultValue={title}
|
||||
size={'large'}
|
||||
onBlur={() => {
|
||||
setVisible(false);
|
||||
}}
|
||||
onChange={(e) => {
|
||||
setTitle(e.target.value);
|
||||
run({
|
||||
filterByTk: props.filterByTk,
|
||||
values: {
|
||||
name: e.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
onClick={() => {
|
||||
setVisible(true);
|
||||
}}
|
||||
>
|
||||
{title || <span style={{ color: '#bbb' }}>未命名</span>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const BlockTemplateDetails = () => {
|
||||
const history = useHistory();
|
||||
const match = useRouteMatch<any>();
|
||||
const key = match?.params?.key;
|
||||
const value = useContext(SchemaComponentContext);
|
||||
const { data, loading } = useRequest({
|
||||
resource: 'uiSchemaTemplates',
|
||||
action: 'get',
|
||||
params: {
|
||||
filterByTk: key,
|
||||
},
|
||||
});
|
||||
if (loading) {
|
||||
return <Spin />;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<AntdPageHeader
|
||||
onBack={() => {
|
||||
history.push('/admin/block-templates');
|
||||
}}
|
||||
ghost={false}
|
||||
title={<EditableTitle filterByTk={key} title={data?.data?.name} />}
|
||||
/>
|
||||
<div style={{ margin: 24 }}>
|
||||
<SchemaComponentContext.Provider value={{ ...value, designable: true }}>
|
||||
<RemoteSchemaComponent uid={data?.data?.uid} />
|
||||
</SchemaComponentContext.Provider>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
56
packages/client/src/schema-templates/BlockTemplatePage.tsx
Normal file
56
packages/client/src/schema-templates/BlockTemplatePage.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
import { Card, PageHeader as AntdPageHeader, Table } from 'antd';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useCompile, useRequest } from '..';
|
||||
|
||||
export const BlockTemplatePage = () => {
|
||||
const { data, loading } = useRequest({
|
||||
resource: 'uiSchemaTemplates',
|
||||
action: 'list',
|
||||
params: {
|
||||
appends: ['collection'],
|
||||
sort: ['-createdAt'],
|
||||
},
|
||||
});
|
||||
const compile = useCompile();
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div>
|
||||
<AntdPageHeader ghost={false} title={t('Block templates')} />
|
||||
<div style={{ margin: 24 }}>
|
||||
<Card bordered={false}>
|
||||
<Table
|
||||
rowSelection={{
|
||||
type: 'checkbox',
|
||||
}}
|
||||
columns={[
|
||||
{
|
||||
dataIndex: 'name',
|
||||
title: t('Template name'),
|
||||
render: (value) => <>{value || '未命名'}</>,
|
||||
},
|
||||
{
|
||||
dataIndex: ['collection', 'title'],
|
||||
title: t('Collection display name'),
|
||||
render: (value) => compile(value),
|
||||
},
|
||||
{
|
||||
dataIndex: 'componentName',
|
||||
title: t('Block type'),
|
||||
render: (value) => value,
|
||||
},
|
||||
{
|
||||
dataIndex: 'actions',
|
||||
title: t('Actions'),
|
||||
render: (_, record) => <Link to={`/admin/block-templates/${record.key}`}>查看</Link>,
|
||||
},
|
||||
]}
|
||||
loading={loading}
|
||||
dataSource={data?.data}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,149 @@
|
||||
import { ISchema, useFieldSchema } from '@formily/react';
|
||||
import { uid } from '@formily/shared';
|
||||
import { Spin } from 'antd';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import React, { createContext, useContext, useMemo } from 'react';
|
||||
import { useAPIClient, useRequest } from '../api-client';
|
||||
import { RouteSwitchContext } from '../route-switch';
|
||||
import { SchemaComponentOptions } from '../schema-component';
|
||||
import { BlockTemplate } from './BlockTemplate';
|
||||
|
||||
export const SchemaTemplateManagerContext = createContext<any>({});
|
||||
|
||||
const SchemaTemplateRouteProvider = (props) => {
|
||||
const { routes, ...others } = useContext(RouteSwitchContext);
|
||||
routes[1].routes.unshift(
|
||||
{
|
||||
type: 'route',
|
||||
path: '/admin/block-templates/:key',
|
||||
component: 'BlockTemplateDetails',
|
||||
},
|
||||
{
|
||||
type: 'route',
|
||||
path: '/admin/block-templates',
|
||||
component: 'BlockTemplatePage',
|
||||
},
|
||||
);
|
||||
return <RouteSwitchContext.Provider value={{ ...others, routes }}>{props.children}</RouteSwitchContext.Provider>;
|
||||
};
|
||||
|
||||
export const SchemaTemplateManagerProvider: React.FC<any> = (props) => {
|
||||
const { templates, refresh } = props;
|
||||
return (
|
||||
<SchemaTemplateManagerContext.Provider value={{ templates, refresh }}>
|
||||
<SchemaTemplateRouteProvider>
|
||||
<SchemaComponentOptions components={{ BlockTemplate }}>{props.children}</SchemaComponentOptions>
|
||||
</SchemaTemplateRouteProvider>
|
||||
</SchemaTemplateManagerContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const regenerateUid = (s: ISchema) => {
|
||||
s['x-uid'] = uid();
|
||||
Object.keys(s.properties || {}).forEach((key) => {
|
||||
regenerateUid(s.properties[key]);
|
||||
});
|
||||
};
|
||||
|
||||
export const useSchemaTemplate = () => {
|
||||
const { getTemplateBySchemaId } = useSchemaTemplateManager();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const schemaId = fieldSchema['x-uid'];
|
||||
return useMemo(() => getTemplateBySchemaId(schemaId), [schemaId]);
|
||||
};
|
||||
|
||||
export const useSchemaTemplateManager = () => {
|
||||
const { refresh, templates = [] } = useContext(SchemaTemplateManagerContext);
|
||||
const api = useAPIClient();
|
||||
return {
|
||||
templates,
|
||||
refresh,
|
||||
async getTemplateSchemaByMode(options) {
|
||||
const { mode, template } = options;
|
||||
if (mode === 'copy') {
|
||||
const { data } = await api.request({
|
||||
url: `/uiSchemas:getJsonSchema/${template.uid}`,
|
||||
});
|
||||
const s = data?.data || {};
|
||||
regenerateUid(s);
|
||||
return cloneDeep(s);
|
||||
} else if (mode === 'reference') {
|
||||
return {
|
||||
type: 'void',
|
||||
'x-component': 'BlockTemplate',
|
||||
'x-component-props': {
|
||||
templateId: template.key,
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
async copyTemplateSchema(template) {
|
||||
const { data } = await api.request({
|
||||
url: `/uiSchemas:getJsonSchema/${template.uid}`,
|
||||
});
|
||||
const s = data?.data || {};
|
||||
regenerateUid(s);
|
||||
return cloneDeep(s);
|
||||
},
|
||||
async saveAsTemplate(values) {
|
||||
const { uid: schemaId } = values;
|
||||
const key = uid();
|
||||
await api.resource('uiSchemaTemplates').create({
|
||||
values: {
|
||||
key,
|
||||
...values,
|
||||
},
|
||||
});
|
||||
await api.request({
|
||||
url: `/uiSchemas:clearAncestor/${schemaId}`,
|
||||
});
|
||||
await refresh();
|
||||
return { key };
|
||||
},
|
||||
async duplicate(template) {
|
||||
const { data } = await api.request({
|
||||
url: `/uiSchemas:getJsonSchema/${template.uid}`,
|
||||
});
|
||||
const s = data?.data || {};
|
||||
regenerateUid(s);
|
||||
return s;
|
||||
},
|
||||
getTemplateBySchemaId(schemaId) {
|
||||
return templates?.find((template) => template.uid === schemaId);
|
||||
},
|
||||
getTemplateById(key) {
|
||||
return templates?.find((template) => template.key === key);
|
||||
},
|
||||
getTemplatesByCollection(collectionName: string) {
|
||||
const items = templates?.filter?.((template) => template.collectionName === collectionName);
|
||||
return items || [];
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const RemoteSchemaTemplateManagerProvider: React.FC = (props) => {
|
||||
const api = useAPIClient();
|
||||
const options = {
|
||||
resource: 'uiSchemaTemplates',
|
||||
action: 'list',
|
||||
params: {
|
||||
appends: ['collection'],
|
||||
paginate: false,
|
||||
},
|
||||
};
|
||||
const service = useRequest(options);
|
||||
if (service.loading) {
|
||||
return <Spin />;
|
||||
}
|
||||
return (
|
||||
<SchemaTemplateManagerProvider
|
||||
refresh={async () => {
|
||||
const { data } = await api.request(options);
|
||||
service.mutate(data);
|
||||
}}
|
||||
templates={service?.data?.data}
|
||||
>
|
||||
{props.children}
|
||||
</SchemaTemplateManagerProvider>
|
||||
);
|
||||
};
|
@ -0,0 +1,19 @@
|
||||
import { LayoutOutlined } from '@ant-design/icons';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { PluginManager } from '../plugin-manager';
|
||||
|
||||
export const SchemaTemplateShortcut = () => {
|
||||
const { t } = useTranslation();
|
||||
const history = useHistory();
|
||||
return (
|
||||
<PluginManager.Toolbar.Item
|
||||
icon={<LayoutOutlined />}
|
||||
title={t('Block templates')}
|
||||
onClick={() => {
|
||||
history.push('/admin/block-templates');
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
5
packages/client/src/schema-templates/index.ts
Normal file
5
packages/client/src/schema-templates/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export * from './BlockTemplateDetails';
|
||||
export * from './BlockTemplatePage';
|
||||
export * from './SchemaTemplateManagerProvider';
|
||||
export * from './SchemaTemplateShortcut';
|
||||
|
@ -0,0 +1,153 @@
|
||||
import { ISchema } from '@formily/react';
|
||||
import { CollectionOptions } from '../../collection-manager';
|
||||
|
||||
const collection: CollectionOptions = {
|
||||
name: 'uiSchemaTemplates',
|
||||
filterTargetKey: 'name',
|
||||
targetKey: 'name',
|
||||
fields: [
|
||||
{
|
||||
type: 'integer',
|
||||
name: 'name',
|
||||
interface: 'input',
|
||||
uiSchema: {
|
||||
title: '{{ t("Template name") }}',
|
||||
type: 'number',
|
||||
'x-component': 'Input',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const uiSchemaTemplatesSchema: ISchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
block1: {
|
||||
type: 'void',
|
||||
'x-collection': 'collections',
|
||||
'x-decorator': 'ResourceActionProvider',
|
||||
'x-decorator-props': {
|
||||
collection,
|
||||
request: {
|
||||
resource: 'uiSchemaTemplates',
|
||||
action: 'list',
|
||||
params: {
|
||||
pageSize: 50,
|
||||
filter: {},
|
||||
// sort: ['sort'],
|
||||
appends: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
// 'x-component': 'CollectionProvider',
|
||||
// 'x-component-props': {
|
||||
// collection,
|
||||
// },
|
||||
properties: {
|
||||
actions: {
|
||||
type: 'void',
|
||||
'x-component': 'ActionBar',
|
||||
'x-component-props': {
|
||||
style: {
|
||||
marginBottom: 16,
|
||||
},
|
||||
},
|
||||
properties: {
|
||||
delete: {
|
||||
type: 'void',
|
||||
title: '{{ t("Delete") }}',
|
||||
'x-component': 'Action',
|
||||
'x-component-props': {
|
||||
useAction: '{{ cm.useBulkDestroyActionAndRefreshCM }}',
|
||||
confirm: {
|
||||
title: "{{t('Delete record')}}",
|
||||
content: "{{t('Are you sure you want to delete it?')}}",
|
||||
},
|
||||
},
|
||||
},
|
||||
create: {
|
||||
type: 'void',
|
||||
title: '{{ t("Add block template") }}',
|
||||
'x-component': 'Action',
|
||||
'x-component-props': {
|
||||
type: 'primary',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
table: {
|
||||
type: 'void',
|
||||
'x-uid': 'input',
|
||||
'x-component': 'Table.Void',
|
||||
'x-component-props': {
|
||||
rowKey: 'name',
|
||||
rowSelection: {
|
||||
type: 'checkbox',
|
||||
},
|
||||
useDataSource: '{{ cm.useDataSourceFromRAC }}',
|
||||
},
|
||||
properties: {
|
||||
column1: {
|
||||
type: 'void',
|
||||
'x-decorator': 'Table.Column.Decorator',
|
||||
'x-component': 'Table.Column',
|
||||
properties: {
|
||||
title: {
|
||||
'x-component': 'CollectionField',
|
||||
'x-read-pretty': true,
|
||||
},
|
||||
},
|
||||
},
|
||||
column2: {
|
||||
type: 'void',
|
||||
'x-decorator': 'Table.Column.Decorator',
|
||||
'x-component': 'Table.Column',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
'x-component': 'CollectionField',
|
||||
'x-read-pretty': true,
|
||||
},
|
||||
},
|
||||
},
|
||||
column3: {
|
||||
type: 'void',
|
||||
title: '{{ t("Actions") }}',
|
||||
'x-component': 'Table.Column',
|
||||
properties: {
|
||||
actions: {
|
||||
type: 'void',
|
||||
'x-component': 'Space',
|
||||
'x-component-props': {
|
||||
split: '|',
|
||||
},
|
||||
properties: {
|
||||
view: {
|
||||
type: 'void',
|
||||
title: '{{ t("Configure fields") }}',
|
||||
'x-component': 'Action.Link',
|
||||
'x-component-props': {},
|
||||
},
|
||||
delete: {
|
||||
type: 'void',
|
||||
title: '{{ t("Delete") }}',
|
||||
'x-component': 'Action.Link',
|
||||
'x-component-props': {
|
||||
confirm: {
|
||||
title: "{{t('Delete record')}}",
|
||||
content: "{{t('Are you sure you want to delete it?')}}",
|
||||
},
|
||||
useAction: '{{ cm.useDestroyActionAndRefreshCM }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
@ -40,6 +40,16 @@ export class UiRoutesStoragePlugin extends Plugin {
|
||||
path: '/admin/workflow',
|
||||
component: 'WorkflowPage',
|
||||
},
|
||||
// {
|
||||
// type: 'route',
|
||||
// path: '/admin/block-templates/:key',
|
||||
// component: 'BlockTemplateDetails',
|
||||
// },
|
||||
// {
|
||||
// type: 'route',
|
||||
// path: '/admin/block-templates',
|
||||
// component: 'BlockTemplatePage',
|
||||
// },
|
||||
{
|
||||
type: 'route',
|
||||
path: '/admin/:name(.+)?',
|
||||
|
@ -0,0 +1,35 @@
|
||||
import { defineCollection } from '@nocobase/database';
|
||||
|
||||
export default defineCollection({
|
||||
name: 'uiSchemaTemplates',
|
||||
autoGenId: false,
|
||||
fields: [
|
||||
{
|
||||
type: 'uid',
|
||||
name: 'key',
|
||||
primaryKey: true,
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'name',
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'componentName',
|
||||
},
|
||||
{
|
||||
type: 'belongsTo',
|
||||
name: 'uiSchema',
|
||||
target: 'uiSchemas',
|
||||
foreignKey: 'uid',
|
||||
onDelete: 'CASCADE',
|
||||
},
|
||||
{
|
||||
type: 'belongsTo',
|
||||
name: 'collection',
|
||||
target: 'collections',
|
||||
foreignKey: 'collectionName',
|
||||
targetKey: 'name',
|
||||
},
|
||||
],
|
||||
});
|
Loading…
Reference in New Issue
Block a user