gchust d31aa4a91c
feat: add convert template block to page block action (#6662)
* feat: add convert template block to page block action

* feat: convert template block to normal block

* fix: remove remaining template properties

* fix: list template not loading data

* fix: user menu render error

* fix: save as template and convert to block should hide in workflow config page

* fix: incorrect translation
2025-04-22 13:25:29 +08:00

330 lines
11 KiB
TypeScript

/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import {
ISchema,
SchemaSettingsModalItem,
useResource,
useSchemaSettings,
useSchemaTemplateManager,
} from '@nocobase/client';
import React from 'react';
import { useT } from '../locale';
import { useFieldSchema, useField, useForm } from '@formily/react';
import { useAPIClient, usePlugin, useDesignable } from '@nocobase/client';
import { uid } from '@nocobase/utils/client';
import { App } from 'antd';
import { PluginBlockTemplateClient } from '../index';
import { blockKeepProps } from '../initializers/TemplateBlockInitializer';
import _ from 'lodash';
import { addToolbarClass, syncExtraTemplateInfo } from '../utils/template';
import { useBlockTemplateMenus } from './BlockTemplateMenusProvider';
import { useLocation } from 'react-router-dom';
const blockDecoratorMenuMaps = {
TableBlockProvider: ['Table', 'table'],
FormBlockProvider: ['FormItem', 'form'],
DetailsBlockProvider: ['Details', 'details'],
'List.Decorator': ['List', 'list'],
'GridCard.Decorator': ['GridCard', 'gridCard'],
CalendarBlockProvider: ['Calendar', 'calendar'],
GanttBlockProvider: ['Gantt', 'gantt'],
KanbanBlockProvider: ['Kanban', 'kanban'],
FilterFormBlockProvider: ['FilterFormItem', 'filterForm'],
'AssociationFilter.Provider': ['FilterCollapse', 'filterCollapse'],
};
export const SaveAsTemplateSetting = () => {
const t = useT();
const fieldSchema = useFieldSchema();
const field = useField();
const { refresh } = useDesignable();
const form = useForm();
const api = useAPIClient();
const blockTemplatesResource = useResource('blockTemplates');
const blockId = uid();
const { message } = App.useApp();
const plugin = usePlugin(PluginBlockTemplateClient);
const { templates } = useBlockTemplateMenus();
const location = useLocation();
const { template: deprecatedTemplate } = useSchemaSettings();
const schemaTemplateManager = useSchemaTemplateManager();
return (
<SchemaSettingsModalItem
title={t('Save as template')}
schema={
{
type: 'object',
title: t('Save as template'),
properties: {
title: {
type: 'string',
'x-decorator': 'FormItem',
title: t('Title'),
required: true,
'x-component': 'Input',
},
key: {
type: 'string',
'x-decorator': 'FormItem',
title: t('Name'),
'x-component': 'Input',
'x-validator': 'uid',
required: true,
'x-value': `t_${uid()}`,
description:
"{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}",
},
description: {
type: 'string',
'x-decorator': 'FormItem',
title: t('Description'),
'x-component': 'Input.TextArea',
},
},
} as ISchema
}
onSubmit={async ({ title, key, description }) => {
// step 0: get type of the current block
const type = location.pathname.startsWith('/page/') ? 'Mobile' : 'Desktop';
const schemaUid = uid();
const isMobile = type === 'Mobile';
const templateSchema = getTemplateSchemaFromPage(fieldSchema.toJSON());
if (deprecatedTemplate || (await containsReferenceTemplate(templateSchema, schemaTemplateManager))) {
message.error(t('This block is using some reference templates, please convert to duplicate template first.'));
return;
}
const schemaOfTemplate = {
type: 'void',
name: key,
'x-uid': `template-${schemaUid}`,
_isJSONSchemaObject: true,
properties: {
template: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'div',
...(isMobile
? {
'x-component-props': {
style: {
padding: '10px',
maxHeight: '100%',
overflow: 'scroll',
},
},
}
: {}),
properties: {
blocks: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-decorator': 'TemplateGridDecorator',
'x-component': 'Grid',
'x-initializer': isMobile ? 'mobile:addBlock' : 'page:addBlock',
'x-uid': blockId,
'x-async': false,
'x-index': 1,
properties: {},
},
},
'x-uid': schemaUid,
'x-async': true,
'x-index': 1,
},
},
};
const menuInfo = blockDecoratorMenuMaps[fieldSchema['x-decorator']];
const newTemplate = {
title,
key,
type,
description,
uid: schemaUid,
configured: true,
collection: fieldSchema['x-decorator-props']?.collection,
dataSource: fieldSchema['x-decorator-props']?.dataSource,
componentType: menuInfo[0],
menuName: menuInfo[1],
};
// step 1: create a template
await api.resource('blockTemplates').create({ values: newTemplate });
templates.push(newTemplate);
plugin.templateInfos.set(key, newTemplate);
// step 2: create a template blcok schema
await api.resource('uiSchemas').insert({ values: schemaOfTemplate });
plugin.setTemplateCache(templateSchema);
await api.request({
url: `/uiSchemas:insertAdjacent/${blockId}?position=beforeEnd`,
method: 'POST',
data: {
schema: templateSchema,
wrap: {
name: uid(),
type: 'void',
'x-component': 'Grid.Row',
'x-index': 1,
properties: {
[uid()]: {
type: 'void',
'x-component': 'Grid.Col',
'x-index': 1,
},
},
},
},
});
// this is a hack to make the schema component refresh to the new schema
const schema = fieldSchema.toJSON();
const fillTemplateInfo = (s: ISchema, t: ISchema) => {
if (!t) {
return;
}
s['x-block-template-key'] = key;
s['x-template-uid'] = t['x-uid'];
if (t.properties) {
for (const key in t.properties) {
fillTemplateInfo(s.properties[key], t.properties[key]);
}
}
};
const keepProps = [
'x-uid',
'version',
'x-template-uid',
'x-block-template-key',
'x-template-root-uid',
...blockKeepProps,
];
const getAllSchemas = (s: ISchema) => {
const sKeys = Object.keys(s);
const omitProps = _.reduce(
sKeys,
(acc, key) => {
acc[key] = null;
return acc;
},
{},
);
const ret = [{ ...omitProps, ..._.pick(s, keepProps) }];
if (s.properties) {
for (const key in s.properties) {
ret.push(...getAllSchemas(s.properties[key]));
}
}
return ret;
};
schema['x-template-root-uid'] = templateSchema['x-uid'];
schema['x-block-template-key'] = key;
schema['x-index'] = fieldSchema['x-index'];
fillTemplateInfo(schema, templateSchema);
// step 3: batchpatch the schema for sync with template
await api.request({
url: `/uiSchemas:batchPatch`,
method: 'POST',
data: getAllSchemas(schema),
});
// step 4: create a link between template and block
await blockTemplatesResource.link({
values: {
templateKey: key,
templateBlockUid: templateSchema['x-uid'],
blockUid: fieldSchema['x-uid'],
},
});
fieldSchema.toJSON = () => {
addToolbarClass(schema);
syncExtraTemplateInfo(schema, plugin.templateInfos, plugin.savedSchemaUids);
return schema;
};
refresh({ refreshParentSchema: true });
// set componentProps, otherwise some components props will not be refreshed
field['componentProps'] = {
...templateSchema['x-component-props'],
key: uid(),
};
if (field.parent?.['componentProps']) {
field.parent['componentProps'] = {
...field.parent['componentProps'],
key: uid(),
};
}
// set decoratorProps, otherwise title will not be refreshed
field['decoratorProps'] = {
...field['decoratorProps'],
...templateSchema['x-decorator-props'],
key: uid(),
};
if (field.parent?.['decoratorProps']) {
field.parent['decoratorProps'] = {
...field.parent['decoratorProps'],
key: uid(),
};
}
form.reset();
form.clearFormGraph('*', false);
message.success(t('Save as template successfully'));
}}
/>
);
};
function getTemplateSchemaFromPage(schema: ISchema) {
const templateSchema = {};
const traverseSchema = (s: ISchema, t: ISchema) => {
if (s['x-template-root-uid']) {
return;
}
t = t || {};
_.merge(t, _.omit(s, ['x-uid', 'properties']));
t['x-uid'] = uid();
if (s.properties) {
for (const key in s.properties) {
if (s.properties[key]['x-template-root-uid']) {
continue;
}
_.set(t, `properties.['${key}']`, {});
traverseSchema(s.properties[key], t.properties[key]);
// array's key will be set to number when render, so we need to set the name to the key
if (s.type === 'array' && t['properties']?.[key]?.name) {
_.set(t, `properties.['${key}'].name`, key);
}
}
}
};
traverseSchema(schema, templateSchema);
return templateSchema;
}
async function containsReferenceTemplate(
schema: ISchema,
schemaTemplateManager: ReturnType<typeof useSchemaTemplateManager>,
) {
if (schema['x-component'] === 'BlockTemplate') {
const templateId = schema['x-component-props']?.templateId;
if (templateId && schemaTemplateManager.getTemplateById(templateId)) {
return true;
}
}
if (schema.properties) {
for (const key in schema.properties) {
if (await containsReferenceTemplate(schema.properties[key], schemaTemplateManager)) {
return true;
}
}
}
}