diff --git a/packages/app/src/pages/index.tsx b/packages/app/src/pages/index.tsx
index 1433bda2d..0332f8a56 100644
--- a/packages/app/src/pages/index.tsx
+++ b/packages/app/src/pages/index.tsx
@@ -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 ;
- }
+ const routes = useRoutes();
return (
-
+
);
});
diff --git a/packages/client/src/index.tsx b/packages/client/src/index.tsx
index 0297aae6f..57b80efd9 100644
--- a/packages/client/src/index.tsx
+++ b/packages/client/src/index.tsx
@@ -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';
diff --git a/packages/client/src/locale/zh_CN.json b/packages/client/src/locale/zh_CN.json
index c6f77f820..4ba4f54d8 100644
--- a/packages/client/src/locale/zh_CN.json
+++ b/packages/client/src/locale/zh_CN.json
@@ -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": "区块类型"
}
diff --git a/packages/client/src/route-switch/RouteSwitchProvider.tsx b/packages/client/src/route-switch/RouteSwitchProvider.tsx
index 35e8b209a..19406b922 100644
--- a/packages/client/src/route-switch/RouteSwitchProvider.tsx
+++ b/packages/client/src/route-switch/RouteSwitchProvider.tsx
@@ -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 {children};
+ const { children, components, routes } = props;
+ return {children};
+}
+
+export function RemoteRouteSwitchProvider(props: RouteSwitchProviderProps) {
+ const { data, loading } = useRequest({
+ url: 'uiRoutes:getAccessible',
+ });
+ if (loading) {
+ return ;
+ }
+ return ;
}
diff --git a/packages/client/src/route-switch/antd/admin-layout/index.tsx b/packages/client/src/route-switch/antd/admin-layout/index.tsx
index 09f1bca21..e6e390dda 100644
--- a/packages/client/src/route-switch/antd/admin-layout/index.tsx
+++ b/packages/client/src/route-switch/antd/admin-layout/index.tsx
@@ -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 (
-
-
-
-
-
+
+
+
+
+
+
+
);
};
diff --git a/packages/client/src/route-switch/context.ts b/packages/client/src/route-switch/context.ts
index a7b71c958..58659224e 100644
--- a/packages/client/src/route-switch/context.ts
+++ b/packages/client/src/route-switch/context.ts
@@ -3,6 +3,7 @@ import { RouteProps } from './types';
export const RouteSwitchContext = createContext({
components: {},
+ routes: [],
});
export const RouteContext = createContext(null);
diff --git a/packages/client/src/route-switch/hooks.ts b/packages/client/src/route-switch/hooks.ts
index 1b4ae9b81..431b91780 100644
--- a/packages/client/src/route-switch/hooks.ts
+++ b/packages/client/src/route-switch/hooks.ts
@@ -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 || [];
+}
diff --git a/packages/client/src/route-switch/types.ts b/packages/client/src/route-switch/types.ts
index c15ceb4ca..0712e5e22 100644
--- a/packages/client/src/route-switch/types.ts
+++ b/packages/client/src/route-switch/types.ts
@@ -23,6 +23,7 @@ export interface RouteProps {
export interface RouteSwitchProviderProps {
components?: any;
children?: any;
+ routes?: RouteRedirectProps[];
}
export type RouteRedirectProps = RedirectProps | RouteProps;
diff --git a/packages/client/src/schema-component/antd/calendar/Calendar.Designer.tsx b/packages/client/src/schema-component/antd/calendar/Calendar.Designer.tsx
index 7bd8cb779..d0eda3895 100644
--- a/packages/client/src/schema-component/antd/calendar/Calendar.Designer.tsx
+++ b/packages/client/src/schema-component/antd/calendar/Calendar.Designer.tsx
@@ -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 (
-
+
{
}}
/>
+
+
{
const { name, title } = useCollection();
return (
+
+
& { uid?: string };
@@ -113,8 +114,11 @@ export const Form: React.FC & { Designer?: any } = observer((props) =
Form.Designer = () => {
const { name, title } = useCollection();
+ const template = useSchemaTemplate();
return (
-
+
+
+
{
@@ -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 (
-
+
{
}}
/>
+
+
{
@@ -29,8 +30,9 @@ export const TableVoidDesigner = () => {
direction: 'asc',
};
});
+ const template = useSchemaTemplate();
return (
-
+
{
}}
/>
+
+
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)) {
diff --git a/packages/client/src/schema-initializer/Initializers/Items/CalendarBlockInitializer.tsx b/packages/client/src/schema-initializer/Initializers/Items/CalendarBlockInitializer.tsx
index 83bdb2c71..6c5d38daf 100644
--- a/packages/client/src/schema-initializer/Initializers/Items/CalendarBlockInitializer.tsx
+++ b/packages/client/src/schema-initializer/Initializers/Items/CalendarBlockInitializer.tsx
@@ -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 (
}
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')}
/>
);
};
diff --git a/packages/client/src/schema-initializer/Initializers/Items/FormBlockInitializer.tsx b/packages/client/src/schema-initializer/Initializers/Items/FormBlockInitializer.tsx
index 77c4244ee..28273894d 100644
--- a/packages/client/src/schema-initializer/Initializers/Items/FormBlockInitializer.tsx
+++ b/packages/client/src/schema-initializer/Initializers/Items/FormBlockInitializer.tsx
@@ -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 (
}
- 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')}
/>
);
};
diff --git a/packages/client/src/schema-initializer/Initializers/Items/KanbanBlockInitializer.tsx b/packages/client/src/schema-initializer/Initializers/Items/KanbanBlockInitializer.tsx
index ba91821e0..616230f68 100644
--- a/packages/client/src/schema-initializer/Initializers/Items/KanbanBlockInitializer.tsx
+++ b/packages/client/src/schema-initializer/Initializers/Items/KanbanBlockInitializer.tsx
@@ -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={}
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')}
/>
);
};
diff --git a/packages/client/src/schema-initializer/Initializers/Items/TableBlockInitializer.tsx b/packages/client/src/schema-initializer/Initializers/Items/TableBlockInitializer.tsx
index cdf87cf1e..a8abb5aa2 100644
--- a/packages/client/src/schema-initializer/Initializers/Items/TableBlockInitializer.tsx
+++ b/packages/client/src/schema-initializer/Initializers/Items/TableBlockInitializer.tsx
@@ -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 (
}
- 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')}
/>
);
};
diff --git a/packages/client/src/schema-initializer/Initializers/utils.ts b/packages/client/src/schema-initializer/Initializers/utils.ts
index 35d7f2fad..6f68f2f2b 100644
--- a/packages/client/src/schema-initializer/Initializers/utils.ts
+++ b/packages/client/src/schema-initializer/Initializers/utils.ts
@@ -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 || '未命名',
+ };
+ }),
+ },
+ ],
+ };
+ }),
+ },
+ ];
+};
diff --git a/packages/client/src/schema-initializer/SchemaInitializer.tsx b/packages/client/src/schema-initializer/SchemaInitializer.tsx
index dba08dcd6..97c6f280b 100644
--- a/packages/client/src/schema-initializer/SchemaInitializer.tsx
+++ b/packages/client/src/schema-initializer/SchemaInitializer.tsx
@@ -42,76 +42,56 @@ SchemaInitializer.Button = observer((props: SchemaInitializerButtonProps) => {
insertAdjacent(insertPosition, wrap(schema));
}
};
- const menu = (
-
- );
+ }
+ if (item.type === 'itemGroup') {
+ return (
+
+ {renderItems(item.children)}
+
+ );
+ }
+ if (item.type === 'subMenu') {
+ return (
+
+ {renderItems(item.children)}
+
+ );
+ }
+ });
+ };
- if (!designable) {
+ const menu = ;
+
+ if (!designable && props.designable !== true) {
return null;
}
@@ -124,15 +104,17 @@ SchemaInitializer.Button = observer((props: SchemaInitializerButtonProps) => {
{...dropdown}
overlay={menu}
>
- {component ? component : (
+ {component ? (
+ component
+ ) : (
@@ -156,16 +138,24 @@ SchemaInitializer.Item = (props: SchemaInitializerItemProps) => {
}
if (item.type === 'itemGroup') {
return (
- // @ts-ignore
-
+
{renderMenuItem(item.children)}
);
}
if (item.type === 'subMenu') {
return (
- // @ts-ignore
-
+
{renderMenuItem(item.children)}
);
diff --git a/packages/client/src/schema-initializer/types.ts b/packages/client/src/schema-initializer/types.ts
index 1a1214452..b44f649d2 100644
--- a/packages/client/src/schema-initializer/types.ts
+++ b/packages/client/src/schema-initializer/types.ts
@@ -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 {
diff --git a/packages/client/src/schema-settings/GeneralSchemaDesigner.tsx b/packages/client/src/schema-settings/GeneralSchemaDesigner.tsx
index 92c916e07..b0a56a7eb 100644
--- a/packages/client/src/schema-settings/GeneralSchemaDesigner.tsx
+++ b/packages/client/src/schema-settings/GeneralSchemaDesigner.tsx
@@ -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 (
- {title &&
{compile(title)}
}
+ {title && (
+
+
+ {compile(title)}
+ {template && 引用模板: {template?.name || '未命名'}}
+
+
+ )}
{draggable && (
diff --git a/packages/client/src/schema-settings/SchemaSettings.tsx b/packages/client/src/schema-settings/SchemaSettings.tsx
index caa3b1059..be8421128 100644
--- a/packages/client/src/schema-settings/SchemaSettings.tsx
+++ b/packages/client/src/schema-settings/SchemaSettings.tsx
@@ -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(null);
@@ -48,19 +61,24 @@ interface SchemaSettingsProviderProps {
fieldSchema?: Schema;
setVisible?: any;
visible?: any;
+ template?: any;
+ collectionName?: any;
}
export const SchemaSettingsProvider: React.FC = (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 (
-
+
{children}
);
};
export const SchemaSettings: React.FC & SchemaSettingsNested = (props) => {
- const { title, dn, field, fieldSchema } = props;
+ const { title, dn, ...others } = props;
const [visible, setVisible] = useState(false);
const DropdownMenu = (
& SchemaSettingsNeste
);
if (dn) {
return (
-
+
{DropdownMenu}
);
@@ -83,6 +101,86 @@ export const SchemaSettings: React.FC & 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 (
+ {
+ 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')}
+
+ );
+ }
+ return (
+ {
+ setVisible(false);
+ const values = await FormDialog('Save as template', () => {
+ return (
+
+
+
+ );
+ }).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 = (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 (
{
@@ -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);
+ }
},
});
}}
diff --git a/packages/client/src/schema-templates/AddBlockTemplate.tsx b/packages/client/src/schema-templates/AddBlockTemplate.tsx
new file mode 100644
index 000000000..4e3b7751e
--- /dev/null
+++ b/packages/client/src/schema-templates/AddBlockTemplate.tsx
@@ -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 (
+ <>
+
+
+ {collections?.map((collection) => {
+ return (
+ {
+ const uiSchema = createTableBlockSchema(collection.name);
+ insert({ uiSchema, collectionName: collection.name });
+ }}
+ >
+ {compile(collection.title)}
+
+ );
+ })}
+
+
+ }
+ >
+
+
+ >
+ );
+};
diff --git a/packages/client/src/schema-templates/BlockTemplate.tsx b/packages/client/src/schema-templates/BlockTemplate.tsx
new file mode 100644
index 000000000..cbf154932
--- /dev/null
+++ b/packages/client/src/schema-templates/BlockTemplate.tsx
@@ -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({});
+
+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 (
+
+
+
+
+
+ );
+});
diff --git a/packages/client/src/schema-templates/BlockTemplateDetails.tsx b/packages/client/src/schema-templates/BlockTemplateDetails.tsx
new file mode 100644
index 000000000..30bc7ec34
--- /dev/null
+++ b/packages/client/src/schema-templates/BlockTemplateDetails.tsx
@@ -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 (
+
+ {visible ? (
+
{
+ setVisible(false);
+ }}
+ onChange={(e) => {
+ setTitle(e.target.value);
+ run({
+ filterByTk: props.filterByTk,
+ values: {
+ name: e.target.value,
+ },
+ });
+ }}
+ />
+ ) : (
+
{
+ setVisible(true);
+ }}
+ >
+ {title || 未命名}
+
+ )}
+
+ );
+};
+
+export const BlockTemplateDetails = () => {
+ const history = useHistory();
+ const match = useRouteMatch();
+ const key = match?.params?.key;
+ const value = useContext(SchemaComponentContext);
+ const { data, loading } = useRequest({
+ resource: 'uiSchemaTemplates',
+ action: 'get',
+ params: {
+ filterByTk: key,
+ },
+ });
+ if (loading) {
+ return ;
+ }
+ return (
+
+
{
+ history.push('/admin/block-templates');
+ }}
+ ghost={false}
+ title={}
+ />
+
+
+
+
+
+
+ );
+};
diff --git a/packages/client/src/schema-templates/BlockTemplatePage.tsx b/packages/client/src/schema-templates/BlockTemplatePage.tsx
new file mode 100644
index 000000000..9c63f99a3
--- /dev/null
+++ b/packages/client/src/schema-templates/BlockTemplatePage.tsx
@@ -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 (
+
+
+
+
+ <>{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) => 查看,
+ },
+ ]}
+ loading={loading}
+ dataSource={data?.data}
+ />
+
+
+
+ );
+};
diff --git a/packages/client/src/schema-templates/SchemaTemplateManagerProvider.tsx b/packages/client/src/schema-templates/SchemaTemplateManagerProvider.tsx
new file mode 100644
index 000000000..d71f345fe
--- /dev/null
+++ b/packages/client/src/schema-templates/SchemaTemplateManagerProvider.tsx
@@ -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({});
+
+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 {props.children};
+};
+
+export const SchemaTemplateManagerProvider: React.FC = (props) => {
+ const { templates, refresh } = props;
+ return (
+
+
+ {props.children}
+
+
+ );
+};
+
+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 ;
+ }
+ return (
+ {
+ const { data } = await api.request(options);
+ service.mutate(data);
+ }}
+ templates={service?.data?.data}
+ >
+ {props.children}
+
+ );
+};
diff --git a/packages/client/src/schema-templates/SchemaTemplateShortcut.tsx b/packages/client/src/schema-templates/SchemaTemplateShortcut.tsx
new file mode 100644
index 000000000..4bf7816ef
--- /dev/null
+++ b/packages/client/src/schema-templates/SchemaTemplateShortcut.tsx
@@ -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 (
+ }
+ title={t('Block templates')}
+ onClick={() => {
+ history.push('/admin/block-templates');
+ }}
+ />
+ );
+};
diff --git a/packages/client/src/schema-templates/index.ts b/packages/client/src/schema-templates/index.ts
new file mode 100644
index 000000000..cd7aad709
--- /dev/null
+++ b/packages/client/src/schema-templates/index.ts
@@ -0,0 +1,5 @@
+export * from './BlockTemplateDetails';
+export * from './BlockTemplatePage';
+export * from './SchemaTemplateManagerProvider';
+export * from './SchemaTemplateShortcut';
+
diff --git a/packages/client/src/schema-templates/schemas/uiSchemaTemplates.ts b/packages/client/src/schema-templates/schemas/uiSchemaTemplates.ts
new file mode 100644
index 000000000..0dee6e3b8
--- /dev/null
+++ b/packages/client/src/schema-templates/schemas/uiSchemaTemplates.ts
@@ -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 }}',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+};
diff --git a/packages/plugin-ui-routes-storage/src/index.ts b/packages/plugin-ui-routes-storage/src/index.ts
index e08972368..77bc6d3f6 100644
--- a/packages/plugin-ui-routes-storage/src/index.ts
+++ b/packages/plugin-ui-routes-storage/src/index.ts
@@ -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(.+)?',
diff --git a/packages/plugin-ui-schema-storage/src/collections/uiSchemaTemplates.ts b/packages/plugin-ui-schema-storage/src/collections/uiSchemaTemplates.ts
new file mode 100644
index 000000000..745288c42
--- /dev/null
+++ b/packages/plugin-ui-schema-storage/src/collections/uiSchemaTemplates.ts
@@ -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',
+ },
+ ],
+});