From 2fb019c3922c283b578c23eef7e08b786d06c138 Mon Sep 17 00:00:00 2001 From: chenos Date: Thu, 17 Mar 2022 09:41:48 +0800 Subject: [PATCH] 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 --- packages/app/src/pages/index.tsx | 50 +++--- packages/client/src/index.tsx | 1 + packages/client/src/locale/zh_CN.json | 7 +- .../src/route-switch/RouteSwitchProvider.tsx | 16 +- .../route-switch/antd/admin-layout/index.tsx | 14 +- packages/client/src/route-switch/context.ts | 1 + packages/client/src/route-switch/hooks.ts | 7 +- packages/client/src/route-switch/types.ts | 1 + .../antd/calendar/Calendar.Designer.tsx | 6 +- .../antd/form/Form.Designer.tsx | 2 + .../src/schema-component/antd/form/Form.tsx | 6 +- .../antd/kanban/Kanban.Designer.tsx | 6 +- .../antd/table/Table.Void.Designer.tsx | 6 +- .../schema-component/hooks/useDesignable.tsx | 20 ++- .../Items/CalendarBlockInitializer.tsx | 25 ++- .../Items/FormBlockInitializer.tsx | 28 ++-- .../Items/KanbanBlockInitializer.tsx | 24 ++- .../Items/TableBlockInitializer.tsx | 34 ++-- .../schema-initializer/Initializers/utils.ts | 77 ++++++++- .../schema-initializer/SchemaInitializer.tsx | 136 ++++++++-------- .../client/src/schema-initializer/types.ts | 2 + .../schema-settings/GeneralSchemaDesigner.tsx | 24 ++- .../src/schema-settings/SchemaSettings.tsx | 122 ++++++++++++-- .../src/schema-templates/AddBlockTemplate.tsx | 59 +++++++ .../src/schema-templates/BlockTemplate.tsx | 26 +++ .../schema-templates/BlockTemplateDetails.tsx | 91 +++++++++++ .../schema-templates/BlockTemplatePage.tsx | 56 +++++++ .../SchemaTemplateManagerProvider.tsx | 149 +++++++++++++++++ .../SchemaTemplateShortcut.tsx | 19 +++ packages/client/src/schema-templates/index.ts | 5 + .../schemas/uiSchemaTemplates.ts | 153 ++++++++++++++++++ .../plugin-ui-routes-storage/src/index.ts | 10 ++ .../src/collections/uiSchemaTemplates.ts | 35 ++++ 33 files changed, 1025 insertions(+), 193 deletions(-) create mode 100644 packages/client/src/schema-templates/AddBlockTemplate.tsx create mode 100644 packages/client/src/schema-templates/BlockTemplate.tsx create mode 100644 packages/client/src/schema-templates/BlockTemplateDetails.tsx create mode 100644 packages/client/src/schema-templates/BlockTemplatePage.tsx create mode 100644 packages/client/src/schema-templates/SchemaTemplateManagerProvider.tsx create mode 100644 packages/client/src/schema-templates/SchemaTemplateShortcut.tsx create mode 100644 packages/client/src/schema-templates/index.ts create mode 100644 packages/client/src/schema-templates/schemas/uiSchemaTemplates.ts create mode 100644 packages/plugin-ui-schema-storage/src/collections/uiSchemaTemplates.ts 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 = ( - - {items?.map((item, indexA) => { - if (item.type === 'divider') { - return ; - } - if (item.type === 'item' && item.component) { - const Component = findComponent(item.component); - return ( - Component && ( - - - - ) - ); - } + const renderItems = (items: any) => { + return items?.map((item, indexA) => { + if (item.type === 'divider') { + return ; + } + if (item.type === 'item' && item.component) { + const Component = findComponent(item.component); return ( - item?.children?.length > 0 && ( - - {item?.children?.map((child, indexB) => { - if (!child.component) { - return null; - } - const Component = findComponent(child.component); - return ( - Component && ( - - - - ) - ); - })} - + Component && ( + + + ) ); - })} - - ); + } + if (item.type === 'itemGroup') { + return ( + + {renderItems(item.children)} + + ); + } + if (item.type === 'subMenu') { + return ( + + {renderItems(item.children)} + + ); + } + }); + }; - if (!designable) { + const menu = {renderItems(items)}; + + 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', + }, + ], +});