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
This commit is contained in:
gchust 2025-04-22 13:25:29 +08:00 committed by GitHub
parent 5eb337bd7a
commit d31aa4a91c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 222 additions and 3 deletions

View File

@ -0,0 +1,182 @@
/**
* 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 { SchemaSettingsItem, useAPIClient, useDesignable, useFormBlockProps } from '@nocobase/client';
import { useFieldSchema, useForm, useField } from '@formily/react';
import { App } from 'antd';
import React from 'react';
import _ from 'lodash';
import { Schema } from '@formily/json-schema';
import { useT } from '../locale';
import { uid } from '@nocobase/utils/client';
const findInsertPosition = (parentSchema, uid) => {
const postion = {
insertPosition: 'beforeBegin',
insertTarget: null,
};
const properties = Object.values(parentSchema.properties || {}).sort((a, b) => {
return (a as any)['x-index'] - (b as any)['x-index'];
});
for (let i = 0; i < properties.length; i++) {
const property = properties[i];
if ((property as any)['x-uid'] === uid) {
postion.insertPosition = 'beforeBegin';
if (i === properties.length - 1) {
postion.insertPosition = 'beforeEnd';
postion.insertTarget = parentSchema['x-uid'];
} else {
postion.insertPosition = 'beforeBegin';
postion.insertTarget = (properties[i + 1] as any)['x-uid'];
}
}
}
return postion;
};
const findParentRootTemplateSchema = (fieldSchema) => {
if (!fieldSchema) {
return null;
}
if (fieldSchema['x-template-root-uid']) {
return fieldSchema;
} else {
return findParentRootTemplateSchema(fieldSchema.parent);
}
};
// Create a copy of the schema with all template associations removed
const convertToNormalBlockSchema = (schema) => {
const newSchema = _.cloneDeep(schema);
// Remove template associations from the schema
const removeTemplateAssociations = (s) => {
// Remove template-specific properties
delete s['x-template-uid'];
delete s['x-template-root-uid'];
delete s['x-template-version'];
delete s['x-block-template-key'];
delete s['x-template-root-ref'];
delete s['x-template-title'];
delete s['x-virtual'];
if (s['x-toolbar-props']?.toolbarClassName?.includes('nb-in-template')) {
s['x-toolbar-props'].toolbarClassName = s['x-toolbar-props'].toolbarClassName.replace('nb-in-template', '');
}
if (s['x-uid']) {
s['x-uid'] = uid();
}
// Process nested properties
if (s.properties) {
for (const key in s.properties) {
if (!s.properties[key]['x-template-root-uid']) {
removeTemplateAssociations(s.properties[key]);
}
}
}
};
removeTemplateAssociations(newSchema);
return newSchema;
};
export const ConvertToNormalBlockSetting = () => {
const { refresh } = useDesignable();
const t = useT();
const api = useAPIClient();
const form = useForm();
const field = useField();
const { form: blockForm } = useFormBlockProps();
const fieldSchema = useFieldSchema();
const { modal, message } = App.useApp();
const blockTemplatesResource = api.resource('blockTemplates');
const confirm = {
okText: t('Yes'),
cancelText: t('No'),
};
return (
<SchemaSettingsItem
title={t('Convert to normal block')}
onClick={() => {
modal.confirm({
title: t('Convert to normal block'),
content: t('Are you sure you want to convert this template block to a normal block?'),
...confirm,
async onOk() {
const newSchema = convertToNormalBlockSchema(fieldSchema.toJSON());
const position = findInsertPosition(fieldSchema.parent, fieldSchema['x-uid']);
// TODO: Remove old schema, and links
// Remove old schema
await api.request({
url: `/uiSchemas:remove/${fieldSchema['x-uid']}`,
});
// Insert new schema
const schema = new Schema(newSchema);
await api.request({
url: `/uiSchemas:insertAdjacent/${position.insertTarget}?position=${position.insertPosition}`,
method: 'post',
data: {
schema,
},
});
// Update the UI to show the new schema
fieldSchema.toJSON = () => {
const ret = schema.toJSON();
return ret;
};
refresh({ refreshParentSchema: true });
// Update component properties
field['componentProps'] = {
...field['componentProps'],
key: uid(),
};
if (field.parent?.['componentProps']) {
field.parent['componentProps'] = {
...field.parent['componentProps'],
key: uid(),
};
}
// Update decorator properties
field['decoratorProps'] = {
...field['decoratorProps'],
key: uid(),
};
if (field.parent?.['decoratorProps']) {
field.parent['decoratorProps'] = {
...field.parent['decoratorProps'],
key: uid(),
};
}
// Reset forms
form.reset();
blockForm?.reset();
form.clearFormGraph('*', false);
blockForm?.clearFormGraph('*', false);
message.success(t('Converted successfully'), 0.2);
},
});
}}
>
{t('Convert to normal block')}
</SchemaSettingsItem>
);
};

View File

@ -74,7 +74,7 @@ export const SaveAsTemplateSetting = () => {
key: {
type: 'string',
'x-decorator': 'FormItem',
title: t('Key'),
title: t('Name'),
'x-component': 'Input',
'x-validator': 'uid',
required: true,
@ -298,6 +298,10 @@ function getTemplateSchemaFromPage(schema: ISchema) {
}
_.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);
}
}
}
};

View File

@ -22,8 +22,9 @@ export const useIsPageBlock = () => {
const isPage = location.pathname.startsWith('/admin/') || location.pathname.startsWith('/page/');
const notInPopup = !location.pathname.includes('/popups/');
const notInSetting = !location.pathname.startsWith('/admin/settings/');
const notInWorkflow = !location.pathname.startsWith('/admin/workflow/workflows/');
const notInBlockTemplate = !location.pathname.startsWith('/block-templates/');
return isPage && notInPopup && notInSetting && notInBlockTemplate;
return isPage && notInPopup && notInSetting && notInWorkflow && notInBlockTemplate;
}, [location.pathname, fieldSchema]);
return isPageBlock;

View File

@ -28,6 +28,8 @@ import {
import { BlockTemplateMenusProvider } from './components/BlockTemplateMenusProvider';
import { disabledDeleteSettingItem } from './settings/disabledDeleteSetting';
import { saveAsTemplateSetting } from './settings/saveAsTemplateSetting';
import { convertToNormalBlockSettingItem } from './settings/convertToNormalBlockSetting';
export class PluginBlockTemplateClient extends Plugin {
templateInfos = new Map();
templateschemacache = {};
@ -158,9 +160,10 @@ export class PluginBlockTemplateClient extends Plugin {
deleteItemIndex !== -1 &&
!schemaSetting.items.find((item) => item.name === 'template-revertSettingItem')
) {
schemaSetting.items.splice(deleteItemIndex, 0, revertSettingItem);
schemaSetting.items.splice(deleteItemIndex, 0, revertSettingItem, convertToNormalBlockSettingItem);
} else {
schemaSetting.add('template-revertSettingItem', revertSettingItem);
schemaSetting.add('template-convertToNormalBlockSettingItem', convertToNormalBlockSettingItem);
}
schemaSetting.add('template-disabledDeleteItem', disabledDeleteSettingItem);
}

View File

@ -0,0 +1,23 @@
/**
* 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 { useFieldSchema } from '@formily/react';
import { ConvertToNormalBlockSetting } from '../components/ConvertToNormalBlockSetting';
import { tStr } from '../locale';
import { useIsInTemplate } from '../hooks/useIsInTemplate';
export const convertToNormalBlockSettingItem = {
name: 'template-convertToNormalBlockSettingItem',
title: tStr('Convert to normal block'),
Component: ConvertToNormalBlockSetting,
useVisible: () => {
const fieldSchema = useFieldSchema();
return fieldSchema?.['x-template-root-uid'];
},
};

View File

@ -12,7 +12,9 @@
"Mobile": "Mobile",
"Current": "Current record",
"Revert to template": "Revert to template",
"Convert to normal block": "Convert to normal block",
"Are you sure you want to revert all changes from the template?": "Are you sure you want to revert all changes from the template?",
"Are you sure you want to convert this template block to a normal block?": "Are you sure you want to convert this template block to a normal block?",
"Templates": "Templates",
"Block templates": "Block templates",
"Submit": "Submit",
@ -22,6 +24,7 @@
"Saved successfully": "Saved successfully",
"Block template": "Block template",
"Reset successfully": "Reset successfully",
"Converted successfully": "Converted successfully",
"Delete successfully": "Delete successfully",
"Template block settings": "Template block settings",
"Filter": "Filter",

View File

@ -12,7 +12,9 @@
"Mobile": "移动端",
"Current": "当前记录",
"Revert to template": "恢复到模板",
"Convert to normal block": "转换成普通区块",
"Are you sure you want to revert all changes from the template?": "您确定要恢复所有对模板的更改吗?",
"Are you sure you want to convert this template block to a normal block?": "您确定要将此模板区块转换为普通区块吗?",
"Templates": "模板",
"Block templates": "区块模板",
"Submit": "提交",
@ -22,6 +24,7 @@
"Saved successfully": "保存成功",
"Block template": "区块模板",
"Reset successfully": "重置成功",
"Converted successfully": "转换成功",
"Delete successfully": "删除成功",
"Template block settings": "模板区块设置",
"Filter": "筛选",