mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-05 21:49:25 +08:00
feat: new block template plugin (#5920)
* feat: new block template plugin * fix: build error * fix: support nested template in blocks * fix: remove undefined x-component properties from schema during template processing [skip ci] * feat: reset action for template block * fix: skip dnd error[skip ci] * fix: dupliate template action[skip ci] * fix: plugin not shown in test env * fix: reset action not been shown * fix: no permission error * fix: loading error in production env * fix: cross env dev error * fix: blocks not shown in production env * fix: insert template failing for the first time * chore: merge * fix: association record option not working correctly * fix: setting error * fix: setting * fix: linkage error * fix: association settings error * fix: association record error * feat: support edit form tpl * fix: support more blocks * fix: avoid nested template * chore: re org codes * chore: refactor[skip ci] * chore: required comment for loading template [skip ci] * refactor: simplify schema handling and add axios interceptors for template blocks[skip ci] * fix: i18n [skip ci] * feat: support showing template title in block * fix: revert to template will lost the template title * fix: delete operation removing blocks not working * fix: template label * fix: i18n error * fix: form type should be shown only when current record * feat: show template in the add new popup * fix: form type switch should not be shown in add new block popup * fix: missing i18n * fix: associate fields should be shown only having current record * fix: switching form not working in cn * fix: incorrect form associate setting * fix: uniq action issue * fix: uniq fields/actions[skip ci] * fix: style issue * fix: error in configure actions * fix: bulk destroy when no records selected * fix: only show revert to template config in the block level * fix: table refresh pagination incorrect after deletion * fix: be able to input name of template * fix: bulk destroy * fix: reset setting will sync with template * fix: block template table style * fix: missing translatation * fix: cache issue * fix: blocks not shown in the popup after modifing the template * feat: add search for template initializers * fix: some blocks are missing template related setting * fix: hide save as template in template configure view * chore: revert incorrect commit * fix: batchpatch error * fix: mobile support * fix: build error * fix: limit one template one block * feat: show revert to template for fields and actions * fix: build error * fix: revert to template not refreshing the ui for actions * fix: revert to template not working for form * fix: duplicate revert to setting setting item * chore: rename reset to revert * feat: disallow delete template's blocks in page * feat: add colorTemplateBgSettingsHover for template block hover state * fix: build error * chore: hide convert to block template setting item for page * fix: data template should be hidden in edit form * fix: fields switch should be disabled * chore: rename var [skip ci] * fix: should not be able remove field in block * fix: after revert settings, is able to remove block in template * fix: revert settings * fix: nested template block error * refactor: cache * fix: template key has not been validated in client side * fix: only show template that has been configured * feat: show template name in edit form * chore: update package.json * fix: duplicated fields after dnd in form * fix: duplicated fields shown in form * chore: hide old block template menu * feat: support mobile block template [skip ci] * fix: filter for type options [skip ci] * fix: incorrect create new button style [skip ci] * feat: add mobile block support * fix: can't restrict one template one block [skip ci] * fix: template title not synced after editing template * fix: keep block deletion should transform the template block to normal block * fix: insert template fails for the first time * fix: destroy error * fix: deploy failure [skip ci] * fix: destory error in subapp with sub domain * fix: destroy error * fix: popup action should be hidden in create new form [skip ci] * fix: possible crash when converting template to block [skip ci] * fix: some properties have not been revert in real time * fix: fitler action condition error * fix: useDataBlockResource error * fix: revert settings not refresh filter action form [skip ci] * fix: new template properties clear * fix: custom request action can't be shown * fix: template tab should not be removable * fix: duplicated delete action in table column * fix: field link popup not shown in block template page * fix: page configure link not working correctly * chore: revert useContextVaraible [skip ci] * fix(popup): fix configured page not taking effect * fix: add blocks button shown in block settings * fix: only loading 20 templates * fix: add block icon has been shown [skip ci] * fix: association not shown in block template configure page * fix: edit association form error [skip ci] * fix: console error [skip ci] * fix: only current field has been refreshed after revert to template [skip ci] * chore: remove incorrect commit file [skip ci] * fix: some action delete action still be shown for template block [skip ci] * fix: template block style[skip ci] * fix: keep position when rever setting[skip ci] * fix: revert to template error when already deleted from block [skip ci] * fix: revert still works even the template has been deleted * fix: popup not shown as template block [skip ci] * fix: bulk destory can't keep blocks[skip ci] * fix: after dnd some unique blocks may be duplicated in the client [skip ci] * fix: duplicate fields error in form * fix: revert setting incorrect [skip ci] * fix: duplicated sub form * refactor: simplify by moving template loading to backend [skip ci] * fix: failing to add template block [skip ci] * fix: assign fields values in bulk update action [skip ci] * fix: can't add fields in nester popup subform [skip ci] * fix: sub-table sub-form not merged correctly [skip ci] * fix: subtable not highlight unique fields correctly [skip ci] * fix: block title will not be synced correctly [skip ci] * fix: can't add block into template [skip ci] * fix: revert setting not refreshed [skip ci] * fix: block template title not correct in some cases [skip ci] * fix: field link's popup merge[skip ci] * fix: cannot read properties of null in some popup [skip ci] * fix: position of ui components have not been saved after dnd [skip ci] * fix: dnd position not saved correctly in some cases [skip ci] * fix: dnd undefined type error [skip ci] * fix: can't swtich form type * fix: some form type switch error [skip ci] * fix: hide chart block from templates [skip ci] * feat: support hide some blocks from template [skip ci] * fix: react error for revert submit button of edit form [skip ci] * fix: hide workflow and approvar block from template * fix: hide connect data block from template [skip ci] * fix: error [skip ci] * fix: associate record options have not been shown for create form [skip ci] * fix: creat form popup will be shown after refresh if switch form type [skip ci] * fix: associate record settings error [skip ci] * fix: mobile template setting page can not open popup [skip ci] * fix: mobile content overflow [skip ci] * chore: update templates menu style [skip ci] * fix: unique disassociate btn [skip ci] * feat: save collection and component info while update template * fix: clear template context info after remove block * chore: update template block entry point * fix: incorrect association field template block [skip ci] * fix: template title only shown after refresh * fix: tooltip can't be revert correctly after moving entry to submenu [skip ci] * fix: incorrect behavior after adding collection submenu entries [skip ci] * fix: edit form support * fix: support current details [skip ci] * fix: edit form btn not correct [skip ci] * fix: create form action params [skip ci] * fix: hide template menu entry in block template configure page [skip ci] * fix: incorrect association [skip ci] * fix: nested template [skip ci] * fix: can't insert template in mobile [skip ci] * fix: association title not correct for details block [skip ci] * chore: remove incorrect submodules commit [skip ci] * fix: create form is using post method [skip ci] * fix: association hasone and belongsto details block error [skip ci] * fix: edit form not able load data [skip ci] * chore: hide tab bar in mobile template configure page [skip ci] * fix: don't show no accessible pages for template configure page [skip ci] * fix: not able to see template list on the first time opening popup [skip ci] * fix: able to remove related approvals template block [skip ci] * fix: data not loading for details hasone relationship [skip ci] * fix: dnd position may not be saved [skip ci] * fix: multi-step-form is able to delete template step[skip ci] * fix: hide rever setting in multi step form step name [skip ci] * fix: extrol wrap when adding blocks from template [skip ci] * fix: nested schema patch [skip ci] * fix: x-acl-action not correct[skip ci] * fix: diassociate action should be unique [skip ci] * fix: disassociate action not unique [skip ci] * fix: mobile popup by url not working [skip ci] * chore: code clean * chore: update delete setting position [skip ci] * chore: keep revert btn position consistant in all places [skip ci] * chore: hide template from workflow setting page [skip ci] * chore: update setting menu icon [skip ci] * chore: rename provider name to avoid duplicated with core [skip ci] * chore: move block template menu to an upper level [skip ci] * fix: hide other block templates menu [skip ci] * fix: hide other blocks when creating template * fix: duplicate revert to template option * fix: mail manager template block not shown in popup * fix: main block not showing in popup * chore: mark old template features as deprecated * chore: hide deprecated templates in block template configure page * fix: read x-virtual from null * fix: ci test error * fix: skip old templates e2e test cases * fix: skip old templates e2e test cases --------- Co-authored-by: Zeke Zhang <958414905@qq.com>
This commit is contained in:
parent
4e3a567c2a
commit
ae6b801132
@ -46,6 +46,7 @@ export const CSSVariableProvider = ({ children }) => {
|
|||||||
document.body.style.setProperty('--colorBgScrollBarActive', colorBgScrollBarActive);
|
document.body.style.setProperty('--colorBgScrollBarActive', colorBgScrollBarActive);
|
||||||
document.body.style.setProperty('--colorSettings', token.colorSettings || defaultTheme.token.colorSettings);
|
document.body.style.setProperty('--colorSettings', token.colorSettings || defaultTheme.token.colorSettings);
|
||||||
document.body.style.setProperty('--colorBgSettingsHover', token.colorBgSettingsHover);
|
document.body.style.setProperty('--colorBgSettingsHover', token.colorBgSettingsHover);
|
||||||
|
document.body.style.setProperty('--colorTemplateBgSettingsHover', token.colorTemplateBgSettingsHover);
|
||||||
document.body.style.setProperty('--colorBorderSettingsHover', token.colorBorderSettingsHover);
|
document.body.style.setProperty('--colorBorderSettingsHover', token.colorBorderSettingsHover);
|
||||||
|
|
||||||
// 设置登录页面的背景色
|
// 设置登录页面的背景色
|
||||||
@ -58,6 +59,7 @@ export const CSSVariableProvider = ({ children }) => {
|
|||||||
token.colorBgContainer,
|
token.colorBgContainer,
|
||||||
token.colorBgLayout,
|
token.colorBgLayout,
|
||||||
token.colorBgSettingsHover,
|
token.colorBgSettingsHover,
|
||||||
|
token.colorTemplateBgSettingsHover,
|
||||||
token.colorBorderSettingsHover,
|
token.colorBorderSettingsHover,
|
||||||
token.colorInfoBg,
|
token.colorInfoBg,
|
||||||
token.colorInfoBorder,
|
token.colorInfoBorder,
|
||||||
|
@ -24,6 +24,7 @@ const defaultTheme: ThemeConfig = {
|
|||||||
// UI 配置组件
|
// UI 配置组件
|
||||||
colorSettings: '#F18B62',
|
colorSettings: '#F18B62',
|
||||||
colorBgSettingsHover: 'rgba(241, 139, 98, 0.06)',
|
colorBgSettingsHover: 'rgba(241, 139, 98, 0.06)',
|
||||||
|
colorTemplateBgSettingsHover: 'rgba(98, 200, 241, 0.06)', // 默认为colorBgSettingsHover的互补色
|
||||||
colorBorderSettingsHover: 'rgba(241, 139, 98, 0.3)',
|
colorBorderSettingsHover: 'rgba(241, 139, 98, 0.3)',
|
||||||
|
|
||||||
// 动画相关
|
// 动画相关
|
||||||
|
@ -30,6 +30,8 @@ export interface CustomToken extends AliasToken {
|
|||||||
colorSettings: string;
|
colorSettings: string;
|
||||||
/** 鼠标悬浮时显示的背景色 */
|
/** 鼠标悬浮时显示的背景色 */
|
||||||
colorBgSettingsHover: string;
|
colorBgSettingsHover: string;
|
||||||
|
/** 鼠标悬浮模板区块时显示的背景色 */
|
||||||
|
colorTemplateBgSettingsHover: string;
|
||||||
/** 鼠标悬浮时显示的边框色 */
|
/** 鼠标悬浮时显示的边框色 */
|
||||||
colorBorderSettingsHover: string;
|
colorBorderSettingsHover: string;
|
||||||
|
|
||||||
|
@ -164,6 +164,7 @@
|
|||||||
"Chart type": "Chart type",
|
"Chart type": "Chart type",
|
||||||
"Chart config": "Chart config",
|
"Chart config": "Chart config",
|
||||||
"Templates": "Templates",
|
"Templates": "Templates",
|
||||||
|
"Template": "Template",
|
||||||
"Select template": "Select template",
|
"Select template": "Select template",
|
||||||
"Action logs": "Action logs",
|
"Action logs": "Action logs",
|
||||||
"Create template": "Create template",
|
"Create template": "Create template",
|
||||||
@ -882,5 +883,7 @@
|
|||||||
"Are you sure you want to hide this menu?": "Are you sure you want to hide this menu?",
|
"Are you sure you want to hide this menu?": "Are you sure you want to hide this menu?",
|
||||||
"After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.": "After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.",
|
"After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.": "After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.",
|
||||||
"If selected, the page will display Tab pages.": "If selected, the page will display Tab pages.",
|
"If selected, the page will display Tab pages.": "If selected, the page will display Tab pages.",
|
||||||
"If selected, the route will be displayed in the menu.": "If selected, the route will be displayed in the menu."
|
"If selected, the route will be displayed in the menu.": "If selected, the route will be displayed in the menu.",
|
||||||
|
"Deprecated": "Deprecated",
|
||||||
|
"The following old template features have been deprecated and will be removed in next version.": "The following old template features have been deprecated and will be removed in next version."
|
||||||
}
|
}
|
||||||
|
@ -148,6 +148,7 @@
|
|||||||
"Chart type": "Tipo del gráfico",
|
"Chart type": "Tipo del gráfico",
|
||||||
"Chart config": "Configuración del gráfico",
|
"Chart config": "Configuración del gráfico",
|
||||||
"Templates": "Plantillas",
|
"Templates": "Plantillas",
|
||||||
|
"Template": "Plantilla",
|
||||||
"Select template": "Seleccione plantilla",
|
"Select template": "Seleccione plantilla",
|
||||||
"Action logs": "Acción logs",
|
"Action logs": "Acción logs",
|
||||||
"Create template": "Crear plantilla",
|
"Create template": "Crear plantilla",
|
||||||
@ -799,5 +800,7 @@
|
|||||||
"Are you sure you want to hide this menu?": "¿Estás seguro de que quieres ocultar este menú?",
|
"Are you sure you want to hide this menu?": "¿Estás seguro de que quieres ocultar este menú?",
|
||||||
"After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.": "Después de ocultar, este menú ya no aparecerá en la barra de menú. Para mostrarlo de nuevo, debe ir a la página de administración de rutas para configurarlo.",
|
"After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.": "Después de ocultar, este menú ya no aparecerá en la barra de menú. Para mostrarlo de nuevo, debe ir a la página de administración de rutas para configurarlo.",
|
||||||
"If selected, the page will display Tab pages.": "Si se selecciona, la página mostrará páginas de pestañas.",
|
"If selected, the page will display Tab pages.": "Si se selecciona, la página mostrará páginas de pestañas.",
|
||||||
"If selected, the route will be displayed in the menu.": "Si se selecciona, la ruta se mostrará en el menú."
|
"If selected, the route will be displayed in the menu.": "Si se selecciona, la ruta se mostrará en el menú.",
|
||||||
|
"Deprecated": "Obsoleto",
|
||||||
|
"The following old template features have been deprecated and will be removed in next version.": "Las siguientes características de plantilla antigua han quedado obsoletas y se eliminarán en la próxima versión."
|
||||||
}
|
}
|
||||||
|
@ -159,6 +159,7 @@
|
|||||||
"Chart type": "Type de graphique",
|
"Chart type": "Type de graphique",
|
||||||
"Chart config": "Configuration du graphique",
|
"Chart config": "Configuration du graphique",
|
||||||
"Templates": "Modèles",
|
"Templates": "Modèles",
|
||||||
|
"Template": "Modèle",
|
||||||
"Select template": "Sélectionner un modèle",
|
"Select template": "Sélectionner un modèle",
|
||||||
"Action logs": "Logs d'action",
|
"Action logs": "Logs d'action",
|
||||||
"Create template": "Créer un modèle",
|
"Create template": "Créer un modèle",
|
||||||
@ -819,5 +820,7 @@
|
|||||||
"Are you sure you want to hide this menu?": "Êtes-vous sûr de vouloir masquer ce menu ?",
|
"Are you sure you want to hide this menu?": "Êtes-vous sûr de vouloir masquer ce menu ?",
|
||||||
"After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.": "Après avoir masqué, ce menu ne sera plus affiché dans la barre de menu. Pour le réafficher, vous devez aller à la page de gestion des routes pour le configurer.",
|
"After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.": "Après avoir masqué, ce menu ne sera plus affiché dans la barre de menu. Pour le réafficher, vous devez aller à la page de gestion des routes pour le configurer.",
|
||||||
"If selected, the page will display Tab pages.": "Si sélectionné, la page affichera des onglets.",
|
"If selected, the page will display Tab pages.": "Si sélectionné, la page affichera des onglets.",
|
||||||
"If selected, the route will be displayed in the menu.": "Si sélectionné, la route sera affichée dans le menu."
|
"If selected, the route will be displayed in the menu.": "Si sélectionné, la route sera affichée dans le menu.",
|
||||||
|
"Deprecated": "Déprécié",
|
||||||
|
"The following old template features have been deprecated and will be removed in next version.": "Les fonctionnalités des anciens modèles ont été dépréciées et seront supprimées dans la prochaine version."
|
||||||
}
|
}
|
||||||
|
@ -145,6 +145,7 @@
|
|||||||
"Chart type": "チャートタイプ",
|
"Chart type": "チャートタイプ",
|
||||||
"Chart config": "チャート設定",
|
"Chart config": "チャート設定",
|
||||||
"Templates": "テンプレート",
|
"Templates": "テンプレート",
|
||||||
|
"Template": "テンプレート",
|
||||||
"Select template": "テンプレートを選択してください",
|
"Select template": "テンプレートを選択してください",
|
||||||
"Action logs": "操作履歴",
|
"Action logs": "操作履歴",
|
||||||
"Create template": "テンプレートを作成",
|
"Create template": "テンプレートを作成",
|
||||||
@ -1037,5 +1038,7 @@
|
|||||||
"Are you sure you want to hide this menu?": "このメニューを非表示にしますか?",
|
"Are you sure you want to hide this menu?": "このメニューを非表示にしますか?",
|
||||||
"After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.": "非表示にすると、このメニューはメニューバーに表示されなくなります。再表示するには、ルート管理ページで設定する必要があります。",
|
"After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.": "非表示にすると、このメニューはメニューバーに表示されなくなります。再表示するには、ルート管理ページで設定する必要があります。",
|
||||||
"If selected, the page will display Tab pages.": "選択されている場合、ページはタブページを表示します。",
|
"If selected, the page will display Tab pages.": "選択されている場合、ページはタブページを表示します。",
|
||||||
"If selected, the route will be displayed in the menu.": "選択されている場合、ルートはメニューに表示されます。"
|
"If selected, the route will be displayed in the menu.": "選択されている場合、ルートはメニューに表示されます。",
|
||||||
|
"Deprecated": "非推奨",
|
||||||
|
"The following old template features have been deprecated and will be removed in next version.": "次の古いテンプレート機能は非推奨になり、次のバージョンで削除されます。"
|
||||||
}
|
}
|
||||||
|
@ -183,6 +183,7 @@
|
|||||||
"Chart type": "차트 유형",
|
"Chart type": "차트 유형",
|
||||||
"Chart config": "차트 구성",
|
"Chart config": "차트 구성",
|
||||||
"Templates": "템플릿",
|
"Templates": "템플릿",
|
||||||
|
"Template": "템플릿",
|
||||||
"Select template": "템플릿 선택",
|
"Select template": "템플릿 선택",
|
||||||
"Action logs": "작업 로그",
|
"Action logs": "작업 로그",
|
||||||
"Create template": "템플릿 생성",
|
"Create template": "템플릿 생성",
|
||||||
@ -910,5 +911,7 @@
|
|||||||
"Are you sure you want to hide this menu?": "이 메뉴를 숨기시겠습니까?",
|
"Are you sure you want to hide this menu?": "이 메뉴를 숨기시겠습니까?",
|
||||||
"After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.": "숨기면 이 메뉴는 메뉴 바에 더 이상 표시되지 않습니다. 다시 표시하려면 라우트 관리 페이지에서 설정해야 합니다.",
|
"After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.": "숨기면 이 메뉴는 메뉴 바에 더 이상 표시되지 않습니다. 다시 표시하려면 라우트 관리 페이지에서 설정해야 합니다.",
|
||||||
"If selected, the page will display Tab pages.": "선택되면 페이지는 탭 페이지를 표시합니다.",
|
"If selected, the page will display Tab pages.": "선택되면 페이지는 탭 페이지를 표시합니다.",
|
||||||
"If selected, the route will be displayed in the menu.": "선택되면 라우트는 메뉴에 표시됩니다."
|
"If selected, the route will be displayed in the menu.": "선택되면 라우트는 메뉴에 표시됩니다.",
|
||||||
|
"Deprecated": "사용 중단됨",
|
||||||
|
"The following old template features have been deprecated and will be removed in next version.": "다음 오래된 템플릿 기능은 사용 중단되었으며 다음 버전에서 제거될 것입니다."
|
||||||
}
|
}
|
||||||
|
@ -112,6 +112,7 @@
|
|||||||
"Chart type": "Tipo de gráfico",
|
"Chart type": "Tipo de gráfico",
|
||||||
"Chart config": "Configuração do gráfico",
|
"Chart config": "Configuração do gráfico",
|
||||||
"Templates": "Modelos",
|
"Templates": "Modelos",
|
||||||
|
"Template": "Modelo",
|
||||||
"Select template": "Selecione um modelo",
|
"Select template": "Selecione um modelo",
|
||||||
"Action logs": "Registros de ação",
|
"Action logs": "Registros de ação",
|
||||||
"Create template": "Criar modelo",
|
"Create template": "Criar modelo",
|
||||||
@ -776,5 +777,7 @@
|
|||||||
"Are you sure you want to hide this menu?": "Tem certeza de que deseja ocultar este menu?",
|
"Are you sure you want to hide this menu?": "Tem certeza de que deseja ocultar este menu?",
|
||||||
"After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.": "Depois de ocultar, este menu não aparecerá mais na barra de menus. Para mostrar novamente, você precisa ir à página de gerenciamento de rotas para configurá-lo.",
|
"After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.": "Depois de ocultar, este menu não aparecerá mais na barra de menus. Para mostrar novamente, você precisa ir à página de gerenciamento de rotas para configurá-lo.",
|
||||||
"If selected, the page will display Tab pages.": "Se selecionado, a página exibirá páginas de abas.",
|
"If selected, the page will display Tab pages.": "Se selecionado, a página exibirá páginas de abas.",
|
||||||
"If selected, the route will be displayed in the menu.": "Se selecionado, a rota será exibida no menu."
|
"If selected, the route will be displayed in the menu.": "Se selecionado, a rota será exibida no menu.",
|
||||||
|
"Deprecated": "Descontinuado",
|
||||||
|
"The following old template features have been deprecated and will be removed in next version.": "As seguintes funcionalidades de modelo antigo foram descontinuadas e serão removidas na próxima versão."
|
||||||
}
|
}
|
||||||
|
@ -102,6 +102,7 @@
|
|||||||
"Chart type": "Тип диаграммы",
|
"Chart type": "Тип диаграммы",
|
||||||
"Chart config": "Конфиг. диаграммы",
|
"Chart config": "Конфиг. диаграммы",
|
||||||
"Templates": "Шаблоны",
|
"Templates": "Шаблоны",
|
||||||
|
"Template": "Шаблон",
|
||||||
"Select template": "Выбрать шаблон",
|
"Select template": "Выбрать шаблон",
|
||||||
"Action logs": "Журналы действий",
|
"Action logs": "Журналы действий",
|
||||||
"Create template": "Создать шаблон",
|
"Create template": "Создать шаблон",
|
||||||
@ -605,5 +606,7 @@
|
|||||||
"Are you sure you want to hide this menu?": "Вы уверены, что хотите скрыть это меню?",
|
"Are you sure you want to hide this menu?": "Вы уверены, что хотите скрыть это меню?",
|
||||||
"After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.": "После скрытия этого меню он больше не будет отображаться в меню. Чтобы снова отобразить его, вам нужно будет перейти на страницу управления маршрутами и настроить его.",
|
"After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.": "После скрытия этого меню он больше не будет отображаться в меню. Чтобы снова отобразить его, вам нужно будет перейти на страницу управления маршрутами и настроить его.",
|
||||||
"If selected, the page will display Tab pages.": "Если выбран, страница будет отображать страницы с вкладками.",
|
"If selected, the page will display Tab pages.": "Если выбран, страница будет отображать страницы с вкладками.",
|
||||||
"If selected, the route will be displayed in the menu.": "Если выбран, маршрут будет отображаться в меню."
|
"If selected, the route will be displayed in the menu.": "Если выбран, маршрут будет отображаться в меню.",
|
||||||
|
"Deprecated": "Устаревший",
|
||||||
|
"The following old template features have been deprecated and will be removed in next version.": "Следующие старые функции шаблонов устарели и будут удалены в следующей версии."
|
||||||
}
|
}
|
||||||
|
@ -102,6 +102,7 @@
|
|||||||
"Chart type": "Grafik türü",
|
"Chart type": "Grafik türü",
|
||||||
"Chart config": "Grafik yapılandırması",
|
"Chart config": "Grafik yapılandırması",
|
||||||
"Templates": "Şablonlar",
|
"Templates": "Şablonlar",
|
||||||
|
"Template": "Şablon",
|
||||||
"Select template": "Şablon seç",
|
"Select template": "Şablon seç",
|
||||||
"Action logs": "Eylem günlükleri",
|
"Action logs": "Eylem günlükleri",
|
||||||
"Create template": "Şablon oluştur",
|
"Create template": "Şablon oluştur",
|
||||||
@ -603,5 +604,7 @@
|
|||||||
"Are you sure you want to hide this menu?": "Bu menüyü gizlemek istediğinizden emin misiniz?",
|
"Are you sure you want to hide this menu?": "Bu menüyü gizlemek istediğinizden emin misiniz?",
|
||||||
"After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.": "Gizlendikten sonra, bu menü artık menü çubuğunda görünmeyecektir. Tekrar görüntülemek için, yönlendirme yönetimi sayfasına gidip onu yapılandırmanız gerekecektir.",
|
"After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.": "Gizlendikten sonra, bu menü artık menü çubuğunda görünmeyecektir. Tekrar görüntülemek için, yönlendirme yönetimi sayfasına gidip onu yapılandırmanız gerekecektir.",
|
||||||
"If selected, the page will display Tab pages.": "Seçildiğinde, sayfa Tab sayfalarını görüntüleyecektir.",
|
"If selected, the page will display Tab pages.": "Seçildiğinde, sayfa Tab sayfalarını görüntüleyecektir.",
|
||||||
"If selected, the route will be displayed in the menu.": "Seçildiğinde, yol menüde görüntülenecektir."
|
"If selected, the route will be displayed in the menu.": "Seçildiğinde, yol menüde görüntülenecektir.",
|
||||||
|
"Deprecated": "Kullanımdan kaldırıldı",
|
||||||
|
"The following old template features have been deprecated and will be removed in next version.": "Aşağıdaki eski şablon özellikleri kullanımdan kaldırıldı ve gelecek sürümlerde kaldırılacaktır."
|
||||||
}
|
}
|
||||||
|
@ -159,6 +159,7 @@
|
|||||||
"Chart type": "Тип діаграми",
|
"Chart type": "Тип діаграми",
|
||||||
"Chart config": "Налаштування діаграми",
|
"Chart config": "Налаштування діаграми",
|
||||||
"Templates": "Шаблони",
|
"Templates": "Шаблони",
|
||||||
|
"Template": "Шаблон",
|
||||||
"Select template": "Вибрати шаблон",
|
"Select template": "Вибрати шаблон",
|
||||||
"Action logs": "Журнал дій",
|
"Action logs": "Журнал дій",
|
||||||
"Create template": "Створити шаблон",
|
"Create template": "Створити шаблон",
|
||||||
@ -819,5 +820,7 @@
|
|||||||
"Are you sure you want to hide this menu?": "Ви впевнені, що хочете приховати це меню?",
|
"Are you sure you want to hide this menu?": "Ви впевнені, що хочете приховати це меню?",
|
||||||
"After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.": "Після приховування цього меню він більше не з'явиться в меню. Щоб знову показати його, вам потрібно перейти на сторінку керування маршрутами і налаштувати його.",
|
"After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.": "Після приховування цього меню він більше не з'явиться в меню. Щоб знову показати його, вам потрібно перейти на сторінку керування маршрутами і налаштувати його.",
|
||||||
"If selected, the page will display Tab pages.": "Якщо вибрано, сторінка відобразить сторінки з вкладками.",
|
"If selected, the page will display Tab pages.": "Якщо вибрано, сторінка відобразить сторінки з вкладками.",
|
||||||
"If selected, the route will be displayed in the menu.": "Якщо вибрано, маршрут буде відображений в меню."
|
"If selected, the route will be displayed in the menu.": "Якщо вибрано, маршрут буде відображений в меню.",
|
||||||
|
"Deprecated": "Застаріло",
|
||||||
|
"The following old template features have been deprecated and will be removed in next version.": "Наступні старі функції шаблонів були застарілі і будуть видалені в наступній версії."
|
||||||
}
|
}
|
||||||
|
@ -183,6 +183,7 @@
|
|||||||
"Chart type": "图表类型",
|
"Chart type": "图表类型",
|
||||||
"Chart config": "图表配置",
|
"Chart config": "图表配置",
|
||||||
"Templates": "模板",
|
"Templates": "模板",
|
||||||
|
"Template": "模板",
|
||||||
"Select template": "选择模板",
|
"Select template": "选择模板",
|
||||||
"Action logs": "操作日志",
|
"Action logs": "操作日志",
|
||||||
"Create template": "创建模板",
|
"Create template": "创建模板",
|
||||||
@ -1078,5 +1079,7 @@
|
|||||||
"Are you sure you want to hide this menu?": "你确定要隐藏这个菜单吗?",
|
"Are you sure you want to hide this menu?": "你确定要隐藏这个菜单吗?",
|
||||||
"After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.": "隐藏后,这个菜单将不再出现在菜单栏中。要再次显示它,你需要到路由管理页面进行设置。",
|
"After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.": "隐藏后,这个菜单将不再出现在菜单栏中。要再次显示它,你需要到路由管理页面进行设置。",
|
||||||
"If selected, the page will display Tab pages.": "如果选中,该页面将显示标签页。",
|
"If selected, the page will display Tab pages.": "如果选中,该页面将显示标签页。",
|
||||||
"If selected, the route will be displayed in the menu.": "如果选中,该路由将显示在菜单中。"
|
"If selected, the route will be displayed in the menu.": "如果选中,该路由将显示在菜单中。",
|
||||||
|
"Deprecated": "已弃用",
|
||||||
|
"The following old templates have been deprecated and will be removed in next version.": "以下旧的模板功能已弃用,将在下个版本移除。"
|
||||||
}
|
}
|
||||||
|
@ -183,6 +183,7 @@
|
|||||||
"Chart type": "圖表型別",
|
"Chart type": "圖表型別",
|
||||||
"Chart config": "圖表設定",
|
"Chart config": "圖表設定",
|
||||||
"Templates": "模板",
|
"Templates": "模板",
|
||||||
|
"Template": "模板",
|
||||||
"Select template": "選擇模板",
|
"Select template": "選擇模板",
|
||||||
"Action logs": "動作日誌",
|
"Action logs": "動作日誌",
|
||||||
"Create template": "建立模板",
|
"Create template": "建立模板",
|
||||||
@ -910,6 +911,8 @@
|
|||||||
"Are you sure you want to hide this menu?": "你確定要隱藏這個菜單嗎?",
|
"Are you sure you want to hide this menu?": "你確定要隱藏這個菜單嗎?",
|
||||||
"After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.": "隱藏後,這個菜單將不再出現在菜單欄中。要再次顯示它,你需要到路由管理頁面進行設置。",
|
"After hiding, this menu will no longer appear in the menu bar. To show it again, you need to go to the route management page to configure it.": "隱藏後,這個菜單將不再出現在菜單欄中。要再次顯示它,你需要到路由管理頁面進行設置。",
|
||||||
"If selected, the page will display Tab pages.": "如果選中,該頁面將顯示標籤頁。",
|
"If selected, the page will display Tab pages.": "如果選中,該頁面將顯示標籤頁。",
|
||||||
"If selected, the route will be displayed in the menu.": "如果選中,該路由將顯示在菜單中。"
|
"If selected, the route will be displayed in the menu.": "如果選中,該路由將顯示在菜單中。",
|
||||||
|
"Deprecated": "已棄用",
|
||||||
|
"The following old template features have been deprecated and will be removed in next version.": "以下舊的模板功能已棄用,將在下個版本移除。"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ import {
|
|||||||
} from './templates';
|
} from './templates';
|
||||||
|
|
||||||
test.describe('Submit: should refresh data after submit', () => {
|
test.describe('Submit: should refresh data after submit', () => {
|
||||||
test('submit in reference template block', async ({ page, mockPage, clearBlockTemplates, mockRecord }) => {
|
test.skip('submit in reference template block', async ({ page, mockPage, clearBlockTemplates, mockRecord }) => {
|
||||||
const nocoPage = await mockPage(submitInReferenceTemplateBlock).waitForInit();
|
const nocoPage = await mockPage(submitInReferenceTemplateBlock).waitForInit();
|
||||||
await mockRecord('collection', { nickname: 'abc' });
|
await mockRecord('collection', { nickname: 'abc' });
|
||||||
await nocoPage.goto();
|
await nocoPage.goto();
|
||||||
|
@ -33,7 +33,7 @@ test.describe('multi data details block schema settings', () => {
|
|||||||
'Linkage rules',
|
'Linkage rules',
|
||||||
'Set the data scope',
|
'Set the data scope',
|
||||||
'Set default sorting rules',
|
'Set default sorting rules',
|
||||||
'Save as template',
|
// 'Save as template',
|
||||||
'Delete',
|
'Delete',
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
@ -26,7 +26,7 @@ test.describe('where single data details block can be added', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// https://nocobase.height.app/T-3848/description
|
// https://nocobase.height.app/T-3848/description
|
||||||
test('popup opened by clicking on the button for the relationship field', async ({
|
test.skip('popup opened by clicking on the button for the relationship field', async ({
|
||||||
page,
|
page,
|
||||||
mockPage,
|
mockPage,
|
||||||
mockRecord,
|
mockRecord,
|
||||||
|
@ -24,7 +24,7 @@ test.describe('single details block schema settings', () => {
|
|||||||
await page.getByLabel('block-item-CardItem-general-form').hover();
|
await page.getByLabel('block-item-CardItem-general-form').hover();
|
||||||
await page.getByLabel('designer-schema-settings-CardItem-FormV2.ReadPrettyDesigner-general').hover();
|
await page.getByLabel('designer-schema-settings-CardItem-FormV2.ReadPrettyDesigner-general').hover();
|
||||||
},
|
},
|
||||||
supportedOptions: ['Edit block title', 'Linkage rules', 'Save as block template', 'Delete'],
|
supportedOptions: ['Edit block title', 'Linkage rules', 'Delete'],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -51,7 +51,8 @@ test.describe('creation form block schema settings', () => {
|
|||||||
await runExpect();
|
await runExpect();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Save as block template & convert reference to duplicate', async ({ page, mockPage }) => {
|
// deprecated
|
||||||
|
test.skip('Save as block template & convert reference to duplicate', async ({ page, mockPage }) => {
|
||||||
await mockPage(oneTableBlockWithActionsAndFormBlocks).goto();
|
await mockPage(oneTableBlockWithActionsAndFormBlocks).goto();
|
||||||
await page.getByRole('button', { name: 'Add new' }).click();
|
await page.getByRole('button', { name: 'Add new' }).click();
|
||||||
|
|
||||||
@ -115,7 +116,7 @@ test.describe('creation form block schema settings', () => {
|
|||||||
await expect(page.getByLabel('block-item-CardItem-general-form')).not.toBeVisible();
|
await expect(page.getByLabel('block-item-CardItem-general-form')).not.toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('save as block Template', async ({ page, mockPage }) => {
|
test.skip('save as block Template', async ({ page, mockPage }) => {
|
||||||
await mockPage(oneEmptyForm).goto();
|
await mockPage(oneEmptyForm).goto();
|
||||||
|
|
||||||
// 先保存为模板 ------------------------------------------------------------------------
|
// 先保存为模板 ------------------------------------------------------------------------
|
||||||
@ -270,7 +271,7 @@ test.describe('creation form block schema settings', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('save block template & using block template', async ({ page, mockPage, clearBlockTemplates }) => {
|
test.skip('save block template & using block template', async ({ page, mockPage, clearBlockTemplates }) => {
|
||||||
// 确保测试结束后已保存的模板会被清空
|
// 确保测试结束后已保存的模板会被清空
|
||||||
await clearBlockTemplates();
|
await clearBlockTemplates();
|
||||||
const nocoPage = await mockPage({
|
const nocoPage = await mockPage({
|
||||||
|
@ -236,7 +236,7 @@ test.describe('linkage rules', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// https://nocobase.height.app/T-3806
|
// https://nocobase.height.app/T-3806
|
||||||
test('after save as block template', async ({ page, mockPage }) => {
|
test.skip('after save as block template', async ({ page, mockPage }) => {
|
||||||
await mockPage(T3806).goto();
|
await mockPage(T3806).goto();
|
||||||
|
|
||||||
// 1. 一开始联动规则应该正常
|
// 1. 一开始联动规则应该正常
|
||||||
|
@ -26,7 +26,7 @@ test.describe('where edit form block can be added', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// https://nocobase.height.app/T-3848/description
|
// https://nocobase.height.app/T-3848/description
|
||||||
test('popup opened by clicking on the button for the relationship field', async ({
|
test.skip('popup opened by clicking on the button for the relationship field', async ({
|
||||||
page,
|
page,
|
||||||
mockPage,
|
mockPage,
|
||||||
mockRecord,
|
mockRecord,
|
||||||
|
@ -84,7 +84,7 @@ test.describe('edit form block schema settings', () => {
|
|||||||
await runExpect();
|
await runExpect();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Convert reference to duplicate & Save as block template', async ({ page, mockPage, mockRecord }) => {
|
test.skip('Convert reference to duplicate & Save as block template', async ({ page, mockPage, mockRecord }) => {
|
||||||
const nocoPage = await mockPage(oneTableBlockWithActionsAndFormBlocks).waitForInit();
|
const nocoPage = await mockPage(oneTableBlockWithActionsAndFormBlocks).waitForInit();
|
||||||
await mockRecord('general');
|
await mockRecord('general');
|
||||||
await nocoPage.goto();
|
await nocoPage.goto();
|
||||||
|
@ -12,9 +12,8 @@ import { useDetailsParentRecord } from '../../details-single/hooks/useDetailsDec
|
|||||||
import { useHiddenForInherit } from './useHiddenForInherit';
|
import { useHiddenForInherit } from './useHiddenForInherit';
|
||||||
|
|
||||||
export function useEditFormBlockDecoratorProps(props) {
|
export function useEditFormBlockDecoratorProps(props) {
|
||||||
const params = useFormBlockParams();
|
const params = useFormBlockParams(props);
|
||||||
let parentRecord;
|
let parentRecord;
|
||||||
|
|
||||||
const { hidden } = useHiddenForInherit(props);
|
const { hidden } = useHiddenForInherit(props);
|
||||||
|
|
||||||
// association 的值是固定不变的,所以这里可以使用 hooks
|
// association 的值是固定不变的,所以这里可以使用 hooks
|
||||||
@ -31,6 +30,6 @@ export function useEditFormBlockDecoratorProps(props) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function useFormBlockParams() {
|
function useFormBlockParams(props) {
|
||||||
return useParamsFromRecord();
|
return useParamsFromRecord(props);
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,7 @@ test.describe('grid card block schema settings', () => {
|
|||||||
'Set the data scope',
|
'Set the data scope',
|
||||||
'Set default sorting rules',
|
'Set default sorting rules',
|
||||||
'Records per page',
|
'Records per page',
|
||||||
'Save as template',
|
// 'Save as template',
|
||||||
'Delete',
|
'Delete',
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
@ -24,7 +24,7 @@ test.describe('list block schema settings', () => {
|
|||||||
'Set the data scope',
|
'Set the data scope',
|
||||||
'Set default sorting rules',
|
'Set default sorting rules',
|
||||||
'Records per page',
|
'Records per page',
|
||||||
'Save as template',
|
// 'Save as template',
|
||||||
'Delete',
|
'Delete',
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
import { expect, test } from '@nocobase/test/e2e';
|
import { expect, test } from '@nocobase/test/e2e';
|
||||||
import { ordinaryBlockTemplatesCannotBeUsedToCreateAssociationBlocksAndViceVersa } from './templatesOfBug';
|
import { ordinaryBlockTemplatesCannotBeUsedToCreateAssociationBlocksAndViceVersa } from './templatesOfBug';
|
||||||
|
|
||||||
test.describe('block template', () => {
|
test.skip('block template', () => {
|
||||||
test('Ordinary block templates cannot be used to create association blocks, and vice versa', async ({
|
test('Ordinary block templates cannot be used to create association blocks, and vice versa', async ({
|
||||||
page,
|
page,
|
||||||
mockPage,
|
mockPage,
|
||||||
|
@ -34,7 +34,7 @@ test.describe('table block schema settings', () => {
|
|||||||
'Set the data scope',
|
'Set the data scope',
|
||||||
'Records per page',
|
'Records per page',
|
||||||
'Connect data blocks',
|
'Connect data blocks',
|
||||||
'Save as template',
|
// 'Save as template',
|
||||||
'Delete',
|
'Delete',
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
@ -9,8 +9,8 @@
|
|||||||
|
|
||||||
import { expect, test } from '@nocobase/test/e2e';
|
import { expect, test } from '@nocobase/test/e2e';
|
||||||
|
|
||||||
test.describe('save as template', () => {
|
test.skip('save as template', () => {
|
||||||
test('save as template, then delete it', async ({ page, mockPage, clearBlockTemplates }) => {
|
test.skip('save as template, then delete it', async ({ page, mockPage, clearBlockTemplates }) => {
|
||||||
// 1. 创建一个区块,然后保存为模板
|
// 1. 创建一个区块,然后保存为模板
|
||||||
await mockPage().goto();
|
await mockPage().goto();
|
||||||
await page.getByLabel('schema-initializer-Grid-page:').hover();
|
await page.getByLabel('schema-initializer-Grid-page:').hover();
|
||||||
|
@ -25,7 +25,7 @@ test.describe('tree table block schema settings', () => {
|
|||||||
'Set default sorting rules',
|
'Set default sorting rules',
|
||||||
'Records per page',
|
'Records per page',
|
||||||
'Connect data blocks',
|
'Connect data blocks',
|
||||||
'Save as template',
|
// 'Save as template',
|
||||||
'Delete',
|
'Delete',
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
@ -25,7 +25,7 @@ test.describe('collapse schema settings', () => {
|
|||||||
await page.getByLabel('block-item-CardItem-general-filter-collapse').hover();
|
await page.getByLabel('block-item-CardItem-general-filter-collapse').hover();
|
||||||
await page.getByLabel('designer-schema-settings-CardItem-AssociationFilter.BlockDesigner-general').hover();
|
await page.getByLabel('designer-schema-settings-CardItem-AssociationFilter.BlockDesigner-general').hover();
|
||||||
},
|
},
|
||||||
supportedOptions: ['Edit block title', 'Save as template', 'Connect data blocks', 'Delete'],
|
supportedOptions: ['Edit block title', 'Connect data blocks', 'Delete'],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@ test.describe('filter block schema settings', () => {
|
|||||||
},
|
},
|
||||||
supportedOptions: [
|
supportedOptions: [
|
||||||
'Edit block title',
|
'Edit block title',
|
||||||
'Save as block template',
|
// 'Save as block template',
|
||||||
'Linkage rules',
|
'Linkage rules',
|
||||||
'Connect data blocks',
|
'Connect data blocks',
|
||||||
'Delete',
|
'Delete',
|
||||||
@ -37,7 +37,7 @@ test.describe('filter block schema settings', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test.describe('connect data blocks', () => {
|
test.describe('connect data blocks', () => {
|
||||||
test('connecting two blocks of the same collection', async ({
|
test.skip('connecting two blocks of the same collection', async ({
|
||||||
page,
|
page,
|
||||||
mockPage,
|
mockPage,
|
||||||
mockRecords,
|
mockRecords,
|
||||||
|
@ -35,12 +35,14 @@ export class PMPlugin extends Plugin {
|
|||||||
// Component: ACLPane,
|
// Component: ACLPane,
|
||||||
// aclSnippet: 'pm.acl.roles',
|
// aclSnippet: 'pm.acl.roles',
|
||||||
// });
|
// });
|
||||||
this.app.pluginSettingsManager.add('ui-schema-storage', {
|
|
||||||
title: '{{t("Block templates")}}',
|
// Replaced by plugin-block-template
|
||||||
icon: 'LayoutOutlined',
|
// this.app.pluginSettingsManager.add('ui-schema-storage', {
|
||||||
Component: BlockTemplatesPane,
|
// title: '{{t("Block templates")}}',
|
||||||
aclSnippet: 'pm.ui-schema-storage.block-templates',
|
// icon: 'LayoutOutlined',
|
||||||
});
|
// Component: BlockTemplatesPane,
|
||||||
|
// aclSnippet: 'pm.ui-schema-storage.block-templates',
|
||||||
|
// });
|
||||||
this.app.pluginSettingsManager.add('system-settings', {
|
this.app.pluginSettingsManager.add('system-settings', {
|
||||||
icon: 'SettingOutlined',
|
icon: 'SettingOutlined',
|
||||||
title: '{{t("System settings")}}',
|
title: '{{t("System settings")}}',
|
||||||
|
@ -167,6 +167,13 @@ export function AssignedFieldValues() {
|
|||||||
'x-component': 'Grid',
|
'x-component': 'Grid',
|
||||||
'x-initializer': 'assignFieldValuesForm:configureFields',
|
'x-initializer': 'assignFieldValuesForm:configureFields',
|
||||||
};
|
};
|
||||||
|
if (fieldSchema['x-template-uid']) {
|
||||||
|
initialSchema['x-template-root-ref'] = {
|
||||||
|
'x-template-uid': fieldSchema['x-template-uid'],
|
||||||
|
'x-path': 'x-action-settings.schemaUid',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const tips = {
|
const tips = {
|
||||||
'customize:update': t(
|
'customize:update': t(
|
||||||
'After clicking the custom button, the following fields of the current record will be saved according to the following form.',
|
'After clicking the custom button, the following fields of the current record will be saved according to the following form.',
|
||||||
|
@ -34,6 +34,9 @@ const useStyles = genStyleHook('nb-action', (token) => {
|
|||||||
background: 'var(--colorBgSettingsHover)',
|
background: 'var(--colorBgSettingsHover)',
|
||||||
border: '0',
|
border: '0',
|
||||||
pointerEvents: 'none',
|
pointerEvents: 'none',
|
||||||
|
'&.nb-in-template': {
|
||||||
|
background: 'var(--colorTemplateBgSettingsHover)',
|
||||||
|
},
|
||||||
'> .general-schema-designer-icons': {
|
'> .general-schema-designer-icons': {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
right: '2px',
|
right: '2px',
|
||||||
|
@ -13,7 +13,14 @@ import _ from 'lodash';
|
|||||||
import React, { FC, Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import React, { FC, Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useDesignable, usePopupSettings } from '../../';
|
import { useDesignable, usePopupSettings } from '../../';
|
||||||
import { WithoutTableFieldResource } from '../../../block-provider';
|
import { WithoutTableFieldResource } from '../../../block-provider';
|
||||||
import { CollectionRecordProvider, useCollectionManager, useCollectionRecordData } from '../../../data-source';
|
import {
|
||||||
|
CollectionRecordProvider,
|
||||||
|
DataBlockProvider,
|
||||||
|
useAssociationName,
|
||||||
|
useCollection,
|
||||||
|
useCollectionManager,
|
||||||
|
useCollectionRecordData,
|
||||||
|
} from '../../../data-source';
|
||||||
import { NocoBaseRecursionField } from '../../../formily/NocoBaseRecursionField';
|
import { NocoBaseRecursionField } from '../../../formily/NocoBaseRecursionField';
|
||||||
import { useOpenModeContext } from '../../../modules/popup/OpenModeProvider';
|
import { useOpenModeContext } from '../../../modules/popup/OpenModeProvider';
|
||||||
import { VariablePopupRecordProvider } from '../../../modules/variable/variablesProvider/VariablePopupRecordProvider';
|
import { VariablePopupRecordProvider } from '../../../modules/variable/variablesProvider/VariablePopupRecordProvider';
|
||||||
@ -278,12 +285,14 @@ export const ReadPrettyInternalViewer: React.FC<ReadPrettyInternalViewerProps> =
|
|||||||
const field = useField();
|
const field = useField();
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
const { options: collectionField } = useAssociationFieldContext();
|
const { options: collectionField } = useAssociationFieldContext();
|
||||||
|
const associationName = useAssociationName();
|
||||||
const { visibleWithURL, setVisibleWithURL } = usePopupUtils();
|
const { visibleWithURL, setVisibleWithURL } = usePopupUtils();
|
||||||
const [btnHover, setBtnHover] = useState(!!visibleWithURL);
|
const [btnHover, setBtnHover] = useState(!!visibleWithURL);
|
||||||
const { defaultOpenMode } = useOpenModeContext();
|
const { defaultOpenMode } = useOpenModeContext();
|
||||||
const parentRecordData = useCollectionRecordData();
|
const parentRecordData = useCollectionRecordData();
|
||||||
const [recordData, setRecordData] = useState(null);
|
const [recordData, setRecordData] = useState(null);
|
||||||
const { isPopupVisibleControlledByURL } = usePopupSettings();
|
const { isPopupVisibleControlledByURL } = usePopupSettings();
|
||||||
|
const collection = useCollection();
|
||||||
|
|
||||||
const onClickItem = useCallback((props: { recordData: any }) => {
|
const onClickItem = useCallback((props: { recordData: any }) => {
|
||||||
setRecordData(props.recordData);
|
setRecordData(props.recordData);
|
||||||
@ -329,6 +338,15 @@ export const ReadPrettyInternalViewer: React.FC<ReadPrettyInternalViewerProps> =
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<DataBlockProvider
|
||||||
|
dataSource={collection.dataSource}
|
||||||
|
collection={collection.name}
|
||||||
|
association={associationName}
|
||||||
|
sourceId={recordData?.[collection.getPrimaryKey()]}
|
||||||
|
record={recordData}
|
||||||
|
parentRecord={parentRecordData}
|
||||||
|
action="get"
|
||||||
|
>
|
||||||
<CollectionRecordProvider isNew={false} record={recordData} parentRecord={parentRecordData}>
|
<CollectionRecordProvider isNew={false} record={recordData} parentRecord={parentRecordData}>
|
||||||
{/* The recordData here is only provided when the popup is opened, not the current row record */}
|
{/* The recordData here is only provided when the popup is opened, not the current row record */}
|
||||||
<VariablePopupRecordProvider>
|
<VariablePopupRecordProvider>
|
||||||
@ -337,6 +355,7 @@ export const ReadPrettyInternalViewer: React.FC<ReadPrettyInternalViewerProps> =
|
|||||||
</WithoutTableFieldResource.Provider>
|
</WithoutTableFieldResource.Provider>
|
||||||
</VariablePopupRecordProvider>
|
</VariablePopupRecordProvider>
|
||||||
</CollectionRecordProvider>
|
</CollectionRecordProvider>
|
||||||
|
</DataBlockProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -255,7 +255,7 @@ const ToManyNester = observer(
|
|||||||
return (
|
return (
|
||||||
<React.Fragment key={index}>
|
<React.Fragment key={index}>
|
||||||
<div style={{ textAlign: 'right' }}>
|
<div style={{ textAlign: 'right' }}>
|
||||||
{!field.readPretty && allowed && (
|
{!field.readPretty && allowed && (!fieldSchema['x-template-uid'] || index > 0) && (
|
||||||
<Tooltip key={'remove'} title={t('Remove')}>
|
<Tooltip key={'remove'} title={t('Remove')}>
|
||||||
<CloseOutlined
|
<CloseOutlined
|
||||||
style={{ zIndex: 1000, color: '#a8a3a3' }}
|
style={{ zIndex: 1000, color: '#a8a3a3' }}
|
||||||
|
@ -45,16 +45,16 @@ describe('CollectionSelect', () => {
|
|||||||
expect(container).toMatchInlineSnapshot(`
|
expect(container).toMatchInlineSnapshot(`
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="css-dev-only-do-not-override-11aiz3o ant-app"
|
class="css-dev-only-do-not-override-1gopndx ant-app"
|
||||||
style="height: 100%;"
|
style="height: 100%;"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
aria-label="block-item-demo title"
|
aria-label="block-item-demo title"
|
||||||
class="nb-block-item nb-form-item css-1elzyjx ant-nb-block-item css-dev-only-do-not-override-11aiz3o"
|
class="nb-block-item nb-form-item css-1elzyjx ant-nb-block-item css-dev-only-do-not-override-1gopndx"
|
||||||
role="button"
|
role="button"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="css-1yh5po ant-formily-item ant-formily-item-layout-horizontal ant-formily-item-feedback-layout-loose ant-formily-item-label-align-right ant-formily-item-control-align-left css-dev-only-do-not-override-11aiz3o"
|
class="css-1yh5po ant-formily-item ant-formily-item-layout-horizontal ant-formily-item-feedback-layout-loose ant-formily-item-label-align-right ant-formily-item-control-align-left css-dev-only-do-not-override-1gopndx"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ant-formily-item-label"
|
class="ant-formily-item-label"
|
||||||
@ -84,7 +84,7 @@ describe('CollectionSelect', () => {
|
|||||||
class="ant-formily-item-control-content-component"
|
class="ant-formily-item-control-content-component"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ant-select css-dev-only-do-not-override-11aiz3o ant-select-focused ant-select-single ant-select-show-arrow ant-select-show-search"
|
class="ant-select css-dev-only-do-not-override-1gopndx ant-select-focused ant-select-single ant-select-show-arrow ant-select-show-search"
|
||||||
data-testid="select-collection"
|
data-testid="select-collection"
|
||||||
role="button"
|
role="button"
|
||||||
>
|
>
|
||||||
@ -182,16 +182,16 @@ describe('CollectionSelect', () => {
|
|||||||
expect(container).toMatchInlineSnapshot(`
|
expect(container).toMatchInlineSnapshot(`
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="css-dev-only-do-not-override-11aiz3o ant-app"
|
class="css-dev-only-do-not-override-1gopndx ant-app"
|
||||||
style="height: 100%;"
|
style="height: 100%;"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
aria-label="block-item-demo title"
|
aria-label="block-item-demo title"
|
||||||
class="nb-block-item nb-form-item css-1elzyjx ant-nb-block-item css-dev-only-do-not-override-11aiz3o"
|
class="nb-block-item nb-form-item css-1elzyjx ant-nb-block-item css-dev-only-do-not-override-1gopndx"
|
||||||
role="button"
|
role="button"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="css-1yh5po ant-formily-item ant-formily-item-layout-horizontal ant-formily-item-feedback-layout-loose ant-formily-item-label-align-right ant-formily-item-control-align-left css-dev-only-do-not-override-11aiz3o"
|
class="css-1yh5po ant-formily-item ant-formily-item-layout-horizontal ant-formily-item-feedback-layout-loose ant-formily-item-label-align-right ant-formily-item-control-align-left css-dev-only-do-not-override-1gopndx"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ant-formily-item-label"
|
class="ant-formily-item-label"
|
||||||
@ -222,7 +222,7 @@ describe('CollectionSelect', () => {
|
|||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<span
|
<span
|
||||||
class="ant-tag css-dev-only-do-not-override-11aiz3o"
|
class="ant-tag css-dev-only-do-not-override-1gopndx"
|
||||||
>
|
>
|
||||||
Users
|
Users
|
||||||
</span>
|
</span>
|
||||||
|
@ -26,7 +26,7 @@ describe('ColorPicker', () => {
|
|||||||
expect(container).toMatchInlineSnapshot(`
|
expect(container).toMatchInlineSnapshot(`
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="css-dev-only-do-not-override-11aiz3o ant-app"
|
class="css-dev-only-do-not-override-1gopndx ant-app"
|
||||||
style="height: 100%;"
|
style="height: 100%;"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@ -35,7 +35,7 @@ describe('ColorPicker', () => {
|
|||||||
style="display: inline-block;"
|
style="display: inline-block;"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ant-color-picker-trigger css-dev-only-do-not-override-11aiz3o"
|
class="ant-color-picker-trigger css-dev-only-do-not-override-1gopndx"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ant-color-picker-color-block"
|
class="ant-color-picker-color-block"
|
||||||
@ -90,7 +90,7 @@ describe('ColorPicker', () => {
|
|||||||
expect(container).toMatchInlineSnapshot(`
|
expect(container).toMatchInlineSnapshot(`
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="css-dev-only-do-not-override-11aiz3o ant-app"
|
class="css-dev-only-do-not-override-1gopndx ant-app"
|
||||||
style="height: 100%;"
|
style="height: 100%;"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@ -99,7 +99,7 @@ describe('ColorPicker', () => {
|
|||||||
role="button"
|
role="button"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ant-color-picker-trigger ant-color-picker-sm css-dev-only-do-not-override-11aiz3o ant-color-picker-trigger-disabled"
|
class="ant-color-picker-trigger ant-color-picker-sm css-dev-only-do-not-override-1gopndx ant-color-picker-trigger-disabled"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ant-color-picker-color-block"
|
class="ant-color-picker-color-block"
|
||||||
|
@ -215,10 +215,10 @@ describe('form.settings', () => {
|
|||||||
title: 'Form data templates',
|
title: 'Form data templates',
|
||||||
type: 'modal',
|
type: 'modal',
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
title: 'Save as block template',
|
// title: 'Save as block template',
|
||||||
type: 'modal',
|
// type: 'modal',
|
||||||
},
|
// },
|
||||||
{
|
{
|
||||||
title: 'Delete',
|
title: 'Delete',
|
||||||
type: 'delete',
|
type: 'delete',
|
||||||
|
@ -27,13 +27,16 @@ import { SchemaSettingsDataScope } from '../../../schema-settings/SchemaSettings
|
|||||||
import { SchemaSettingsTemplate } from '../../../schema-settings/SchemaSettingsTemplate';
|
import { SchemaSettingsTemplate } from '../../../schema-settings/SchemaSettingsTemplate';
|
||||||
import { useSchemaTemplate } from '../../../schema-templates';
|
import { useSchemaTemplate } from '../../../schema-templates';
|
||||||
import { useBlockTemplateContext } from '../../../schema-templates/BlockTemplateProvider';
|
import { useBlockTemplateContext } from '../../../schema-templates/BlockTemplateProvider';
|
||||||
import { useDesignable } from '../../hooks';
|
import { useCompile, useDesignable } from '../../hooks';
|
||||||
import { removeNullCondition } from '../filter';
|
import { removeNullCondition } from '../filter';
|
||||||
|
import { useApp } from '../../../application';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated - 已使用 SchemaSettings 替代
|
* @deprecated - 已使用 SchemaSettings 替代
|
||||||
*/
|
*/
|
||||||
export const ListDesigner = () => {
|
export const ListDesigner = () => {
|
||||||
|
const app = useApp();
|
||||||
|
const compile = useCompile();
|
||||||
const { name, title } = useCollection_deprecated();
|
const { name, title } = useCollection_deprecated();
|
||||||
const template = useSchemaTemplate();
|
const template = useSchemaTemplate();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -57,8 +60,15 @@ export const ListDesigner = () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
const { componentNamePrefix } = useBlockTemplateContext();
|
const { componentNamePrefix } = useBlockTemplateContext();
|
||||||
|
const RevertSetting =
|
||||||
|
app.schemaSettingsManager.getAll()['blockSettings:list']?.get('template-revertSettingItem')?.['Component'] || null;
|
||||||
|
let designerTitle = title;
|
||||||
|
if (fieldSchema['x-template-uid']) {
|
||||||
|
designerTitle = `${compile(title)} [${t('Template')}: ${compile(fieldSchema['x-template-title'])}]`;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GeneralSchemaDesigner template={template} title={title || name}>
|
<GeneralSchemaDesigner template={template} title={designerTitle || name}>
|
||||||
<SchemaSettingsBlockTitleItem />
|
<SchemaSettingsBlockTitleItem />
|
||||||
<SchemaSettingsDataScope
|
<SchemaSettingsDataScope
|
||||||
collectionName={name}
|
collectionName={name}
|
||||||
@ -188,11 +198,15 @@ export const ListDesigner = () => {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{!fieldSchema['x-template-uid'] ? (
|
||||||
|
<>
|
||||||
|
{!RevertSetting && (
|
||||||
<SchemaSettingsTemplate
|
<SchemaSettingsTemplate
|
||||||
componentName={`${componentNamePrefix}List`}
|
componentName={`${componentNamePrefix}List`}
|
||||||
collectionName={name}
|
collectionName={name}
|
||||||
resourceName={defaultResource}
|
resourceName={defaultResource}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
<SchemaSettingsDivider />
|
<SchemaSettingsDivider />
|
||||||
<SchemaSettingsRemove
|
<SchemaSettingsRemove
|
||||||
removeParentsIfNoChildren
|
removeParentsIfNoChildren
|
||||||
@ -200,6 +214,10 @@ export const ListDesigner = () => {
|
|||||||
'x-component': 'Grid',
|
'x-component': 'Grid',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<RevertSetting />
|
||||||
|
)}
|
||||||
</GeneralSchemaDesigner>
|
</GeneralSchemaDesigner>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -40,8 +40,15 @@ export const usePopupSettings = () => {
|
|||||||
const isOldMobileMode = pathname?.includes('/mobile/') || hash?.includes('/mobile/');
|
const isOldMobileMode = pathname?.includes('/mobile/') || hash?.includes('/mobile/');
|
||||||
const isNewMobileMode = pathname?.includes('/m/');
|
const isNewMobileMode = pathname?.includes('/m/');
|
||||||
const isPCMode = pathname?.includes('/admin/');
|
const isPCMode = pathname?.includes('/admin/');
|
||||||
|
const isMobileTemplateSettingsPage = pathname?.includes('/m/block-templates/');
|
||||||
|
|
||||||
return (isPCMode || isNewMobileMode) && !isOldMobileMode && enableURL && !isInSettingsPage;
|
return (
|
||||||
|
(isPCMode || isNewMobileMode) &&
|
||||||
|
!isOldMobileMode &&
|
||||||
|
enableURL &&
|
||||||
|
!isInSettingsPage &&
|
||||||
|
!isMobileTemplateSettingsPage
|
||||||
|
);
|
||||||
}, [enableURL, isInSettingsPage]);
|
}, [enableURL, isInSettingsPage]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -21,12 +21,12 @@ describe('Pagination', () => {
|
|||||||
expect(container).toMatchInlineSnapshot(`
|
expect(container).toMatchInlineSnapshot(`
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="css-dev-only-do-not-override-11aiz3o ant-app"
|
class="css-dev-only-do-not-override-1gopndx ant-app"
|
||||||
style="height: 100%;"
|
style="height: 100%;"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<ul
|
<ul
|
||||||
class="ant-pagination css-dev-only-do-not-override-11aiz3o"
|
class="ant-pagination css-dev-only-do-not-override-1gopndx"
|
||||||
>
|
>
|
||||||
<li
|
<li
|
||||||
aria-disabled="true"
|
aria-disabled="true"
|
||||||
@ -131,7 +131,7 @@ describe('Pagination', () => {
|
|||||||
expect(container).toMatchInlineSnapshot(`
|
expect(container).toMatchInlineSnapshot(`
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="css-dev-only-do-not-override-11aiz3o ant-app"
|
class="css-dev-only-do-not-override-1gopndx ant-app"
|
||||||
style="height: 100%;"
|
style="height: 100%;"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -62,8 +62,13 @@ export const TabsDesigner = () => {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{/* if it is created by template, do not show remove button */}
|
||||||
|
{fieldSchema['x-template-uid'] ? null : (
|
||||||
|
<>
|
||||||
<SchemaSettingsDivider />
|
<SchemaSettingsDivider />
|
||||||
<SchemaSettingsRemove />
|
<SchemaSettingsRemove />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</GeneralSchemaDesigner>
|
</GeneralSchemaDesigner>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -47,7 +47,7 @@ export const Tabs: any = React.memo((props: TabsProps) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}, [fieldSchema.mapProperties((s, key) => key).join()]);
|
}, [fieldSchema]);
|
||||||
|
|
||||||
const tabBarExtraContent = useMemo(
|
const tabBarExtraContent = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
@ -102,6 +102,9 @@ const designerCss = css`
|
|||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
&.nb-in-template {
|
||||||
|
background: var(--colorTemplateBgSettingsHover);
|
||||||
|
}
|
||||||
> .general-schema-designer-icons {
|
> .general-schema-designer-icons {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 2px;
|
right: 2px;
|
||||||
|
@ -20,11 +20,11 @@ describe('UnixTimestamp', () => {
|
|||||||
expect(container).toMatchInlineSnapshot(`
|
expect(container).toMatchInlineSnapshot(`
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="css-dev-only-do-not-override-11aiz3o ant-app"
|
class="css-dev-only-do-not-override-1gopndx ant-app"
|
||||||
style="height: 100%;"
|
style="height: 100%;"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ant-picker css-dev-only-do-not-override-11aiz3o"
|
class="ant-picker css-dev-only-do-not-override-1gopndx"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="ant-picker-input"
|
class="ant-picker-input"
|
||||||
@ -77,7 +77,7 @@ describe('UnixTimestamp', () => {
|
|||||||
expect(container).toMatchInlineSnapshot(`
|
expect(container).toMatchInlineSnapshot(`
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="css-dev-only-do-not-override-11aiz3o ant-app"
|
class="css-dev-only-do-not-override-1gopndx ant-app"
|
||||||
style="height: 100%;"
|
style="height: 100%;"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* 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 React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { QuestionCircleOutlined } from '@ant-design/icons';
|
||||||
|
import { Space, Tooltip } from 'antd';
|
||||||
|
|
||||||
|
export const DeprecatedTemplateTitle = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<Space>
|
||||||
|
{t('Deprecated')}
|
||||||
|
<Tooltip
|
||||||
|
title={t('The following old template features have been deprecated and will be removed in next versions.')}
|
||||||
|
>
|
||||||
|
<QuestionCircleOutlined />
|
||||||
|
</Tooltip>
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DeprecatedTemplateTitleElement = <DeprecatedTemplateTitle />;
|
@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// hooks/useTemplateBlockNotifier.ts
|
||||||
|
import { useFieldSchema, useForm } from '@formily/react';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
export const useTemplateBlockNotifier = () => {
|
||||||
|
const form = useForm();
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
|
||||||
|
// only when TemplateGridDecorator is used, the blockAdded event will be notified
|
||||||
|
const hasTemplateGridDecorator = useMemo(() => {
|
||||||
|
let current = fieldSchema;
|
||||||
|
while (current) {
|
||||||
|
if (current['x-decorator'] === 'TemplateGridDecorator') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
current = current.parent;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}, [fieldSchema]);
|
||||||
|
|
||||||
|
const notify = (info: { collection: string; dataSource: string; componentType: string; menuName: string }) => {
|
||||||
|
if (hasTemplateGridDecorator) {
|
||||||
|
form.notify('blockAdded', info);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return notify;
|
||||||
|
};
|
@ -149,6 +149,7 @@ export {
|
|||||||
useRecordCollectionDataSourceItems,
|
useRecordCollectionDataSourceItems,
|
||||||
useRemoveGridFormItem,
|
useRemoveGridFormItem,
|
||||||
useTableColumnInitializerFields,
|
useTableColumnInitializerFields,
|
||||||
|
registerInitializerMenusGenerator,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
|
|
||||||
export class SchemaInitializerPlugin extends Plugin {
|
export class SchemaInitializerPlugin extends Plugin {
|
||||||
|
@ -22,6 +22,7 @@ import { Collection, CollectionFieldOptions } from '../../data-source/collection
|
|||||||
import { useCompile } from '../../schema-component';
|
import { useCompile } from '../../schema-component';
|
||||||
import { useSchemaTemplateManager } from '../../schema-templates';
|
import { useSchemaTemplateManager } from '../../schema-templates';
|
||||||
import { useCollectionDataSourceItems } from '../utils';
|
import { useCollectionDataSourceItems } from '../utils';
|
||||||
|
import { useTemplateBlockNotifier } from '../hooks/useTemplateBlockNotifier';
|
||||||
|
|
||||||
const MENU_ITEM_HEIGHT = 32;
|
const MENU_ITEM_HEIGHT = 32;
|
||||||
const STEP = 15;
|
const STEP = 15;
|
||||||
@ -332,8 +333,16 @@ export const DataBlockInitializer: FC<DataBlockInitializerProps> = (props) => {
|
|||||||
const { insert, setVisible } = useSchemaInitializer();
|
const { insert, setVisible } = useSchemaInitializer();
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
const { getTemplateSchemaByMode } = useSchemaTemplateManager();
|
const { getTemplateSchemaByMode } = useSchemaTemplateManager();
|
||||||
|
const templateBlockAddedNotifier = useTemplateBlockNotifier();
|
||||||
|
|
||||||
const onClick = useCallback(
|
const onClick = useCallback(
|
||||||
async (options) => {
|
async (options) => {
|
||||||
|
templateBlockAddedNotifier({
|
||||||
|
collection: options.item.name,
|
||||||
|
dataSource: options.item.dataSource,
|
||||||
|
componentType: componentType,
|
||||||
|
menuName: name,
|
||||||
|
});
|
||||||
const { item, fromOthersInPopup } = options;
|
const { item, fromOthersInPopup } = options;
|
||||||
|
|
||||||
if (propsOnClick) {
|
if (propsOnClick) {
|
||||||
@ -343,6 +352,8 @@ export const DataBlockInitializer: FC<DataBlockInitializerProps> = (props) => {
|
|||||||
if (item.template) {
|
if (item.template) {
|
||||||
const s = await getTemplateSchemaByMode(item);
|
const s = await getTemplateSchemaByMode(item);
|
||||||
templateWrap ? insert(templateWrap(s, { item, fromOthersInPopup })) : insert(s);
|
templateWrap ? insert(templateWrap(s, { item, fromOthersInPopup })) : insert(s);
|
||||||
|
} else if (item.schemaInsertor) {
|
||||||
|
await item.schemaInsertor(insert, { item, name, fromOthersInPopup });
|
||||||
} else {
|
} else {
|
||||||
if (onCreateBlockSchema) {
|
if (onCreateBlockSchema) {
|
||||||
onCreateBlockSchema({ item, fromOthersInPopup });
|
onCreateBlockSchema({ item, fromOthersInPopup });
|
||||||
@ -351,7 +362,17 @@ export const DataBlockInitializer: FC<DataBlockInitializerProps> = (props) => {
|
|||||||
|
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
},
|
},
|
||||||
[getTemplateSchemaByMode, insert, setVisible, onCreateBlockSchema, propsOnClick, templateWrap],
|
[
|
||||||
|
getTemplateSchemaByMode,
|
||||||
|
insert,
|
||||||
|
setVisible,
|
||||||
|
onCreateBlockSchema,
|
||||||
|
propsOnClick,
|
||||||
|
templateWrap,
|
||||||
|
templateBlockAddedNotifier,
|
||||||
|
name,
|
||||||
|
componentType,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
const items =
|
const items =
|
||||||
itemsFromProps ||
|
itemsFromProps ||
|
||||||
|
@ -15,8 +15,12 @@ import { SchemaInitializerSwitch, useSchemaInitializer } from '../../application
|
|||||||
import { useCurrentSchema } from '../utils';
|
import { useCurrentSchema } from '../utils';
|
||||||
|
|
||||||
export const InitializerWithSwitch = (props) => {
|
export const InitializerWithSwitch = (props) => {
|
||||||
const { type, schema, item, remove: passInRemove, disabled } = props;
|
const { type, schema, item, remove: passInRemove, disabled: propsDisabled } = props;
|
||||||
const { exists, remove } = useCurrentSchema(
|
const {
|
||||||
|
exists,
|
||||||
|
remove,
|
||||||
|
schema: currentSchema,
|
||||||
|
} = useCurrentSchema(
|
||||||
schema?.[type] || item?.schema?.[type],
|
schema?.[type] || item?.schema?.[type],
|
||||||
type,
|
type,
|
||||||
item.find,
|
item.find,
|
||||||
@ -25,6 +29,8 @@ export const InitializerWithSwitch = (props) => {
|
|||||||
);
|
);
|
||||||
const { insert } = useSchemaInitializer();
|
const { insert } = useSchemaInitializer();
|
||||||
const update = useUpdate();
|
const update = useUpdate();
|
||||||
|
const isInTemplate = !!currentSchema?.['x-template-uid'];
|
||||||
|
const disabled = propsDisabled || isInTemplate;
|
||||||
return (
|
return (
|
||||||
<SchemaInitializerSwitch
|
<SchemaInitializerSwitch
|
||||||
checked={exists}
|
checked={exists}
|
||||||
|
@ -36,6 +36,7 @@ import { isAssocField } from '../filter-provider/utils';
|
|||||||
import { useActionContext, useCompile, useDesignable } from '../schema-component';
|
import { useActionContext, useCompile, useDesignable } from '../schema-component';
|
||||||
import { useSchemaTemplateManager } from '../schema-templates';
|
import { useSchemaTemplateManager } from '../schema-templates';
|
||||||
import { useBlockTemplateContext } from '../schema-templates/BlockTemplateProvider';
|
import { useBlockTemplateContext } from '../schema-templates/BlockTemplateProvider';
|
||||||
|
import { DeprecatedTemplateTitleElement } from './components/DeprecatedTemplateTitle';
|
||||||
|
|
||||||
export const itemsMerge = (items1) => {
|
export const itemsMerge = (items1) => {
|
||||||
return items1;
|
return items1;
|
||||||
@ -838,21 +839,24 @@ export const useRecordCollectionDataSourceItems = (
|
|||||||
.filter((template) => {
|
.filter((template) => {
|
||||||
return ['FormItem', 'ReadPrettyFormItem'].includes(componentName) || template.resourceName === resourceName;
|
return ['FormItem', 'ReadPrettyFormItem'].includes(componentName) || template.resourceName === resourceName;
|
||||||
});
|
});
|
||||||
if (!templates.length) {
|
const extralCollectionMenuItems = Array.from(initializerMenusGenerators.values())
|
||||||
|
.map((generator) => generator({ collection, componentName }))
|
||||||
|
.filter(Boolean)
|
||||||
|
.flat();
|
||||||
|
if ((!templates.length && !extralCollectionMenuItems.length) || isInTemplateSettingPage()) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const index = 0;
|
const index = 0;
|
||||||
return [
|
const deprecatedTemplatesMenuItems = [];
|
||||||
{
|
if (templates.length) {
|
||||||
key: `${collectionName || componentName}_table_blank`,
|
deprecatedTemplatesMenuItems.push(
|
||||||
type: 'item',
|
|
||||||
name: collection.name,
|
|
||||||
title: t('Blank block'),
|
|
||||||
item,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
type: 'divider',
|
type: 'divider',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'itemGroup',
|
||||||
|
title: DeprecatedTemplateTitleElement,
|
||||||
|
children: [
|
||||||
{
|
{
|
||||||
key: `${collectionName || componentName}_table_subMenu_${index}_copy`,
|
key: `${collectionName || componentName}_table_subMenu_${index}_copy`,
|
||||||
type: 'subMenu',
|
type: 'subMenu',
|
||||||
@ -891,6 +895,20 @@ export const useRecordCollectionDataSourceItems = (
|
|||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: `${collectionName || componentName}_table_blank`,
|
||||||
|
type: 'item',
|
||||||
|
name: collection.name,
|
||||||
|
title: t('Blank block'),
|
||||||
|
item,
|
||||||
|
},
|
||||||
|
...extralCollectionMenuItems,
|
||||||
|
...deprecatedTemplatesMenuItems,
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -937,6 +955,7 @@ export const useCollectionDataSourceItems = ({
|
|||||||
filterCollections: filter,
|
filterCollections: filter,
|
||||||
showAssociationFields,
|
showAssociationFields,
|
||||||
componentNamePrefix,
|
componentNamePrefix,
|
||||||
|
name,
|
||||||
});
|
});
|
||||||
const association = useAssociationName();
|
const association = useAssociationName();
|
||||||
|
|
||||||
@ -1515,7 +1534,13 @@ const getChildren = ({
|
|||||||
|
|
||||||
return componentName && template.componentName === componentName;
|
return componentName && template.componentName === componentName;
|
||||||
});
|
});
|
||||||
if (!templates.length) {
|
const extralCollectionMenuItems = Array.from(initializerMenusGenerators.values())
|
||||||
|
.map((generator) => {
|
||||||
|
return generator({ item, index, componentName, association });
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
.flat();
|
||||||
|
if ((!templates.length && !extralCollectionMenuItems.length) || isInTemplateSettingPage()) {
|
||||||
return {
|
return {
|
||||||
type: 'item',
|
type: 'item',
|
||||||
name: item.name,
|
name: item.name,
|
||||||
@ -1523,22 +1548,16 @@ const getChildren = ({
|
|||||||
dataSource,
|
dataSource,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
const deprecatedTemplatesMenuItems = [];
|
||||||
key: `${componentName}_table_subMenu_${index}`,
|
if (templates.length) {
|
||||||
type: 'subMenu',
|
deprecatedTemplatesMenuItems.push(
|
||||||
name: `${item.name}_${index}`,
|
|
||||||
title,
|
|
||||||
dataSource,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
type: 'item',
|
|
||||||
name: item.name,
|
|
||||||
dataSource,
|
|
||||||
title: t('Blank block'),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
type: 'divider',
|
type: 'divider',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'itemGroup',
|
||||||
|
title: DeprecatedTemplateTitleElement,
|
||||||
|
children: [
|
||||||
{
|
{
|
||||||
key: `${componentName}_table_subMenu_${index}_copy`,
|
key: `${componentName}_table_subMenu_${index}_copy`,
|
||||||
type: 'subMenu',
|
type: 'subMenu',
|
||||||
@ -1586,6 +1605,25 @@ const getChildren = ({
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
key: `${componentName}_table_subMenu_${index}`,
|
||||||
|
type: 'subMenu',
|
||||||
|
name: `${item.name}_${index}`,
|
||||||
|
title,
|
||||||
|
dataSource,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
name: item.name,
|
||||||
|
dataSource,
|
||||||
|
title: t('Blank block'),
|
||||||
|
},
|
||||||
|
...extralCollectionMenuItems,
|
||||||
|
...deprecatedTemplatesMenuItems,
|
||||||
|
],
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -1712,11 +1750,13 @@ function useAssociationFields({
|
|||||||
filterCollections,
|
filterCollections,
|
||||||
showAssociationFields,
|
showAssociationFields,
|
||||||
componentNamePrefix,
|
componentNamePrefix,
|
||||||
|
name,
|
||||||
}: {
|
}: {
|
||||||
componentName: string;
|
componentName: string;
|
||||||
filterCollections: (options: { collection?: Collection; associationField?: CollectionFieldOptions }) => boolean;
|
filterCollections: (options: { collection?: Collection; associationField?: CollectionFieldOptions }) => boolean;
|
||||||
componentNamePrefix: string;
|
componentNamePrefix: string;
|
||||||
showAssociationFields?: boolean;
|
showAssociationFields?: boolean;
|
||||||
|
name: string;
|
||||||
}) {
|
}) {
|
||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
const { getCollectionFields } = useCollectionManager_deprecated();
|
const { getCollectionFields } = useCollectionManager_deprecated();
|
||||||
@ -1764,7 +1804,12 @@ function useAssociationFields({
|
|||||||
|
|
||||||
return template.componentName === componentName;
|
return template.componentName === componentName;
|
||||||
});
|
});
|
||||||
if (!templates.length) {
|
const keyPrefix = `associationFiled_table_subMenu`;
|
||||||
|
const extralCollectionMenuItems = Array.from(initializerMenusGenerators.values())
|
||||||
|
.map((generator) => generator({ collection, index, field, componentName, keyPrefix, name }))
|
||||||
|
.filter(Boolean)
|
||||||
|
.flat();
|
||||||
|
if ((!templates.length && !extralCollectionMenuItems.length) || isInTemplateSettingPage()) {
|
||||||
return {
|
return {
|
||||||
type: 'item',
|
type: 'item',
|
||||||
name: `${field.collectionName}.${field.name}`,
|
name: `${field.collectionName}.${field.name}`,
|
||||||
@ -1774,24 +1819,16 @@ function useAssociationFields({
|
|||||||
associationField: field,
|
associationField: field,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
const deprecatedTemplatesMenuItems = [];
|
||||||
key: `associationFiled_${componentName}_table_subMenu_${index}`,
|
if (templates.length) {
|
||||||
type: 'subMenu',
|
deprecatedTemplatesMenuItems.push(
|
||||||
name: `${field.target}_${index}`,
|
|
||||||
title,
|
|
||||||
dataSource,
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
type: 'item',
|
|
||||||
name: `${field.collectionName}.${field.name}`,
|
|
||||||
collectionName: field.target,
|
|
||||||
dataSource,
|
|
||||||
title: t('Blank block'),
|
|
||||||
associationField: field,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
type: 'divider',
|
type: 'divider',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'itemGroup',
|
||||||
|
title: DeprecatedTemplateTitleElement,
|
||||||
|
children: [
|
||||||
{
|
{
|
||||||
key: `associationFiled_${componentName}_table_subMenu_${index}_copy`,
|
key: `associationFiled_${componentName}_table_subMenu_${index}_copy`,
|
||||||
type: 'subMenu',
|
type: 'subMenu',
|
||||||
@ -1843,6 +1880,27 @@ function useAssociationFields({
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
key: `associationFiled_${componentName}_table_subMenu_${index}`,
|
||||||
|
type: 'subMenu',
|
||||||
|
name: `${field.target}_${index}`,
|
||||||
|
title,
|
||||||
|
dataSource,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
name: `${field.collectionName}.${field.name}`,
|
||||||
|
collectionName: field.target,
|
||||||
|
dataSource,
|
||||||
|
title: t('Blank block'),
|
||||||
|
associationField: field,
|
||||||
|
},
|
||||||
|
...extralCollectionMenuItems,
|
||||||
|
...deprecatedTemplatesMenuItems,
|
||||||
|
],
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}, [
|
}, [
|
||||||
@ -1859,3 +1917,17 @@ function useAssociationFields({
|
|||||||
componentNamePrefix,
|
componentNamePrefix,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isInTemplateSettingPage = () => window.location.pathname.includes('/block-templates/');
|
||||||
|
|
||||||
|
const initializerMenusGenerators = new Map<
|
||||||
|
string,
|
||||||
|
(options: any) => SchemaInitializerItemType | SchemaInitializerItemType[]
|
||||||
|
>();
|
||||||
|
|
||||||
|
export function registerInitializerMenusGenerator(
|
||||||
|
key: string,
|
||||||
|
generator: (options: any) => SchemaInitializerItemType | SchemaInitializerItemType[],
|
||||||
|
) {
|
||||||
|
initializerMenusGenerators.set(key, generator);
|
||||||
|
}
|
||||||
|
@ -140,7 +140,13 @@ export const GeneralSchemaDesigner: FC<GeneralSchemaDesignerProps> = (props: any
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SchemaToolbarProvider {...contextValue}>
|
<SchemaToolbarProvider {...contextValue}>
|
||||||
<div className={classNames('general-schema-designer', overrideAntdCSS)}>
|
<div
|
||||||
|
className={classNames(
|
||||||
|
'general-schema-designer',
|
||||||
|
overrideAntdCSS,
|
||||||
|
fieldSchema['x-template-uid'] ? 'nb-in-template' : '',
|
||||||
|
)}
|
||||||
|
>
|
||||||
{title && (
|
{title && (
|
||||||
<div className={classNames('general-schema-designer-title', titleCss)}>
|
<div className={classNames('general-schema-designer-title', titleCss)}>
|
||||||
<Space size={2}>
|
<Space size={2}>
|
||||||
@ -241,6 +247,7 @@ const InternalSchemaToolbar: FC<SchemaToolbarProps> = React.memo((props) => {
|
|||||||
const dataSourceContext = useDataSource();
|
const dataSourceContext = useDataSource();
|
||||||
const dataSource = dataSources?.length > 1 && dataSourceContext;
|
const dataSource = dataSources?.length > 1 && dataSourceContext;
|
||||||
const refreshFieldSchema = useRefreshFieldSchema();
|
const refreshFieldSchema = useRefreshFieldSchema();
|
||||||
|
const templateTitleLabel = useRef(t('Reference template'));
|
||||||
|
|
||||||
const refresh = useCallback(() => {
|
const refresh = useCallback(() => {
|
||||||
refreshFieldSchema({ refreshParentSchema: true });
|
refreshFieldSchema({ refreshParentSchema: true });
|
||||||
@ -249,8 +256,14 @@ const InternalSchemaToolbar: FC<SchemaToolbarProps> = React.memo((props) => {
|
|||||||
const titleArr = useMemo(() => {
|
const titleArr = useMemo(() => {
|
||||||
if (!title) return undefined;
|
if (!title) return undefined;
|
||||||
if (typeof title === 'string') return [compile(title)];
|
if (typeof title === 'string') return [compile(title)];
|
||||||
if (Array.isArray(title)) return compile(title);
|
if (Array.isArray(title)) {
|
||||||
}, [title]);
|
if (title.length === 1 && fieldSchema['x-template-title']) {
|
||||||
|
templateTitleLabel.current = t('Template');
|
||||||
|
return compile([title[0], fieldSchema['x-template-title']]);
|
||||||
|
}
|
||||||
|
return compile(title);
|
||||||
|
}
|
||||||
|
}, [title, fieldSchema]);
|
||||||
|
|
||||||
const { render: schemaSettingsRender, exists: schemaSettingsExists } = useSchemaSettingsRender(
|
const { render: schemaSettingsRender, exists: schemaSettingsExists } = useSchemaSettingsRender(
|
||||||
settings || fieldSchema?.['x-settings'],
|
settings || fieldSchema?.['x-settings'],
|
||||||
@ -367,7 +380,7 @@ const InternalSchemaToolbar: FC<SchemaToolbarProps> = React.memo((props) => {
|
|||||||
</span>
|
</span>
|
||||||
{titleArr[1] && (
|
{titleArr[1] && (
|
||||||
<span className={'toolbar-title-tag'}>
|
<span className={'toolbar-title-tag'}>
|
||||||
{`${t('Reference template')}: ${`${titleArr[1]}` || t('Untitled')}`}
|
{`${templateTitleLabel.current}: ${`${titleArr[1]}` || t('Untitled')}`}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
|
@ -23,7 +23,9 @@ export const useStyles = genStyleHook('nb-schema-toolbar', (token) => {
|
|||||||
border: '2px solid var(--colorBorderSettingsHover)',
|
border: '2px solid var(--colorBorderSettingsHover)',
|
||||||
background: 'var(--colorBgSettingsHover)',
|
background: 'var(--colorBgSettingsHover)',
|
||||||
pointerEvents: 'none',
|
pointerEvents: 'none',
|
||||||
|
'&.nb-in-template': {
|
||||||
|
background: 'var(--colorTemplateBgSettingsHover)',
|
||||||
|
},
|
||||||
'&.hidden': {
|
'&.hidden': {
|
||||||
// Visually hide the element while keeping it in document flow to prevent reflow/repaint
|
// Visually hide the element while keeping it in document flow to prevent reflow/repaint
|
||||||
transform: 'scale(0)',
|
transform: 'scale(0)',
|
||||||
|
@ -27,8 +27,8 @@ export const CustomRequestActionACLDecorator = (props) => {
|
|||||||
cacheKey: listByCurrentRoleUrl,
|
cacheKey: listByCurrentRoleUrl,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
const requestId = fieldSchema?.['x-custom-request-id'] || fieldSchema?.['x-uid'];
|
||||||
if (!isRoot && !data?.data?.includes(fieldSchema?.['x-uid'])) {
|
if (!isRoot && !data?.data?.includes(requestId)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,6 +56,9 @@ export function CustomRequestSettingsItem() {
|
|||||||
onSubmit={async (config) => {
|
onSubmit={async (config) => {
|
||||||
const { ...requestSettings } = config;
|
const { ...requestSettings } = config;
|
||||||
fieldSchema['x-response-type'] = requestSettings.responseType;
|
fieldSchema['x-response-type'] = requestSettings.responseType;
|
||||||
|
const isSelfRequest =
|
||||||
|
!fieldSchema['x-custom-request-id'] || fieldSchema['x-custom-request-id'] === fieldSchema['x-uid'];
|
||||||
|
|
||||||
await customRequestsResource.updateOrCreate({
|
await customRequestsResource.updateOrCreate({
|
||||||
values: {
|
values: {
|
||||||
key: fieldSchema['x-uid'],
|
key: fieldSchema['x-uid'],
|
||||||
@ -67,14 +70,19 @@ export function CustomRequestSettingsItem() {
|
|||||||
},
|
},
|
||||||
filterKeys: ['key'],
|
filterKeys: ['key'],
|
||||||
});
|
});
|
||||||
dn.emit('patch', {
|
const schema = {
|
||||||
schema: {
|
|
||||||
'x-response-type': requestSettings.responseType,
|
'x-response-type': requestSettings.responseType,
|
||||||
'x-uid': fieldSchema['x-uid'],
|
'x-uid': fieldSchema['x-uid'],
|
||||||
},
|
};
|
||||||
|
if (!isSelfRequest && fieldSchema['x-custom-request-id']) {
|
||||||
|
schema['x-custom-request-id'] = fieldSchema['x-uid'];
|
||||||
|
fieldSchema['x-custom-request-id'] = fieldSchema['x-uid'];
|
||||||
|
}
|
||||||
|
await dn.emit('patch', {
|
||||||
|
schema,
|
||||||
});
|
});
|
||||||
refresh();
|
|
||||||
dn.refresh();
|
dn.refresh();
|
||||||
|
refresh();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
@ -87,6 +95,7 @@ export function CustomRequestACL() {
|
|||||||
const customRequestsResource = useCustomRequestsResource();
|
const customRequestsResource = useCustomRequestsResource();
|
||||||
const { message } = App.useApp();
|
const { message } = App.useApp();
|
||||||
const { data, refresh } = useGetCustomRequest();
|
const { data, refresh } = useGetCustomRequest();
|
||||||
|
const { dn } = useDesignable();
|
||||||
const { refresh: refreshRoleCustomKeys } = useRequest<{ data: string[] }>(
|
const { refresh: refreshRoleCustomKeys } = useRequest<{ data: string[] }>(
|
||||||
{
|
{
|
||||||
url: listByCurrentRoleUrl,
|
url: listByCurrentRoleUrl,
|
||||||
@ -107,6 +116,19 @@ export function CustomRequestACL() {
|
|||||||
}}
|
}}
|
||||||
beforeOpen={() => !data && refresh()}
|
beforeOpen={() => !data && refresh()}
|
||||||
onSubmit={async ({ roles }) => {
|
onSubmit={async ({ roles }) => {
|
||||||
|
const isSelfRequest =
|
||||||
|
!fieldSchema['x-custom-request-id'] || fieldSchema['x-custom-request-id'] === fieldSchema['x-uid'];
|
||||||
|
|
||||||
|
if (!isSelfRequest) {
|
||||||
|
fieldSchema['x-custom-request-id'] = fieldSchema['x-uid'];
|
||||||
|
await dn.emit('patch', {
|
||||||
|
schema: {
|
||||||
|
'x-uid': fieldSchema['x-uid'],
|
||||||
|
'x-custom-request-id': fieldSchema['x-uid'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await customRequestsResource.updateOrCreate({
|
await customRequestsResource.updateOrCreate({
|
||||||
values: {
|
values: {
|
||||||
key: fieldSchema['x-uid'],
|
key: fieldSchema['x-uid'],
|
||||||
@ -116,6 +138,7 @@ export function CustomRequestACL() {
|
|||||||
});
|
});
|
||||||
refresh();
|
refresh();
|
||||||
refreshRoleCustomKeys();
|
refreshRoleCustomKeys();
|
||||||
|
dn.refresh();
|
||||||
return message.success(t('Saved successfully'));
|
return message.success(t('Saved successfully'));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -52,8 +52,9 @@ export const useCustomizeRequestActionProps = () => {
|
|||||||
actionField.data ??= {};
|
actionField.data ??= {};
|
||||||
actionField.data.loading = true;
|
actionField.data.loading = true;
|
||||||
try {
|
try {
|
||||||
|
const requestId = fieldSchema['x-custom-request-id'] || fieldSchema['x-uid'];
|
||||||
const res = await apiClient.request({
|
const res = await apiClient.request({
|
||||||
url: `/customRequests:send/${fieldSchema['x-uid']}`,
|
url: `/customRequests:send/${requestId}`,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: {
|
data: {
|
||||||
currentRecord: {
|
currentRecord: {
|
||||||
|
@ -12,7 +12,8 @@ import { useRequest } from '@nocobase/client';
|
|||||||
|
|
||||||
export const useGetCustomRequest = () => {
|
export const useGetCustomRequest = () => {
|
||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
const url = `customRequests:get/${fieldSchema['x-uid']}`;
|
const requestId = fieldSchema['x-custom-request-id'] || fieldSchema['x-uid'];
|
||||||
|
const url = `customRequests:get/${requestId}`;
|
||||||
return useRequest<{ data: { options: any; title: string; roles: any[] } }>(
|
return useRequest<{ data: { options: any; title: string; roles: any[] } }>(
|
||||||
{
|
{
|
||||||
url,
|
url,
|
||||||
|
@ -60,6 +60,10 @@ export const actionDesignerCss = css`
|
|||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
'&.nb-in-template': {
|
||||||
|
background: 'var(--colorTemplateBgSettingsHover)';
|
||||||
|
}
|
||||||
|
,
|
||||||
> .general-schema-designer-icons {
|
> .general-schema-designer-icons {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 2px;
|
right: 2px;
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
/node_modules
|
||||||
|
/src
|
@ -0,0 +1 @@
|
|||||||
|
# @nocobase/plugin-block-template
|
2
packages/plugins/@nocobase/plugin-block-template/client.d.ts
vendored
Normal file
2
packages/plugins/@nocobase/plugin-block-template/client.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './dist/client';
|
||||||
|
export { default } from './dist/client';
|
@ -0,0 +1 @@
|
|||||||
|
module.exports = require('./dist/client/index.js');
|
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "@nocobase/plugin-block-template",
|
||||||
|
"displayName": "Block: template",
|
||||||
|
"displayName.zh-CN": "区块:模板",
|
||||||
|
"description": "Create and manage block templates for reuse on pages.",
|
||||||
|
"description.zh-CN": "创建和管理区块模板,用于在页面中重复使用。",
|
||||||
|
"version": "1.6.0-alpha.14",
|
||||||
|
"license": "AGPL-3.0",
|
||||||
|
"main": "dist/server/index.js",
|
||||||
|
"homepage": "https://docs.nocobase.com/handbook/block-template",
|
||||||
|
"homepage.zh-CN": "https://docs-cn.nocobase.com/handbook/block-template",
|
||||||
|
"dependencies": {},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@nocobase/client": "1.x",
|
||||||
|
"@nocobase/server": "1.x",
|
||||||
|
"@nocobase/test": "1.x",
|
||||||
|
"@nocobase/plugin-ui-schema-storage": "1.x"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"Block",
|
||||||
|
"Template"
|
||||||
|
]
|
||||||
|
}
|
2
packages/plugins/@nocobase/plugin-block-template/server.d.ts
vendored
Normal file
2
packages/plugins/@nocobase/plugin-block-template/server.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './dist/server';
|
||||||
|
export { default } from './dist/server';
|
@ -0,0 +1 @@
|
|||||||
|
module.exports = require('./dist/server/index.js');
|
249
packages/plugins/@nocobase/plugin-block-template/src/client/client.d.ts
vendored
Normal file
249
packages/plugins/@nocobase/plugin-block-template/src/client/client.d.ts
vendored
Normal file
@ -0,0 +1,249 @@
|
|||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// CSS modules
|
||||||
|
type CSSModuleClasses = { readonly [key: string]: string };
|
||||||
|
|
||||||
|
declare module '*.module.css' {
|
||||||
|
const classes: CSSModuleClasses;
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
|
declare module '*.module.scss' {
|
||||||
|
const classes: CSSModuleClasses;
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
|
declare module '*.module.sass' {
|
||||||
|
const classes: CSSModuleClasses;
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
|
declare module '*.module.less' {
|
||||||
|
const classes: CSSModuleClasses;
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
|
declare module '*.module.styl' {
|
||||||
|
const classes: CSSModuleClasses;
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
|
declare module '*.module.stylus' {
|
||||||
|
const classes: CSSModuleClasses;
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
|
declare module '*.module.pcss' {
|
||||||
|
const classes: CSSModuleClasses;
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
|
declare module '*.module.sss' {
|
||||||
|
const classes: CSSModuleClasses;
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CSS
|
||||||
|
declare module '*.css' { }
|
||||||
|
declare module '*.scss' { }
|
||||||
|
declare module '*.sass' { }
|
||||||
|
declare module '*.less' { }
|
||||||
|
declare module '*.styl' { }
|
||||||
|
declare module '*.stylus' { }
|
||||||
|
declare module '*.pcss' { }
|
||||||
|
declare module '*.sss' { }
|
||||||
|
|
||||||
|
// Built-in asset types
|
||||||
|
// see `src/node/constants.ts`
|
||||||
|
|
||||||
|
// images
|
||||||
|
declare module '*.apng' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.png' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.jpg' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.jpeg' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.jfif' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.pjpeg' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.pjp' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.gif' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.svg' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.ico' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.webp' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.avif' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
// media
|
||||||
|
declare module '*.mp4' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.webm' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.ogg' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.mp3' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.wav' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.flac' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.aac' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.opus' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.mov' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.m4a' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.vtt' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
// fonts
|
||||||
|
declare module '*.woff' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.woff2' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.eot' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.ttf' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.otf' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
// other
|
||||||
|
declare module '*.webmanifest' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.pdf' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.txt' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
// wasm?init
|
||||||
|
declare module '*.wasm?init' {
|
||||||
|
const initWasm: (options?: WebAssembly.Imports) => Promise<WebAssembly.Instance>;
|
||||||
|
export default initWasm;
|
||||||
|
}
|
||||||
|
|
||||||
|
// web worker
|
||||||
|
declare module '*?worker' {
|
||||||
|
const workerConstructor: {
|
||||||
|
new(options?: { name?: string }): Worker;
|
||||||
|
};
|
||||||
|
export default workerConstructor;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*?worker&inline' {
|
||||||
|
const workerConstructor: {
|
||||||
|
new(options?: { name?: string }): Worker;
|
||||||
|
};
|
||||||
|
export default workerConstructor;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*?worker&url' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*?sharedworker' {
|
||||||
|
const sharedWorkerConstructor: {
|
||||||
|
new(options?: { name?: string }): SharedWorker;
|
||||||
|
};
|
||||||
|
export default sharedWorkerConstructor;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*?sharedworker&inline' {
|
||||||
|
const sharedWorkerConstructor: {
|
||||||
|
new(options?: { name?: string }): SharedWorker;
|
||||||
|
};
|
||||||
|
export default sharedWorkerConstructor;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*?sharedworker&url' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*?raw' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*?url' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*?inline' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
@ -0,0 +1,60 @@
|
|||||||
|
/**
|
||||||
|
* 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 { tStr } from '../locale';
|
||||||
|
|
||||||
|
export const blockTemplatesCollection = {
|
||||||
|
name: 'blockTemplates',
|
||||||
|
filterTargetKey: 'key',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
name: 'title',
|
||||||
|
interface: 'input',
|
||||||
|
uiSchema: {
|
||||||
|
type: 'string',
|
||||||
|
title: "{{t('Title')}}",
|
||||||
|
required: true,
|
||||||
|
'x-component': 'Input',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
name: 'key',
|
||||||
|
interface: 'input',
|
||||||
|
uiSchema: {
|
||||||
|
type: 'string',
|
||||||
|
title: "{{t('Name')}}",
|
||||||
|
required: true,
|
||||||
|
'x-component': 'Input',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
name: 'type',
|
||||||
|
interface: 'radioGroup',
|
||||||
|
uiSchema: {
|
||||||
|
type: 'string',
|
||||||
|
title: tStr('Type'),
|
||||||
|
'x-component': 'Radio.Group',
|
||||||
|
enum: '{{ typeOptions }}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
name: 'description',
|
||||||
|
interface: 'textarea',
|
||||||
|
uiSchema: {
|
||||||
|
type: 'string',
|
||||||
|
title: "{{t('Description')}}",
|
||||||
|
'x-component': 'Input.TextArea',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
@ -0,0 +1,38 @@
|
|||||||
|
import { ActionContextProvider, SchemaComponent, FormBlockProvider } from '@nocobase/client';
|
||||||
|
import React, { createContext, useState } from 'react';
|
||||||
|
import { useT } from '../locale';
|
||||||
|
import { Button } from 'antd';
|
||||||
|
import { PlusOutlined } from '@ant-design/icons';
|
||||||
|
import { createActionSchema } from '../schemas/createActionSchema';
|
||||||
|
import { createForm } from '@formily/core';
|
||||||
|
import { uid } from '@nocobase/utils/client';
|
||||||
|
|
||||||
|
export const NewTemplateFormContext = createContext(null);
|
||||||
|
|
||||||
|
export const AddNewTemplate = () => {
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
const [form, setForm] = useState(null);
|
||||||
|
const t = useT();
|
||||||
|
const handleClick = () => {
|
||||||
|
setForm(
|
||||||
|
createForm({
|
||||||
|
initialValues: {
|
||||||
|
key: `t_${uid()}`,
|
||||||
|
type: 'Desktop',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
setVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ActionContextProvider value={{ visible, setVisible }}>
|
||||||
|
<Button icon={<PlusOutlined />} type={'primary'} onClick={handleClick}>
|
||||||
|
{t('Add new')}
|
||||||
|
</Button>
|
||||||
|
<NewTemplateFormContext.Provider value={form}>
|
||||||
|
<SchemaComponent schema={createActionSchema} />
|
||||||
|
</NewTemplateFormContext.Provider>
|
||||||
|
</ActionContextProvider>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,15 @@
|
|||||||
|
import { createContext, useContext } from 'react';
|
||||||
|
|
||||||
|
interface BlockTemplateInfo {
|
||||||
|
uid?: string;
|
||||||
|
key?: string;
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
configured?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BlockTemplateInfoContext = createContext<BlockTemplateInfo>({});
|
||||||
|
|
||||||
|
export const useBlockTemplateInfo = () => {
|
||||||
|
return useContext(BlockTemplateInfoContext);
|
||||||
|
};
|
@ -0,0 +1,54 @@
|
|||||||
|
/**
|
||||||
|
* 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 {
|
||||||
|
ExtendCollectionsProvider,
|
||||||
|
SchemaComponent,
|
||||||
|
SchemaComponentContext,
|
||||||
|
useSchemaComponentContext,
|
||||||
|
} from '@nocobase/client';
|
||||||
|
import React from 'react';
|
||||||
|
import { blockTemplatesCollection } from '../collections/blockTemplates';
|
||||||
|
import { blockTemplatesSchema } from '../schemas/blockTemplates';
|
||||||
|
import {
|
||||||
|
useDuplicateAction,
|
||||||
|
useCreateActionProps,
|
||||||
|
useEditActionProps,
|
||||||
|
useEditFormProps,
|
||||||
|
useDeleteAction,
|
||||||
|
useBulkDestroyAction,
|
||||||
|
} from '../hooks';
|
||||||
|
import { useT } from '../locale';
|
||||||
|
|
||||||
|
export const BlockTemplateList = () => {
|
||||||
|
const scCtx = useSchemaComponentContext();
|
||||||
|
const t = useT();
|
||||||
|
const typeOptions = [
|
||||||
|
{ label: t('Desktop'), value: 'Desktop' },
|
||||||
|
{ label: t('Mobile'), value: 'Mobile' },
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
<ExtendCollectionsProvider collections={[blockTemplatesCollection]}>
|
||||||
|
<SchemaComponentContext.Provider value={{ ...scCtx, designable: false }}>
|
||||||
|
<SchemaComponent
|
||||||
|
schema={blockTemplatesSchema}
|
||||||
|
scope={{
|
||||||
|
useCreateActionProps,
|
||||||
|
useEditActionProps,
|
||||||
|
useEditFormProps,
|
||||||
|
useDeleteAction,
|
||||||
|
useDuplicateAction,
|
||||||
|
useBulkDestroyAction,
|
||||||
|
typeOptions,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</SchemaComponentContext.Provider>
|
||||||
|
</ExtendCollectionsProvider>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,218 @@
|
|||||||
|
/**
|
||||||
|
* 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 {
|
||||||
|
useRequest,
|
||||||
|
useAPIClient,
|
||||||
|
usePlugin,
|
||||||
|
registerInitializerMenusGenerator,
|
||||||
|
useResource,
|
||||||
|
ISchema,
|
||||||
|
SchemaInitializerItemType,
|
||||||
|
} from '@nocobase/client';
|
||||||
|
import React, { createContext, useContext, useEffect } from 'react';
|
||||||
|
import PluginBlockTemplateClient from '..';
|
||||||
|
import PluginMobileClient from '@nocobase/plugin-mobile/client';
|
||||||
|
import { useT } from '../locale';
|
||||||
|
import { findBlockRootSchema } from '../utils/schema';
|
||||||
|
import { convertTemplateToBlock, correctIdReferences } from '../initializers/TemplateBlockInitializer';
|
||||||
|
import { useMemoizedFn } from 'ahooks';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
|
interface BlockTemplateContextProps {
|
||||||
|
loading: boolean;
|
||||||
|
templates: any[];
|
||||||
|
handleTemplateClick: (item: any, options?: any, insert?: any) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BlockTemplateMenusContext = createContext<BlockTemplateContextProps>({
|
||||||
|
loading: false,
|
||||||
|
templates: [],
|
||||||
|
handleTemplateClick: async () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const useBlockTemplateMenus = () => {
|
||||||
|
return useContext(BlockTemplateMenusContext);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BlockTemplateMenusProvider = ({ children }) => {
|
||||||
|
const api = useAPIClient();
|
||||||
|
const plugin = usePlugin(PluginBlockTemplateClient);
|
||||||
|
const mobilePlugin = usePlugin(PluginMobileClient);
|
||||||
|
const blockTemplatesResource = useResource('blockTemplates');
|
||||||
|
const t = useT();
|
||||||
|
const isMobile = window.location.pathname.startsWith(mobilePlugin.mobileBasename);
|
||||||
|
const location = useLocation();
|
||||||
|
const previousPathRef = React.useRef(location.pathname);
|
||||||
|
|
||||||
|
const { data, loading, refresh } = useRequest<{
|
||||||
|
data: {
|
||||||
|
key: string;
|
||||||
|
title: string;
|
||||||
|
componentType?: string;
|
||||||
|
menuName?: string;
|
||||||
|
collection?: string;
|
||||||
|
dataSource?: string;
|
||||||
|
}[];
|
||||||
|
}>(
|
||||||
|
{
|
||||||
|
url: 'blockTemplates:list',
|
||||||
|
method: 'get',
|
||||||
|
params: {
|
||||||
|
filter: {
|
||||||
|
configured: true,
|
||||||
|
type: isMobile ? 'Mobile' : { $ne: 'Mobile' },
|
||||||
|
},
|
||||||
|
paginate: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cacheKey: 'blockTemplates',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const isLeavingTemplatesPage =
|
||||||
|
previousPathRef.current.includes('/settings/block-templates') &&
|
||||||
|
!location.pathname.includes('/settings/block-templates');
|
||||||
|
if (isLeavingTemplatesPage) {
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
previousPathRef.current = location.pathname;
|
||||||
|
}, [location.pathname, refresh]);
|
||||||
|
|
||||||
|
const handleTemplateClick = useMemoizedFn(async ({ item }, options?: any, insert?: any) => {
|
||||||
|
const { uid } = item;
|
||||||
|
const { data } = await api.request({
|
||||||
|
url: `uiSchemas:getProperties/${uid}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const template = data?.data;
|
||||||
|
const schemas = convertTemplateToBlock(template, item.key, options);
|
||||||
|
plugin.setTemplateCache(findBlockRootSchema(template['properties']?.['blocks']));
|
||||||
|
correctIdReferences(schemas);
|
||||||
|
for (const schema of schemas) {
|
||||||
|
insert?.(schema);
|
||||||
|
}
|
||||||
|
// server hook only support root node, so we do the link from client
|
||||||
|
const links = [];
|
||||||
|
const fillLink = (schema: ISchema) => {
|
||||||
|
if (schema['x-template-root-uid']) {
|
||||||
|
links.push({
|
||||||
|
templateKey: item.key,
|
||||||
|
templateBlockUid: schema['x-template-root-uid'],
|
||||||
|
blockUid: schema['x-uid'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (schema.properties) {
|
||||||
|
for (const key in schema.properties) {
|
||||||
|
fillLink(schema.properties[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
for (const schema of schemas) {
|
||||||
|
fillLink(schema);
|
||||||
|
}
|
||||||
|
blockTemplatesResource.link({
|
||||||
|
values: links,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
data?.data?.forEach((item) => {
|
||||||
|
plugin.templateInfos.set(item.key, item);
|
||||||
|
});
|
||||||
|
}, [data?.data, plugin.templateInfos]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const generator = ({ collection, association, item, index, field, componentName, dataSource, keyPrefix, name }) => {
|
||||||
|
let collectionName = collection?.name || item?.options?.name;
|
||||||
|
const dataSourceName = dataSource || item?.options?.dataSource || collection?.dataSource;
|
||||||
|
const isInWorkflowPage = window.location.pathname.includes('/admin/workflow');
|
||||||
|
|
||||||
|
if (componentName?.startsWith('mobile-')) {
|
||||||
|
componentName = componentName.replace('mobile-', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plugin.isInBlockTemplateConfigPage() || isInWorkflowPage) {
|
||||||
|
// hide menu in template config page
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field) {
|
||||||
|
// association field
|
||||||
|
collectionName = field?.target;
|
||||||
|
}
|
||||||
|
const isDetails = name === 'details' || componentName === 'ReadPrettyFormItem';
|
||||||
|
const children = data?.data
|
||||||
|
?.filter(
|
||||||
|
(d) =>
|
||||||
|
(d.componentType === componentName ||
|
||||||
|
name === d['menuName'] ||
|
||||||
|
(isDetails && d['menuName'] === 'details')) &&
|
||||||
|
d.collection === collectionName &&
|
||||||
|
d.dataSource === dataSourceName,
|
||||||
|
)
|
||||||
|
.map((m) => {
|
||||||
|
return {
|
||||||
|
type: 'item',
|
||||||
|
name: m.key,
|
||||||
|
item: m,
|
||||||
|
title: m.title,
|
||||||
|
schemaInsertor: (insert, { item, fromOthersInPopup, name }) => {
|
||||||
|
const options = { dataSourceName };
|
||||||
|
if (association && (name === 'editForm' || name === 'currentRecord')) {
|
||||||
|
options['association'] = association;
|
||||||
|
}
|
||||||
|
if (field) {
|
||||||
|
options['association'] = `${collection?.name}.${field.name}`;
|
||||||
|
options['associationType'] = field.type;
|
||||||
|
} else {
|
||||||
|
options['collectionName'] = collectionName;
|
||||||
|
}
|
||||||
|
options['currentRecord'] = name === 'currentRecord' && isDetails;
|
||||||
|
if (name === 'editForm') {
|
||||||
|
options['currentRecord'] = true;
|
||||||
|
}
|
||||||
|
return handleTemplateClick(item, options, insert);
|
||||||
|
},
|
||||||
|
} as SchemaInitializerItemType;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!children?.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
type: 'divider',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'itemGroup',
|
||||||
|
title: t('Block template'),
|
||||||
|
children,
|
||||||
|
},
|
||||||
|
] as SchemaInitializerItemType[];
|
||||||
|
};
|
||||||
|
registerInitializerMenusGenerator('block_template', generator);
|
||||||
|
}, [data?.data, plugin.isInBlockTemplateConfigPage, handleTemplateClick, t, plugin]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BlockTemplateMenusContext.Provider
|
||||||
|
value={{
|
||||||
|
loading,
|
||||||
|
templates: data?.data || [],
|
||||||
|
handleTemplateClick,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</BlockTemplateMenusContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
BlockTemplateMenusProvider.displayName = 'BlockTemplateMenusProvider';
|
@ -0,0 +1,49 @@
|
|||||||
|
/**
|
||||||
|
* 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 React, { useEffect } from 'react';
|
||||||
|
import { MobilePage } from '@nocobase/plugin-mobile/client';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
import { useRequest } from '@nocobase/client';
|
||||||
|
import { Spin } from 'antd';
|
||||||
|
import { BlockTemplateInfoContext } from './BlockTemplateInfoContext';
|
||||||
|
|
||||||
|
export const BlockTemplateMobilePage = () => {
|
||||||
|
const { key } = useParams<{ key: string }>();
|
||||||
|
const { data, loading } = useRequest<any>({
|
||||||
|
url: `blockTemplates:get/${key}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// hide tab bar
|
||||||
|
const tabBar = document.querySelector('.ant-nb-mobile-tab-bar');
|
||||||
|
const tabBarDisplay = (tabBar as HTMLElement)?.style?.display;
|
||||||
|
if (tabBar) {
|
||||||
|
(tabBar as HTMLElement).style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// show tab bar
|
||||||
|
return () => {
|
||||||
|
const tabBar = document.querySelector('.ant-nb-mobile-tab-bar');
|
||||||
|
if (tabBar) {
|
||||||
|
(tabBar as HTMLElement).style.display = tabBarDisplay;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <Spin />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BlockTemplateInfoContext.Provider value={data?.data}>
|
||||||
|
<MobilePage />
|
||||||
|
</BlockTemplateInfoContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,63 @@
|
|||||||
|
/**
|
||||||
|
* 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 { useRequest, RemoteSchemaComponent } from '@nocobase/client';
|
||||||
|
import React from 'react';
|
||||||
|
import { useT } from '../locale';
|
||||||
|
import { useParams } from 'react-router';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { Breadcrumb, Spin, theme } from 'antd';
|
||||||
|
import { BlockTemplateInfoContext } from './BlockTemplateInfoContext';
|
||||||
|
|
||||||
|
export const BlockTemplatePage = () => {
|
||||||
|
const params = useParams();
|
||||||
|
const { token } = theme.useToken();
|
||||||
|
const t = useT();
|
||||||
|
const { data, loading } = useRequest<any>({
|
||||||
|
url: `blockTemplates:get/${params.key}`,
|
||||||
|
});
|
||||||
|
const { title } = data?.data || {};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <Spin />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const schemaUid = data?.data?.uid;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
margin: -token.margin,
|
||||||
|
padding: token.paddingSM,
|
||||||
|
background: token.colorBgContainer,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Breadcrumb
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
title: <Link to={`/admin/settings/block-templates`}>{t('Block template')}</Link>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: title,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div style={{ marginTop: token.marginXL, position: 'relative', zIndex: 0 /** create a new z-index context */ }}>
|
||||||
|
<BlockTemplateInfoContext.Provider value={data?.data}>
|
||||||
|
<RemoteSchemaComponent uid={schemaUid} />
|
||||||
|
</BlockTemplateInfoContext.Provider>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* 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 { useCollectionRecordData, useFilterByTk } from '@nocobase/client';
|
||||||
|
import React from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { useT } from '../locale';
|
||||||
|
export const ConfigureLink = () => {
|
||||||
|
const value = useFilterByTk();
|
||||||
|
const recordData = useCollectionRecordData();
|
||||||
|
const t = useT();
|
||||||
|
let to = `/admin/settings/block-templates/${value}`;
|
||||||
|
if (recordData.type === 'Mobile') {
|
||||||
|
to = `/m/block-templates/${recordData.key}/${recordData.uid}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Link to={to}>{t('Configure')}</Link>;
|
||||||
|
};
|
@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* 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 { SchemaSettingsDivider, SchemaSettingsItem } from '@nocobase/client';
|
||||||
|
import { QuestionCircleOutlined } from '@ant-design/icons';
|
||||||
|
import { Tooltip, Space } from 'antd';
|
||||||
|
import React from 'react';
|
||||||
|
import { useT } from '../locale';
|
||||||
|
|
||||||
|
export const DisabledDeleteItem = () => {
|
||||||
|
const t = useT();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SchemaSettingsDivider />
|
||||||
|
<SchemaSettingsItem disabled={true} title={t('Delete')}>
|
||||||
|
<Space>
|
||||||
|
{t('Delete')}
|
||||||
|
<Tooltip title={t('This is part of a template, deletion is not allowed')}>
|
||||||
|
<QuestionCircleOutlined />
|
||||||
|
</Tooltip>
|
||||||
|
</Space>
|
||||||
|
</SchemaSettingsItem>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,218 @@
|
|||||||
|
/**
|
||||||
|
* 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,
|
||||||
|
usePlugin,
|
||||||
|
SchemaSettingsDivider,
|
||||||
|
} from '@nocobase/client';
|
||||||
|
import { useFieldSchema, useForm, useField } from '@formily/react';
|
||||||
|
import { App } from 'antd';
|
||||||
|
import React from 'react';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { blockKeepProps, convertTplBlock, formSchemaPatch } from '../initializers/TemplateBlockInitializer';
|
||||||
|
import { Schema } from '@formily/json-schema';
|
||||||
|
import { useT } from '../locale';
|
||||||
|
import PluginBlockTemplateClient from '..';
|
||||||
|
import { addToolbarClass, syncExtraTemplateInfo } from '../utils/template';
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RevertSetting = () => {
|
||||||
|
const { refresh, remove } = useDesignable();
|
||||||
|
const plugin = usePlugin(PluginBlockTemplateClient);
|
||||||
|
const t = useT();
|
||||||
|
const api = useAPIClient();
|
||||||
|
const form = useForm();
|
||||||
|
const field = useField();
|
||||||
|
// const { runAsync } = useDataBlockRequest();
|
||||||
|
const { form: blockForm } = useFormBlockProps();
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const { modal, message } = App.useApp();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SchemaSettingsDivider />
|
||||||
|
<SchemaSettingsItem
|
||||||
|
title={t('Revert to template')}
|
||||||
|
onClick={() => {
|
||||||
|
modal.confirm({
|
||||||
|
title: t('Revert to template'),
|
||||||
|
content: t('Are you sure you want to revert all changes from the template?'),
|
||||||
|
...confirm,
|
||||||
|
async onOk() {
|
||||||
|
const templateSchemaId = _.get(fieldSchema, 'x-template-uid');
|
||||||
|
const res = await api.request({
|
||||||
|
url: `/uiSchemas:getJsonSchema/${templateSchemaId}`,
|
||||||
|
});
|
||||||
|
const templateSchema = res.data?.data;
|
||||||
|
if (!templateSchema?.['x-uid']) {
|
||||||
|
// this means the template has already been deleted
|
||||||
|
remove(null, {
|
||||||
|
removeParentsIfNoChildren: true,
|
||||||
|
breakRemoveOn: {
|
||||||
|
'x-component': 'Grid',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
refresh({ refreshParentSchema: true });
|
||||||
|
form.reset();
|
||||||
|
form.clearFormGraph();
|
||||||
|
blockForm?.clearFormGraph();
|
||||||
|
message.success(t('Reset successfully'), 0.2);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rootSchema = findParentRootTemplateSchema(fieldSchema);
|
||||||
|
const isRoot = rootSchema === fieldSchema;
|
||||||
|
if (isRoot) {
|
||||||
|
plugin.setTemplateCache(templateSchema);
|
||||||
|
} else {
|
||||||
|
// patch the edit form button schema, keep same as the form
|
||||||
|
if (fieldSchema['x-settings']?.includes('updateSubmit')) {
|
||||||
|
templateSchema['x-settings'] = 'actionSettings:updateSubmit';
|
||||||
|
templateSchema['x-use-component-props'] = 'useUpdateActionProps';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// patch filter block
|
||||||
|
// remove this when multiple blocks template supported
|
||||||
|
if (fieldSchema['x-filter-targets']) {
|
||||||
|
templateSchema['x-filter-targets'] = fieldSchema['x-filter-targets'];
|
||||||
|
}
|
||||||
|
|
||||||
|
const newSchema = convertTplBlock(
|
||||||
|
templateSchema,
|
||||||
|
false,
|
||||||
|
isRoot,
|
||||||
|
rootSchema?.['x-uid'],
|
||||||
|
rootSchema?.['x-block-template-key'],
|
||||||
|
);
|
||||||
|
newSchema['x-index'] = fieldSchema['x-index'];
|
||||||
|
for (const p of blockKeepProps) {
|
||||||
|
if (_.hasIn(fieldSchema, p)) {
|
||||||
|
_.set(newSchema, p, _.get(fieldSchema, p));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
fieldSchema['x-decorator'] === 'FormBlockProvider' &&
|
||||||
|
fieldSchema['x-use-decorator-props'] === 'useEditFormBlockDecoratorProps'
|
||||||
|
) {
|
||||||
|
formSchemaPatch(newSchema, {
|
||||||
|
collectionName: fieldSchema['x-decorator-props']['collection'],
|
||||||
|
dataSourceName: fieldSchema['x-decorator-props']['dataSource'],
|
||||||
|
association: fieldSchema['x-decorator-props']['association'],
|
||||||
|
currentRecord: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// remove old schema
|
||||||
|
const position = findInsertPosition(fieldSchema.parent, fieldSchema['x-uid']);
|
||||||
|
|
||||||
|
await api.request({
|
||||||
|
url: `/uiSchemas:remove/${fieldSchema['x-uid']}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
// insertAdjacent
|
||||||
|
const schema = new Schema(newSchema);
|
||||||
|
schema.name = fieldSchema.name;
|
||||||
|
await api.request({
|
||||||
|
url: `/uiSchemas:insertAdjacent/${position.insertTarget}?position=${position.insertPosition}`,
|
||||||
|
method: 'post',
|
||||||
|
data: {
|
||||||
|
schema,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// this is a hack to make the schema component refresh to the new schema
|
||||||
|
fieldSchema.toJSON = () => {
|
||||||
|
let ret;
|
||||||
|
if (schema['x-template-root-uid'] || fieldSchema.parent?.['x-template-root-uid']) {
|
||||||
|
ret = schema.toJSON();
|
||||||
|
} else {
|
||||||
|
const mergedSchema = _.merge(templateSchema, schema.toJSON());
|
||||||
|
ret = mergedSchema;
|
||||||
|
}
|
||||||
|
addToolbarClass(ret);
|
||||||
|
syncExtraTemplateInfo(ret, plugin.templateInfos, plugin.savedSchemaUids);
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
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();
|
||||||
|
blockForm?.reset();
|
||||||
|
form.clearFormGraph('*', false);
|
||||||
|
blockForm?.clearFormGraph('*', false);
|
||||||
|
|
||||||
|
message.success(t('Reset successfully'), 0.2);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('Revert to template')}
|
||||||
|
</SchemaSettingsItem>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,163 @@
|
|||||||
|
/**
|
||||||
|
* 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 React, { useCallback, useContext, useEffect, useRef } from 'react';
|
||||||
|
import { observer, useFieldSchema, useField, useFormEffects, useForm } from '@formily/react';
|
||||||
|
import { onFieldReact } from '@formily/core';
|
||||||
|
import { useUpdate } from 'ahooks';
|
||||||
|
import { SchemaComponentOnChangeContext, useAPIClient } from '@nocobase/client';
|
||||||
|
import { useBlockTemplateInfo } from './BlockTemplateInfoContext';
|
||||||
|
import { Schema } from '@nocobase/utils';
|
||||||
|
import { findBlockRootSchema } from '../utils/schema';
|
||||||
|
|
||||||
|
export const TemplateGridDecorator = observer((props: any) => {
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const field = useField();
|
||||||
|
const update = useUpdate();
|
||||||
|
const preInitializerDisplay = useRef('block');
|
||||||
|
const preBlockSchemaUid = useRef(findBlockRootSchema(fieldSchema)?.['x-uid']);
|
||||||
|
const api = useAPIClient();
|
||||||
|
const template = useBlockTemplateInfo();
|
||||||
|
const form = useForm();
|
||||||
|
|
||||||
|
const { onChange: onChangeFromContext } = useContext(SchemaComponentOnChangeContext);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const dispose = form.subscribe(({ type, payload }) => {
|
||||||
|
if (type === 'blockAdded') {
|
||||||
|
api.resource('blockTemplates').update({
|
||||||
|
filter: {
|
||||||
|
key: template.key,
|
||||||
|
},
|
||||||
|
values: {
|
||||||
|
collection: payload.collection,
|
||||||
|
dataSource: payload.dataSource,
|
||||||
|
componentType: payload.componentType,
|
||||||
|
menuName: payload.menuName,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
form.unsubscribe(dispose);
|
||||||
|
};
|
||||||
|
}, [form]);
|
||||||
|
|
||||||
|
fieldSchema['x-initializer-props'] = {
|
||||||
|
style: {
|
||||||
|
display: preInitializerDisplay.current,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// used to hide add blocks button in toolbar, for the moment, only need to support one layer
|
||||||
|
const updateInitializer = useCallback(
|
||||||
|
(display: string) => {
|
||||||
|
let updatedLayer = 0;
|
||||||
|
const updater = (s: Schema) => {
|
||||||
|
if (s['x-toolbar'] || s['x-settings']) {
|
||||||
|
s['x-toolbar-props'] = {
|
||||||
|
...s['x-toolbar-props'],
|
||||||
|
initializer: display === 'block' ? undefined : false,
|
||||||
|
};
|
||||||
|
updatedLayer++;
|
||||||
|
}
|
||||||
|
if (updatedLayer > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (s.properties) {
|
||||||
|
Object.keys(s.properties).forEach((key) => {
|
||||||
|
updater(s.properties[key]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
updater(fieldSchema);
|
||||||
|
},
|
||||||
|
[fieldSchema],
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateInitializerDisplay = useCallback(
|
||||||
|
(configured: boolean) => {
|
||||||
|
const initializerDisplay = !configured ? 'block' : 'none';
|
||||||
|
if (initializerDisplay !== preInitializerDisplay.current) {
|
||||||
|
preInitializerDisplay.current = initializerDisplay;
|
||||||
|
field.decoratorProps.style = {
|
||||||
|
display: initializerDisplay,
|
||||||
|
};
|
||||||
|
updateInitializer(initializerDisplay);
|
||||||
|
field.form.clearFormGraph(); // refresh form graph
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[update, field, updateInitializer],
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateTemplateConfigured = useCallback(
|
||||||
|
(configured: boolean) => {
|
||||||
|
if (template && template.configured !== configured) {
|
||||||
|
const values = { configured };
|
||||||
|
if (!configured) {
|
||||||
|
values['dataSource'] = null;
|
||||||
|
values['collection'] = null;
|
||||||
|
values['componentType'] = null;
|
||||||
|
values['menuName'] = null;
|
||||||
|
}
|
||||||
|
api.resource('blockTemplates').update({
|
||||||
|
filter: {
|
||||||
|
key: template.key,
|
||||||
|
},
|
||||||
|
values,
|
||||||
|
});
|
||||||
|
template.configured = configured;
|
||||||
|
if (!configured && preBlockSchemaUid.current) {
|
||||||
|
// this means the block has already been deleted
|
||||||
|
// let's call the remove API, otherwise the block will still be cached in server!!!
|
||||||
|
// maybe better way is to update cache in server clearXUidPathCache?
|
||||||
|
api.request({
|
||||||
|
url: `/uiSchemas:remove/${preBlockSchemaUid.current}`,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
preBlockSchemaUid.current = findBlockRootSchema(fieldSchema)?.['x-uid'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[api, template, fieldSchema],
|
||||||
|
);
|
||||||
|
|
||||||
|
const syncTemplateInfo = useCallback(() => {
|
||||||
|
const configured = Object.keys(fieldSchema['properties'] || {}).length > 0;
|
||||||
|
updateInitializerDisplay(configured);
|
||||||
|
updateTemplateConfigured(configured);
|
||||||
|
}, [fieldSchema, updateInitializerDisplay, updateTemplateConfigured]);
|
||||||
|
|
||||||
|
const onChange = useCallback(
|
||||||
|
// schema will not be passed here, is it a core bug?
|
||||||
|
() => {
|
||||||
|
syncTemplateInfo();
|
||||||
|
onChangeFromContext?.(fieldSchema);
|
||||||
|
},
|
||||||
|
[fieldSchema, syncTemplateInfo, onChangeFromContext],
|
||||||
|
);
|
||||||
|
|
||||||
|
useFormEffects(() => {
|
||||||
|
onFieldReact('blocks.*.*', () => {
|
||||||
|
syncTemplateInfo();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
syncTemplateInfo();
|
||||||
|
}, [syncTemplateInfo]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SchemaComponentOnChangeContext.Provider value={{ onChange }}>
|
||||||
|
{props.children}
|
||||||
|
</SchemaComponentOnChangeContext.Provider>
|
||||||
|
);
|
||||||
|
});
|
@ -0,0 +1,18 @@
|
|||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from './AddNewTemplate';
|
||||||
|
export * from './BlockTemplateList';
|
||||||
|
export * from './BlockTemplatePage';
|
||||||
|
export * from './BlockTemplateMobilePage';
|
||||||
|
export * from './ConfigureLink';
|
||||||
|
export * from './RevertSetting';
|
||||||
|
export * from './BlockTemplateInfoContext';
|
||||||
|
export * from './TemplateGridDecorator';
|
||||||
|
export * from './DisabledDeleteItem';
|
@ -0,0 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const BlockName = 'Template';
|
||||||
|
export const BlockNameLowercase = BlockName.toLowerCase();
|
||||||
|
export const NAMESPACE = 'block-template';
|
@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from './useDuplicateAction';
|
||||||
|
export * from './useCreateActionProps';
|
||||||
|
export * from './useEditActionProps';
|
||||||
|
export * from './useDeleteAction';
|
||||||
|
export * from './useEditFormProps';
|
||||||
|
export * from './useBulkDestroyAction';
|
@ -0,0 +1,66 @@
|
|||||||
|
/**
|
||||||
|
* 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 { useAPIClient, usePlugin, useDataBlockRequest, useActionContext } from '@nocobase/client';
|
||||||
|
import { useField } from '@formily/react';
|
||||||
|
import { App } from 'antd';
|
||||||
|
import { useT } from '../locale';
|
||||||
|
import { useForm } from '@formily/react';
|
||||||
|
import { useTableBlockProps } from '@nocobase/client';
|
||||||
|
|
||||||
|
export const useBulkDestroyAction = () => {
|
||||||
|
const apiClient = useAPIClient();
|
||||||
|
const { setVisible } = useActionContext();
|
||||||
|
const { data, refresh, run } = useDataBlockRequest();
|
||||||
|
const field = useField();
|
||||||
|
const t = useT();
|
||||||
|
const form = useForm();
|
||||||
|
const { message } = App.useApp();
|
||||||
|
const { onRowSelectionChange } = useTableBlockProps();
|
||||||
|
|
||||||
|
return {
|
||||||
|
async run() {
|
||||||
|
const selectedRowKeys = field.data?.selectedRowKeys;
|
||||||
|
if (!selectedRowKeys?.length) {
|
||||||
|
message.error(t('Please select the records you want to delete'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await apiClient.request({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/blockTemplates:destroy',
|
||||||
|
params: {
|
||||||
|
filterByTk: selectedRowKeys,
|
||||||
|
removeSchema: !form.values.keepBlocks,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset selection
|
||||||
|
onRowSelectionChange([], []);
|
||||||
|
form.reset();
|
||||||
|
|
||||||
|
// Calculate pagination after deletion
|
||||||
|
const currentPage = data?.['meta']?.page || 1;
|
||||||
|
const pageSize = data?.['meta']?.pageSize || 20;
|
||||||
|
const totalCount = data?.['meta']?.count || 0;
|
||||||
|
const remainingItems = totalCount - selectedRowKeys.length;
|
||||||
|
const lastPage = Math.max(Math.ceil(remainingItems / pageSize), 1);
|
||||||
|
|
||||||
|
// Update data with appropriate page
|
||||||
|
if (currentPage > lastPage) {
|
||||||
|
run({ page: lastPage, pageSize });
|
||||||
|
} else {
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
setVisible(false);
|
||||||
|
message.success(t('Deleted successfully'));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,89 @@
|
|||||||
|
/**
|
||||||
|
* 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 { useForm } from '@formily/react';
|
||||||
|
import { useActionContext, useAPIClient, useDataBlockRequest, useDataBlockResource } from '@nocobase/client';
|
||||||
|
import { uid } from '@nocobase/utils/client';
|
||||||
|
import { App as AntdApp } from 'antd';
|
||||||
|
import { useT } from '../locale';
|
||||||
|
|
||||||
|
export const useCreateActionProps = () => {
|
||||||
|
const { setVisible } = useActionContext();
|
||||||
|
const { message } = AntdApp.useApp();
|
||||||
|
const form = useForm();
|
||||||
|
const resource = useDataBlockResource();
|
||||||
|
const api = useAPIClient();
|
||||||
|
const { refresh } = useDataBlockRequest();
|
||||||
|
const t = useT();
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'primary',
|
||||||
|
async onClick() {
|
||||||
|
await form.submit();
|
||||||
|
const values = form.values;
|
||||||
|
const key = values.key;
|
||||||
|
const schemaUid = uid();
|
||||||
|
const isMobile = values['type'] === 'Mobile';
|
||||||
|
const schema = {
|
||||||
|
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': uid(),
|
||||||
|
'x-async': false,
|
||||||
|
'x-index': 1,
|
||||||
|
properties: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'x-uid': schemaUid,
|
||||||
|
'x-async': true,
|
||||||
|
'x-index': 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await resource.create({
|
||||||
|
values: {
|
||||||
|
...values,
|
||||||
|
uid: schemaUid,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await api.resource('uiSchemas').insert({ values: schema });
|
||||||
|
form.reset();
|
||||||
|
refresh();
|
||||||
|
message.success(t('Saved successfully'));
|
||||||
|
setVisible(false);
|
||||||
|
form.values.key = `t_${uid()}`;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,59 @@
|
|||||||
|
/**
|
||||||
|
* 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 {
|
||||||
|
useCollection,
|
||||||
|
useCollectionRecordData,
|
||||||
|
useDataBlockResource,
|
||||||
|
usePlugin,
|
||||||
|
useDataBlockRequest,
|
||||||
|
} from '@nocobase/client';
|
||||||
|
import { App as AntdApp } from 'antd';
|
||||||
|
import { useForm } from '@formily/react';
|
||||||
|
import { useT } from '../locale';
|
||||||
|
|
||||||
|
export function useDeleteAction() {
|
||||||
|
const { message } = AntdApp.useApp();
|
||||||
|
const record = useCollectionRecordData();
|
||||||
|
const resource = useDataBlockResource();
|
||||||
|
const { data, refresh, run } = useDataBlockRequest();
|
||||||
|
const collection = useCollection();
|
||||||
|
const t = useT();
|
||||||
|
const form = useForm();
|
||||||
|
|
||||||
|
return {
|
||||||
|
async run() {
|
||||||
|
if (!collection) {
|
||||||
|
throw new Error('collection does not exist');
|
||||||
|
}
|
||||||
|
await form.submit();
|
||||||
|
const keepBlocks = form.values.keepBlocks;
|
||||||
|
await resource.destroy({
|
||||||
|
filterByTk: record[collection.filterTargetKey],
|
||||||
|
removeSchema: !keepBlocks,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Calculate pagination after deletion
|
||||||
|
const currentPage = data?.['meta']?.page || 1;
|
||||||
|
const pageSize = data?.['meta']?.pageSize || 20;
|
||||||
|
const totalCount = data?.['meta']?.count || 0;
|
||||||
|
const remainingItems = totalCount - 1;
|
||||||
|
const lastPage = Math.max(Math.ceil(remainingItems / pageSize), 1);
|
||||||
|
|
||||||
|
// Update data with appropriate page
|
||||||
|
if (currentPage > lastPage) {
|
||||||
|
run({ page: lastPage, pageSize });
|
||||||
|
} else {
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
message.success(t('Deleted successfully'));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
@ -0,0 +1,95 @@
|
|||||||
|
/**
|
||||||
|
* 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 {
|
||||||
|
useCollection,
|
||||||
|
useCollectionRecordData,
|
||||||
|
useBlockRequestContext,
|
||||||
|
useDataBlockResource,
|
||||||
|
useAPIClient,
|
||||||
|
useActionContext,
|
||||||
|
} from '@nocobase/client';
|
||||||
|
import { useForm } from '@formily/react';
|
||||||
|
import { App as AntdApp } from 'antd';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { uid } from '@nocobase/utils/client';
|
||||||
|
|
||||||
|
const duplicateSchema = (schema) => {
|
||||||
|
if (!schema) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (schema['x-component'] === 'CustomRequestAction') {
|
||||||
|
schema['x-custom-request-id'] = schema['x-custom-request-id'] || schema['x-uid'];
|
||||||
|
}
|
||||||
|
schema['x-uid'] = uid();
|
||||||
|
if (schema.properties) {
|
||||||
|
for (const key of Object.keys(schema.properties)) {
|
||||||
|
duplicateSchema(schema.properties[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return schema;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useDuplicateAction() {
|
||||||
|
const { message } = AntdApp.useApp();
|
||||||
|
const api = useAPIClient();
|
||||||
|
const record = useCollectionRecordData();
|
||||||
|
const resource = useDataBlockResource();
|
||||||
|
const { service } = useBlockRequestContext();
|
||||||
|
const collection = useCollection();
|
||||||
|
const form = useForm();
|
||||||
|
const { setVisible } = useActionContext();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
return {
|
||||||
|
async run() {
|
||||||
|
if (loading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await form.submit();
|
||||||
|
setLoading(true);
|
||||||
|
const values = form.values;
|
||||||
|
if (!collection) {
|
||||||
|
throw new Error('collection does not exist');
|
||||||
|
}
|
||||||
|
const schemaUid = record.uid;
|
||||||
|
const { data: schema } = await api.request({
|
||||||
|
url: `uiSchemas:getJsonSchema/${schemaUid}`,
|
||||||
|
});
|
||||||
|
const duplicatedSchema = duplicateSchema(_.cloneDeep(schema?.data));
|
||||||
|
const newSchemaUid = duplicatedSchema['x-uid'];
|
||||||
|
const newKey = `t_${uid()}`;
|
||||||
|
await api.resource('uiSchemas').insert({
|
||||||
|
values: {
|
||||||
|
type: 'void',
|
||||||
|
name: newKey,
|
||||||
|
'x-uid': `template-${newSchemaUid}`,
|
||||||
|
_isJSONSchemaObject: true,
|
||||||
|
properties: {
|
||||||
|
template: duplicatedSchema,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await resource.create({
|
||||||
|
values: {
|
||||||
|
...record,
|
||||||
|
title: `${values.title}`,
|
||||||
|
key: newKey,
|
||||||
|
uid: newSchemaUid,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await service.refresh();
|
||||||
|
setVisible(false);
|
||||||
|
setLoading(false);
|
||||||
|
await form.reset();
|
||||||
|
message.success('Duplicated!');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
/**
|
||||||
|
* 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 { useForm } from '@formily/react';
|
||||||
|
import {
|
||||||
|
useActionContext,
|
||||||
|
useCollection,
|
||||||
|
useDataBlockRequest,
|
||||||
|
useDataBlockResource,
|
||||||
|
usePlugin,
|
||||||
|
} from '@nocobase/client';
|
||||||
|
import { App as AntdApp } from 'antd';
|
||||||
|
import { useT } from '../locale';
|
||||||
|
import { PluginBlockTemplateClient } from '..';
|
||||||
|
|
||||||
|
export const useEditActionProps = () => {
|
||||||
|
const { setVisible } = useActionContext();
|
||||||
|
const { message } = AntdApp.useApp();
|
||||||
|
const form = useForm();
|
||||||
|
const resource = useDataBlockResource();
|
||||||
|
const collection = useCollection();
|
||||||
|
const plugin = usePlugin(PluginBlockTemplateClient);
|
||||||
|
const t = useT();
|
||||||
|
const { refresh } = useDataBlockRequest();
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'primary',
|
||||||
|
async onClick() {
|
||||||
|
await form.submit();
|
||||||
|
const values = form.values;
|
||||||
|
await resource.update({
|
||||||
|
values,
|
||||||
|
filterByTk: values[collection.filterTargetKey],
|
||||||
|
});
|
||||||
|
refresh();
|
||||||
|
message.success(t('Saved successfully'));
|
||||||
|
plugin.templateInfos.set(values[collection.filterTargetKey], values);
|
||||||
|
setVisible(false);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* 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 { createForm } from '@formily/core';
|
||||||
|
import { useCollectionRecordData } from '@nocobase/client';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
export const useEditFormProps = () => {
|
||||||
|
const recordData = useCollectionRecordData();
|
||||||
|
const form = useMemo(
|
||||||
|
() =>
|
||||||
|
createForm({
|
||||||
|
initialValues: recordData,
|
||||||
|
}),
|
||||||
|
[recordData],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
form,
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,23 @@
|
|||||||
|
import { useFieldSchema } from '@formily/react';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A custom hook that determines if the current field schema is part of a template block
|
||||||
|
*
|
||||||
|
* @param includeRoot - When true, returns true for both root and child template elements.
|
||||||
|
* When false, only returns true for child template elements.
|
||||||
|
* Defaults to true.
|
||||||
|
* @returns boolean - Returns true if the field is part of a template block, considering the includeRoot parameter
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Check if component is in any template block (including root)
|
||||||
|
* const isInTemplate = useIsInTemplate();
|
||||||
|
*
|
||||||
|
* // Check if component is in template block but not the root
|
||||||
|
* const isInTemplateNotRoot = useIsInTemplate(false);
|
||||||
|
*/
|
||||||
|
export const useIsInTemplate = (includeRoot = true) => {
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const templateBlock = _.get(fieldSchema, 'x-template-uid');
|
||||||
|
return !!templateBlock && (includeRoot || !_.get(fieldSchema, 'x-template-root-uid'));
|
||||||
|
};
|
@ -0,0 +1,173 @@
|
|||||||
|
/**
|
||||||
|
* 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 { Plugin } from '@nocobase/client';
|
||||||
|
import { templateBlockInitializerItem } from './initializers';
|
||||||
|
import { NAMESPACE } from './constants';
|
||||||
|
import { BlockTemplateList, BlockTemplatePage } from './components';
|
||||||
|
import { ISchema, Schema } from '@formily/json-schema';
|
||||||
|
import * as _ from 'lodash';
|
||||||
|
import { revertSettingItem } from './settings/revertSetting';
|
||||||
|
import { getFullSchema } from './utils/template';
|
||||||
|
import { registerTemplateBlockInterceptors } from './utils/interceptors';
|
||||||
|
import { TemplateGridDecorator } from './components/TemplateGridDecorator';
|
||||||
|
import PluginMobileClient from '@nocobase/plugin-mobile/client';
|
||||||
|
import { BlockTemplateMobilePage } from './components/BlockTemplateMobilePage';
|
||||||
|
import {
|
||||||
|
hideBlocksFromTemplate,
|
||||||
|
hideConnectDataBlocksFromTemplate,
|
||||||
|
hideConvertToBlockSettingItem,
|
||||||
|
hideDeleteSettingItem,
|
||||||
|
} from './utils/setting';
|
||||||
|
import { BlockTemplateMenusProvider } from './components/BlockTemplateMenusProvider';
|
||||||
|
import { disabledDeleteSettingItem } from './settings/disabledDeleteSetting';
|
||||||
|
|
||||||
|
export class PluginBlockTemplateClient extends Plugin {
|
||||||
|
templateInfos = new Map();
|
||||||
|
templateschemacache = {};
|
||||||
|
pageBlocks = {};
|
||||||
|
savedSchemaUids = new Set<string>();
|
||||||
|
injectInitializers = [
|
||||||
|
'page:addBlock',
|
||||||
|
'popup:common:addBlock',
|
||||||
|
'popup:addNew:addBlock',
|
||||||
|
'mobile:addBlock',
|
||||||
|
'mobile:popup:common:addBlock',
|
||||||
|
];
|
||||||
|
|
||||||
|
async afterAdd() {
|
||||||
|
// await this.app.pm.add()
|
||||||
|
}
|
||||||
|
|
||||||
|
async beforeLoad() {}
|
||||||
|
|
||||||
|
async load() {
|
||||||
|
Schema.registerPatches((s: ISchema) => {
|
||||||
|
if (s['x-template-infos']) {
|
||||||
|
for (const key in s['x-template-infos']) {
|
||||||
|
const templateInfo = s['x-template-infos'][key];
|
||||||
|
this.templateInfos.set(key, templateInfo);
|
||||||
|
}
|
||||||
|
delete s['x-template-infos'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s['x-template-schemas']) {
|
||||||
|
for (const key in s['x-template-schemas']) {
|
||||||
|
const templateSchema = s['x-template-schemas'][key];
|
||||||
|
this.templateschemacache[key] = templateSchema;
|
||||||
|
}
|
||||||
|
delete s['x-template-schemas'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// add version check here is to avoid modifying the template schema before insertAdjacent api
|
||||||
|
// otherwise, the template root schema will be a full copy of the original schema
|
||||||
|
if (s['x-template-root-uid'] && (s['version'] || s['x-template-root-ref'])) {
|
||||||
|
const sc = getFullSchema(s, this.templateschemacache, this.templateInfos, this.savedSchemaUids);
|
||||||
|
this.pageBlocks[sc['x-uid']] = sc;
|
||||||
|
return sc;
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register axios interceptors for template block operations
|
||||||
|
registerTemplateBlockInterceptors(this.app.apiClient, this.pageBlocks, this.savedSchemaUids);
|
||||||
|
|
||||||
|
this.app.addComponents({ TemplateGridDecorator });
|
||||||
|
this.app.addProviders([BlockTemplateMenusProvider]);
|
||||||
|
|
||||||
|
for (const initializer of this.injectInitializers) {
|
||||||
|
this.app.schemaInitializerManager.addItem(initializer, 'otherBlocks.templates', templateBlockInitializerItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#afterAllPluginsLoaded();
|
||||||
|
this.app.pluginSettingsManager.add('block-templates', {
|
||||||
|
title: `{{t("Block templates", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
icon: 'ProfileOutlined',
|
||||||
|
Component: BlockTemplateList,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.app.pluginSettingsManager.add(`block-templates/:key`, {
|
||||||
|
title: false,
|
||||||
|
pluginKey: 'block-templates',
|
||||||
|
isTopLevel: false,
|
||||||
|
Component: BlockTemplatePage,
|
||||||
|
});
|
||||||
|
|
||||||
|
// add mobile router
|
||||||
|
this.app.pluginManager.get<PluginMobileClient>('mobile')?.mobileRouter?.add('mobile.schema.blockTemplate', {
|
||||||
|
path: `/block-templates/:key/:pageSchemaUid`,
|
||||||
|
Component: BlockTemplateMobilePage,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
isInBlockTemplateConfigPage() {
|
||||||
|
const mobilePath = this.app.pluginManager.get<PluginMobileClient>('mobile')?.mobileBasename + '/block-templates';
|
||||||
|
const desktopPath = 'admin/settings/block-templates';
|
||||||
|
return window.location.pathname.includes(desktopPath) || window.location.pathname.includes(mobilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
setTemplateCache = (schema?: ISchema) => {
|
||||||
|
if (!schema) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.templateschemacache[schema['x-uid']] = schema;
|
||||||
|
};
|
||||||
|
|
||||||
|
clearTemplateCache = (templateRootUid: string) => {
|
||||||
|
delete this.templateschemacache[templateRootUid];
|
||||||
|
};
|
||||||
|
|
||||||
|
#afterAllPluginsLoaded = () => {
|
||||||
|
// Check if this.app.loading is true every 1s
|
||||||
|
// If true, wait 1s and check again
|
||||||
|
// If false, stop checking and add template settings
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
if (!this.app.loading) {
|
||||||
|
clearInterval(interval);
|
||||||
|
|
||||||
|
hideBlocksFromTemplate(this.injectInitializers, this.app);
|
||||||
|
// add template settings
|
||||||
|
const schemaSettings = this.app.schemaSettingsManager.getAll();
|
||||||
|
for (const key in schemaSettings) {
|
||||||
|
const schemaSetting = this.app.schemaSettingsManager.get(key);
|
||||||
|
// if not filter out fieldSettings:component:, we will show two revert setting item
|
||||||
|
if (schemaSetting && !key.startsWith('fieldSettings:component:')) {
|
||||||
|
for (let i = 0; i < schemaSetting.items.length; i++) {
|
||||||
|
// hide convert to block setting item
|
||||||
|
hideConvertToBlockSettingItem(
|
||||||
|
schemaSetting.items[i],
|
||||||
|
schemaSetting.items[i - 1],
|
||||||
|
schemaSetting.items[i + 1],
|
||||||
|
);
|
||||||
|
// hide connect data blocks setting item from template configure page
|
||||||
|
hideConnectDataBlocksFromTemplate(schemaSetting.items[i]);
|
||||||
|
// hide delete setting item
|
||||||
|
hideDeleteSettingItem(schemaSetting.items[i], schemaSetting.items[i - 1]);
|
||||||
|
}
|
||||||
|
const deleteItemIndex = schemaSetting.items.findIndex((item, index) => {
|
||||||
|
const nextItem = schemaSetting.items[index + 1];
|
||||||
|
return item['type'] === 'divider' && (nextItem?.name === 'delete' || nextItem?.name === 'remove');
|
||||||
|
});
|
||||||
|
if (
|
||||||
|
deleteItemIndex !== -1 &&
|
||||||
|
!schemaSetting.items.find((item) => item.name === 'template-revertSettingItem')
|
||||||
|
) {
|
||||||
|
schemaSetting.items.splice(deleteItemIndex, 0, revertSettingItem);
|
||||||
|
} else {
|
||||||
|
schemaSetting.add('template-revertSettingItem', revertSettingItem);
|
||||||
|
}
|
||||||
|
schemaSetting.add('template-disabledDeleteItem', disabledDeleteSettingItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PluginBlockTemplateClient;
|
@ -0,0 +1,467 @@
|
|||||||
|
/**
|
||||||
|
* 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 { SchemaInitializerItem, usePlugin, ISchema, useSchemaInitializer } from '@nocobase/client';
|
||||||
|
import React, { useState, useRef, useEffect } from 'react';
|
||||||
|
import { CopyOutlined, LoadingOutlined } from '@ant-design/icons';
|
||||||
|
import { Input, Divider, Empty } from 'antd';
|
||||||
|
import * as _ from 'lodash';
|
||||||
|
import { uid } from '@nocobase/utils/client';
|
||||||
|
import PluginBlockTemplateClient from '..';
|
||||||
|
import { useT } from '../locale';
|
||||||
|
import { useBlockTemplateMenus } from '../components/BlockTemplateMenusProvider';
|
||||||
|
import { useMemoizedFn } from 'ahooks';
|
||||||
|
import { findBlockRootSchema } from '../utils/schema';
|
||||||
|
|
||||||
|
export function convertTplBlock(
|
||||||
|
tpl,
|
||||||
|
virtual = false,
|
||||||
|
isRoot = true,
|
||||||
|
newRootId?: string,
|
||||||
|
templateKey?: string,
|
||||||
|
options?: any,
|
||||||
|
) {
|
||||||
|
if (!newRootId) {
|
||||||
|
newRootId = uid();
|
||||||
|
}
|
||||||
|
// 如果是Grid, Grid.Row, Grid.Col, 则复制一份
|
||||||
|
if (tpl['x-component'] === 'Grid' || tpl['x-component'] === 'Grid.Row' || tpl['x-component'] === 'Grid.Col') {
|
||||||
|
const newSchema = _.cloneDeep({
|
||||||
|
...tpl,
|
||||||
|
'x-uid': uid(), // 生成一个新的uid
|
||||||
|
properties: {},
|
||||||
|
});
|
||||||
|
if (virtual) {
|
||||||
|
newSchema['x-virtual'] = true;
|
||||||
|
}
|
||||||
|
if (newSchema['x-decorator'] === 'TemplateGridDecorator') {
|
||||||
|
delete newSchema['x-decorator'];
|
||||||
|
}
|
||||||
|
for (const key in tpl.properties) {
|
||||||
|
const t = convertTplBlock(tpl.properties[key], virtual, isRoot, newRootId, templateKey, options);
|
||||||
|
if (isRoot) {
|
||||||
|
newRootId = uid(); // 多个区块支持,每个Grid.Row都要生成一个新的uid
|
||||||
|
}
|
||||||
|
if (t) {
|
||||||
|
newSchema.properties[key] = t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newSchema;
|
||||||
|
} else {
|
||||||
|
const newSchema = {
|
||||||
|
// ...tpl,
|
||||||
|
// 'x-component': rootComponent === tpl ? 'XTemplate' : tpl['x-component'],
|
||||||
|
// 'x-decorator': tpl['x-decorator'],
|
||||||
|
// type: rootComponent ? 'void' : tpl.type,
|
||||||
|
// name: tpl.name,
|
||||||
|
'x-uid': `${newRootId}-${tpl['x-uid']}`,
|
||||||
|
'x-template-uid': tpl['x-uid'],
|
||||||
|
properties: {},
|
||||||
|
};
|
||||||
|
if (virtual) {
|
||||||
|
newSchema['x-virtual'] = true;
|
||||||
|
}
|
||||||
|
if (tpl['x-settings']) {
|
||||||
|
newSchema['x-settings'] = tpl['x-settings'];
|
||||||
|
}
|
||||||
|
if (isRoot) {
|
||||||
|
newSchema['x-template-root-uid'] = tpl['x-uid'];
|
||||||
|
newSchema['x-uid'] = newRootId;
|
||||||
|
newSchema['x-template-version'] = '1.0';
|
||||||
|
}
|
||||||
|
|
||||||
|
blockKeepProps.forEach((prop) => {
|
||||||
|
if (_.hasIn(tpl, prop)) {
|
||||||
|
_.set(newSchema, prop, _.get(tpl, prop));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (templateKey) {
|
||||||
|
newSchema['x-block-template-key'] = templateKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
// custom request action will saved in other schema
|
||||||
|
if (tpl['x-component'] === 'CustomRequestAction') {
|
||||||
|
newSchema['x-custom-request-id'] = tpl['x-custom-request-id'] || tpl['x-uid'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// association field will saved in other schema
|
||||||
|
if (tpl['x-component'] === 'Action' && _.get(tpl, 'x-action-settings.schemaUid')) {
|
||||||
|
newSchema['x-action-settings'] = {
|
||||||
|
schemaUid: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tpl['x-component']) {
|
||||||
|
newSchema['x-no-component'] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// filter should be in tpl
|
||||||
|
if (_.get(tpl, 'x-filter-targets')) {
|
||||||
|
newSchema['x-filter-targets'] = tpl['x-filter-targets'];
|
||||||
|
}
|
||||||
|
for (const key in tpl.properties) {
|
||||||
|
newSchema.properties[key] = convertTplBlock(tpl.properties[key], virtual, false, newRootId, templateKey);
|
||||||
|
}
|
||||||
|
if (isRoot && options) {
|
||||||
|
schemaPatch(newSchema, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
return newSchema;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const blockKeepProps = [
|
||||||
|
'x-decorator',
|
||||||
|
'x-decorator-props.collection',
|
||||||
|
'x-decorator-props.association',
|
||||||
|
'x-decorator-props.dataSource',
|
||||||
|
'x-decorator-props.action',
|
||||||
|
'x-decorator-props.params',
|
||||||
|
'x-acl-action',
|
||||||
|
'x-settings',
|
||||||
|
'x-use-decorator-props',
|
||||||
|
'x-is-current',
|
||||||
|
];
|
||||||
|
|
||||||
|
export function formSchemaPatch(currentSchema: ISchema, options?: any) {
|
||||||
|
const { collectionName, dataSourceName, association, currentRecord } = options;
|
||||||
|
|
||||||
|
if (currentRecord) {
|
||||||
|
currentSchema['x-decorator-props'] = {
|
||||||
|
action: 'get',
|
||||||
|
collection: collectionName,
|
||||||
|
association: association,
|
||||||
|
dataSource: dataSourceName,
|
||||||
|
};
|
||||||
|
currentSchema['x-data-templates'] = {
|
||||||
|
display: false,
|
||||||
|
};
|
||||||
|
currentSchema['x-acl-action'] = `${association || collectionName}:update`;
|
||||||
|
currentSchema['x-settings'] = 'blockSettings:editForm';
|
||||||
|
currentSchema['x-use-decorator-props'] = 'useEditFormBlockDecoratorProps';
|
||||||
|
currentSchema['x-is-current'] = true;
|
||||||
|
|
||||||
|
const comKey = Object.keys(currentSchema.properties)[0];
|
||||||
|
if (comKey) {
|
||||||
|
const actionKey = Object.keys(currentSchema['properties'][comKey]['properties']).find((key) => {
|
||||||
|
return key !== 'grid';
|
||||||
|
});
|
||||||
|
if (actionKey) {
|
||||||
|
_.set(currentSchema, `properties.${comKey}.x-use-component-props`, 'useEditFormBlockProps');
|
||||||
|
_.set(currentSchema, `properties.${comKey}.properties.${actionKey}.x-initializer`, 'editForm:configureActions');
|
||||||
|
|
||||||
|
const actionBarSchema = _.get(currentSchema, `properties.${comKey}.properties.${actionKey}.properties`, {});
|
||||||
|
for (const key in actionBarSchema) {
|
||||||
|
if (actionBarSchema[key]['x-settings']?.includes('createSubmit')) {
|
||||||
|
actionBarSchema[key]['x-settings'] = 'actionSettings:updateSubmit';
|
||||||
|
if (actionBarSchema[key]['x-use-component-props'] !== 'useStepsFormSubmitActionProps') {
|
||||||
|
actionBarSchema[key]['x-use-component-props'] = 'useUpdateActionProps';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
currentSchema['x-decorator-props'] = {
|
||||||
|
collection: collectionName,
|
||||||
|
association: association,
|
||||||
|
dataSource: dataSourceName,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function detailsSchemaPatch(currentSchema: ISchema, options?: any) {
|
||||||
|
const { collectionName, dataSourceName, association, currentRecord, associationType } = options;
|
||||||
|
currentSchema['x-decorator-props'] = {
|
||||||
|
action: 'list',
|
||||||
|
collection: association ? null : collectionName,
|
||||||
|
association: association,
|
||||||
|
dataSource: dataSourceName,
|
||||||
|
readPretty: true,
|
||||||
|
params: {
|
||||||
|
pageSize: 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
currentSchema['x-acl-action'] = `${association || collectionName}:view`; //currentSchema['x-acl-action'].replace(':get', ':view');
|
||||||
|
currentSchema['x-settings'] = 'blockSettings:detailsWithPagination';
|
||||||
|
currentSchema['x-use-decorator-props'] = 'useDetailsWithPaginationDecoratorProps';
|
||||||
|
|
||||||
|
if (currentRecord || associationType === 'hasOne' || associationType === 'belongsTo') {
|
||||||
|
currentSchema['x-acl-action'] = `${association || collectionName}:get`;
|
||||||
|
currentSchema['x-decorator-props']['action'] = 'get';
|
||||||
|
currentSchema['x-settings'] = 'blockSettings:details';
|
||||||
|
currentSchema['x-use-decorator-props'] = 'useDetailsDecoratorProps';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentRecord) {
|
||||||
|
currentSchema['x-is-current'] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function nestedSchemaPatch(currentSchema: ISchema) {
|
||||||
|
// Handle child blocks recursively
|
||||||
|
if (currentSchema.properties && currentSchema['x-decorator-props']?.association) {
|
||||||
|
const processChildBlock = (schema: ISchema, parentAssociation?: string) => {
|
||||||
|
const decoratorName = schema['x-decorator'];
|
||||||
|
|
||||||
|
// If this is a DetailsBlockProvider or FormBlockProvider
|
||||||
|
if (decoratorName === 'DetailsBlockProvider' || decoratorName === 'FormBlockProvider') {
|
||||||
|
if (!schema['x-decorator-props']?.association && parentAssociation) {
|
||||||
|
const settings = schema['x-settings'];
|
||||||
|
if (settings === 'blockSettings:editForm' || settings === 'blockSettings:details') {
|
||||||
|
schema['x-decorator-props'].association = parentAssociation;
|
||||||
|
schema['x-is-current'] = true;
|
||||||
|
schema['x-acl-action'] = `${parentAssociation}:${schema['x-acl-action']?.split(':')[1]}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const decoratorProps = schema['x-decorator-props'];
|
||||||
|
if (decoratorProps && decoratorProps.collection && !decoratorProps.association) {
|
||||||
|
// the association is not set in parent datablock provider, so we don't need to process down
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the current block's association for its children
|
||||||
|
const currentAssociation = decoratorProps?.association || parentAssociation;
|
||||||
|
|
||||||
|
// Process children recursively
|
||||||
|
if (schema.properties) {
|
||||||
|
Object.values(schema.properties).forEach((childSchema) => {
|
||||||
|
processChildBlock(childSchema, currentAssociation);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start processing from the root's immediate children
|
||||||
|
Object.values(currentSchema.properties).forEach((childSchema) => {
|
||||||
|
processChildBlock(childSchema, currentSchema['x-decorator-props']?.association);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function schemaPatch(currentSchema: ISchema, options?: any) {
|
||||||
|
const { collectionName, dataSourceName, association } = options;
|
||||||
|
|
||||||
|
const decoratorName = currentSchema['x-decorator'];
|
||||||
|
if (decoratorName === 'DetailsBlockProvider') {
|
||||||
|
detailsSchemaPatch(currentSchema, options);
|
||||||
|
} else if (decoratorName === 'FormBlockProvider') {
|
||||||
|
formSchemaPatch(currentSchema, options);
|
||||||
|
} else if (decoratorName) {
|
||||||
|
currentSchema['x-decorator-props'] = {
|
||||||
|
action: 'list',
|
||||||
|
collection: collectionName,
|
||||||
|
association: association,
|
||||||
|
dataSource: dataSourceName,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
nestedSchemaPatch(currentSchema);
|
||||||
|
return currentSchema;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSchemaUidMaps(schema, idMap = {}) {
|
||||||
|
if (schema['x-template-uid']) {
|
||||||
|
idMap[schema['x-template-uid']] = schema['x-uid'];
|
||||||
|
}
|
||||||
|
if (schema.properties) {
|
||||||
|
for (const key in schema.properties) {
|
||||||
|
getSchemaUidMaps(schema.properties[key], idMap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return idMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
function correctIdReference(schema, idMaps) {
|
||||||
|
const skipReplaceKeys = ['x-uid', 'x-template-uid', 'x-template-root-uid', 'x-custom-request-id'];
|
||||||
|
for (const key in schema) {
|
||||||
|
if (!skipReplaceKeys.includes(key)) {
|
||||||
|
if (schema[key] && typeof schema[key] === 'string') {
|
||||||
|
schema[key] = idMaps[schema[key]] || schema[key];
|
||||||
|
}
|
||||||
|
if (schema[key] && typeof schema[key] === 'object') {
|
||||||
|
correctIdReference(schema[key], idMaps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function correctIdReferences(schemas) {
|
||||||
|
const idMaps = {};
|
||||||
|
for (const schema of schemas) {
|
||||||
|
_.merge(idMaps, getSchemaUidMaps(schema));
|
||||||
|
}
|
||||||
|
for (const schema of schemas) {
|
||||||
|
correctIdReference(schema, idMaps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertTemplateToBlock(data, templateKey?: string, options?: any) {
|
||||||
|
// debugger;
|
||||||
|
let tpls = data?.properties; // Grid开始的区块
|
||||||
|
tpls = _.get(Object.values(tpls), '0.properties'); // Grid.Row开始的区块
|
||||||
|
const schemas = [];
|
||||||
|
// 遍历 tpl的所有属性,每一个属性其实是一个区块
|
||||||
|
for (const key in tpls) {
|
||||||
|
const tpl = tpls[key];
|
||||||
|
const schema = convertTplBlock(tpl, false, true, undefined, templateKey, options);
|
||||||
|
if (schema) {
|
||||||
|
schemas.push(findBlockRootSchema(schema));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return schemas;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SearchInput = ({ value: outValue, onChange }) => {
|
||||||
|
const [value, setValue] = useState<string>(outValue);
|
||||||
|
const inputRef = useRef<any>('');
|
||||||
|
const compositionRef = useRef<boolean>(false);
|
||||||
|
const t = useT();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setValue(outValue);
|
||||||
|
}, [outValue]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const focusInput = () => {
|
||||||
|
if (inputRef.current) {
|
||||||
|
inputRef.current.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver((entries) => {
|
||||||
|
if (entries.some((v) => v.isIntersecting)) {
|
||||||
|
focusInput();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (inputRef.current?.input) {
|
||||||
|
observer.observe(inputRef.current.input);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observer.disconnect();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (!compositionRef.current) {
|
||||||
|
onChange(e.target.value);
|
||||||
|
setValue(e.target.value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleComposition = (e: React.CompositionEvent<HTMLInputElement> | any) => {
|
||||||
|
if (e.type === 'compositionend') {
|
||||||
|
compositionRef.current = false;
|
||||||
|
handleChange(e);
|
||||||
|
} else {
|
||||||
|
compositionRef.current = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div onClick={(e) => e.stopPropagation()}>
|
||||||
|
<Input
|
||||||
|
ref={inputRef}
|
||||||
|
allowClear
|
||||||
|
style={{ padding: '4px 8px', boxShadow: 'none', borderRadius: 0 }}
|
||||||
|
bordered={false}
|
||||||
|
placeholder={t('Search and select template')}
|
||||||
|
value={value}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
onChange={handleChange}
|
||||||
|
onCompositionStart={handleComposition}
|
||||||
|
onCompositionEnd={handleComposition}
|
||||||
|
onCompositionUpdate={handleComposition}
|
||||||
|
/>
|
||||||
|
<Divider style={{ margin: 0 }} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TemplateBlockInitializer = () => {
|
||||||
|
const { insert } = useSchemaInitializer();
|
||||||
|
const plugin = usePlugin(PluginBlockTemplateClient);
|
||||||
|
const [searchValue, setSearchValue] = useState('');
|
||||||
|
const t = useT();
|
||||||
|
const { templates, handleTemplateClick, loading } = useBlockTemplateMenus();
|
||||||
|
|
||||||
|
const filteredData = templates
|
||||||
|
?.filter((item) => !item.dataSource)
|
||||||
|
.filter((item) => !searchValue || item.title.toLowerCase().includes(searchValue.toLowerCase()));
|
||||||
|
|
||||||
|
const menuItems = [
|
||||||
|
{
|
||||||
|
key: 'search',
|
||||||
|
label: (
|
||||||
|
<SearchInput
|
||||||
|
value={searchValue}
|
||||||
|
onChange={(val: string) => {
|
||||||
|
setSearchValue(val);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
onClick: (e) => {
|
||||||
|
e.domEvent.stopPropagation();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...(filteredData?.length
|
||||||
|
? filteredData.map((item) => ({
|
||||||
|
label: item.title,
|
||||||
|
...item,
|
||||||
|
}))
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
key: 'empty',
|
||||||
|
style: {
|
||||||
|
height: 150,
|
||||||
|
},
|
||||||
|
label: (
|
||||||
|
<div onClick={(e) => e.stopPropagation()}>
|
||||||
|
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={t('No data')} />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
templates?.forEach((item) => {
|
||||||
|
plugin.templateInfos.set(item.key, item);
|
||||||
|
});
|
||||||
|
}, [templates, plugin.templateInfos]);
|
||||||
|
|
||||||
|
const onClick = useMemoizedFn((item) => {
|
||||||
|
handleTemplateClick(item, {}, insert);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<LoadingOutlined /> {t('Templates')}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SchemaInitializerItem
|
||||||
|
closeInitializerMenuWhenClick={true}
|
||||||
|
title={'{{t("Block template")}}'}
|
||||||
|
icon={<CopyOutlined style={{ marginRight: 0 }} />}
|
||||||
|
items={menuItems}
|
||||||
|
name={'templates'}
|
||||||
|
onClick={onClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,11 @@
|
|||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from './TemplateBlockInitializer';
|
||||||
|
export * from './templateBlockInitializerItem';
|
@ -0,0 +1,21 @@
|
|||||||
|
/**
|
||||||
|
* 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 { SchemaInitializerItemTypeWithoutName, usePlugin } from '@nocobase/client';
|
||||||
|
import { TemplateBlockInitializer } from './TemplateBlockInitializer';
|
||||||
|
|
||||||
|
export const templateBlockInitializerItem: SchemaInitializerItemTypeWithoutName = {
|
||||||
|
name: 'templates',
|
||||||
|
Component: TemplateBlockInitializer,
|
||||||
|
title: '{{t("Block template")}}',
|
||||||
|
icon: 'TableOutlined',
|
||||||
|
// // sort: -1,
|
||||||
|
wrap: (t) => t,
|
||||||
|
useVisible: () => false,
|
||||||
|
};
|
@ -0,0 +1,21 @@
|
|||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
import pkg from '../../package.json';
|
||||||
|
import { useApp } from '@nocobase/client';
|
||||||
|
|
||||||
|
export function useT() {
|
||||||
|
const app = useApp();
|
||||||
|
return (str: string) => app.i18n.t(str, { ns: [pkg.name, 'client'] });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function tStr(key: string) {
|
||||||
|
return `{{t(${JSON.stringify(key)}, { ns: ['${pkg.name}', 'client'], nsMode: 'fallback' })}}`;
|
||||||
|
}
|
@ -0,0 +1,280 @@
|
|||||||
|
/**
|
||||||
|
* 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 } from '@nocobase/client';
|
||||||
|
import { uid } from '@nocobase/utils/client';
|
||||||
|
import { blockTemplatesCollection } from '../collections/blockTemplates';
|
||||||
|
import { createActionSchema } from './createActionSchema';
|
||||||
|
import { ConfigureLink } from '../components/ConfigureLink';
|
||||||
|
import { editActionSchema } from './editActionSchema';
|
||||||
|
import { NAMESPACE } from '../constants';
|
||||||
|
import { tStr } from '../locale';
|
||||||
|
import { bulkDestroySchema } from './bulkDestroySchema';
|
||||||
|
import { AddNewTemplate } from '../components/AddNewTemplate';
|
||||||
|
|
||||||
|
export const blockTemplatesSchema: ISchema = {
|
||||||
|
type: 'void',
|
||||||
|
name: uid(),
|
||||||
|
'x-component': 'CardItem',
|
||||||
|
'x-decorator': 'TableBlockProvider',
|
||||||
|
'x-decorator-props': {
|
||||||
|
collection: blockTemplatesCollection.name,
|
||||||
|
action: 'list',
|
||||||
|
params: {
|
||||||
|
sort: '-createdAt',
|
||||||
|
},
|
||||||
|
showIndex: true,
|
||||||
|
dragSort: false,
|
||||||
|
rowKey: 'key',
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
actions: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'ActionBar',
|
||||||
|
'x-component-props': {
|
||||||
|
style: {
|
||||||
|
marginBottom: 20,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
filter: {
|
||||||
|
type: 'void',
|
||||||
|
title: '{{ t("Filter") }}',
|
||||||
|
default: {
|
||||||
|
$and: [{ title: { $includes: '' } }],
|
||||||
|
},
|
||||||
|
'x-action': 'filter',
|
||||||
|
'x-component': 'Filter.Action',
|
||||||
|
'x-use-component-props': 'useFilterActionProps',
|
||||||
|
'x-component-props': {
|
||||||
|
icon: 'FilterOutlined',
|
||||||
|
},
|
||||||
|
'x-align': 'left',
|
||||||
|
},
|
||||||
|
refresh: {
|
||||||
|
type: 'void',
|
||||||
|
title: '{{ t("Refresh") }}',
|
||||||
|
'x-component': 'Action',
|
||||||
|
'x-use-component-props': 'useRefreshActionProps',
|
||||||
|
'x-component-props': {
|
||||||
|
icon: 'ReloadOutlined',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
bulkDestroySchema,
|
||||||
|
addNew: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': AddNewTemplate,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
table: {
|
||||||
|
type: 'array',
|
||||||
|
'x-component': 'TableV2',
|
||||||
|
'x-use-component-props': 'useTableBlockProps',
|
||||||
|
'x-component-props': {
|
||||||
|
rowKey: blockTemplatesCollection.filterTargetKey,
|
||||||
|
rowSelection: {
|
||||||
|
type: 'checkbox',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
title: {
|
||||||
|
type: 'void',
|
||||||
|
title: '{{ t("Title") }}',
|
||||||
|
'x-component': 'TableV2.Column',
|
||||||
|
'x-component-props': {
|
||||||
|
width: 150,
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
title: {
|
||||||
|
type: 'string',
|
||||||
|
'x-component': 'CollectionField',
|
||||||
|
'x-read-pretty': true,
|
||||||
|
'x-component-props': {
|
||||||
|
ellipsis: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
key: {
|
||||||
|
type: 'void',
|
||||||
|
title: '{{ t("Name") }}',
|
||||||
|
'x-component': 'TableV2.Column',
|
||||||
|
'x-component-props': {
|
||||||
|
width: 80,
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
key: {
|
||||||
|
type: 'string',
|
||||||
|
'x-component': 'CollectionField',
|
||||||
|
'x-read-pretty': true,
|
||||||
|
'x-component-props': {
|
||||||
|
ellipsis: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: 'void',
|
||||||
|
title: tStr('Type'),
|
||||||
|
'x-component': 'TableV2.Column',
|
||||||
|
'x-component-props': {
|
||||||
|
width: 80,
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
type: {
|
||||||
|
type: 'string',
|
||||||
|
'x-component': 'CollectionField',
|
||||||
|
'x-read-pretty': true,
|
||||||
|
'x-component-props': {
|
||||||
|
ellipsis: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: 'void',
|
||||||
|
title: '{{ t("Description") }}',
|
||||||
|
'x-component': 'TableV2.Column',
|
||||||
|
properties: {
|
||||||
|
description: {
|
||||||
|
type: 'string',
|
||||||
|
'x-component': 'CollectionField',
|
||||||
|
'x-read-pretty': true,
|
||||||
|
'x-component-props': {
|
||||||
|
ellipsis: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
type: 'void',
|
||||||
|
title: '{{ t("Actions") }}',
|
||||||
|
'x-component': 'TableV2.Column',
|
||||||
|
properties: {
|
||||||
|
actions: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Space',
|
||||||
|
'x-component-props': {
|
||||||
|
split: '|',
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
configure: {
|
||||||
|
type: 'void',
|
||||||
|
title: tStr('Configure'),
|
||||||
|
'x-component': ConfigureLink,
|
||||||
|
},
|
||||||
|
editActionSchema,
|
||||||
|
duplicate: {
|
||||||
|
type: 'void',
|
||||||
|
title: `{{t("Duplicate", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
'x-component': 'Action.Link',
|
||||||
|
'x-component-props': {
|
||||||
|
openSize: 'small',
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
modal: {
|
||||||
|
type: 'void',
|
||||||
|
title: `{{t("Duplicate to new template", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
'x-decorator': 'FormV2',
|
||||||
|
'x-component': 'Action.Modal',
|
||||||
|
properties: {
|
||||||
|
title: {
|
||||||
|
type: 'string',
|
||||||
|
title: '{{t("Title")}}',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Input',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Action.Modal.Footer',
|
||||||
|
properties: {
|
||||||
|
submit: {
|
||||||
|
type: 'void',
|
||||||
|
title: '{{t("Submit")}}',
|
||||||
|
'x-component': 'Action',
|
||||||
|
'x-component-props': {
|
||||||
|
type: 'primary',
|
||||||
|
useAction: '{{ useDuplicateAction }}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cancel: {
|
||||||
|
type: 'void',
|
||||||
|
title: '{{t("Cancel")}}',
|
||||||
|
'x-component': 'Action',
|
||||||
|
'x-component-props': {
|
||||||
|
useAction: '{{ cm.useCancelAction }}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
delete: {
|
||||||
|
type: 'void',
|
||||||
|
title: tStr('Delete'),
|
||||||
|
'x-component': 'Action.Link',
|
||||||
|
'x-component-props': {
|
||||||
|
openSize: 'small',
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
modal: {
|
||||||
|
type: 'void',
|
||||||
|
title: `{{t("Delete", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
'x-decorator': 'FormV2',
|
||||||
|
'x-component': 'Action.Modal',
|
||||||
|
'x-component-props': {
|
||||||
|
openSize: 'small',
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
keepBlocks: {
|
||||||
|
type: 'boolean',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Checkbox',
|
||||||
|
default: true,
|
||||||
|
'x-content': tStr('Keep the created blocks?'),
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Action.Modal.Footer',
|
||||||
|
properties: {
|
||||||
|
submit: {
|
||||||
|
type: 'void',
|
||||||
|
title: '{{t("Submit")}}',
|
||||||
|
'x-component': 'Action',
|
||||||
|
'x-component-props': {
|
||||||
|
type: 'primary',
|
||||||
|
useAction: '{{ useDeleteAction }}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cancel: {
|
||||||
|
type: 'void',
|
||||||
|
title: '{{t("Cancel")}}',
|
||||||
|
'x-component': 'Action',
|
||||||
|
'x-component-props': {
|
||||||
|
useAction: '{{ cm.useCancelAction }}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
@ -0,0 +1,67 @@
|
|||||||
|
/**
|
||||||
|
* 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 { useMemo } from 'react';
|
||||||
|
import { tStr } from '../locale';
|
||||||
|
import { createForm } from '@formily/core';
|
||||||
|
|
||||||
|
export const bulkDestroySchema = {
|
||||||
|
name: 'bulkDestroySchema',
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Action',
|
||||||
|
'x-component-props': {
|
||||||
|
openSize: 'small',
|
||||||
|
icon: 'DeleteOutlined',
|
||||||
|
},
|
||||||
|
title: '{{t("Delete")}}',
|
||||||
|
properties: {
|
||||||
|
modal: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Action.Modal',
|
||||||
|
'x-decorator': 'FormV2',
|
||||||
|
'x-use-decorator-props': function useCreateFormProps() {
|
||||||
|
const form = useMemo(() => createForm(), []);
|
||||||
|
return { form };
|
||||||
|
},
|
||||||
|
title: '{{t("Delete")}}',
|
||||||
|
properties: {
|
||||||
|
keepBlocks: {
|
||||||
|
type: 'boolean',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Checkbox',
|
||||||
|
'x-content': tStr('Keep the created blocks?'),
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Action.Modal.Footer',
|
||||||
|
properties: {
|
||||||
|
cancel: {
|
||||||
|
type: 'void',
|
||||||
|
title: '{{t("Cancel")}}',
|
||||||
|
'x-component': 'Action',
|
||||||
|
'x-component-props': {
|
||||||
|
useAction: '{{ cm.useCancelAction }}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
submit: {
|
||||||
|
type: 'void',
|
||||||
|
title: '{{t("Submit")}}',
|
||||||
|
'x-component': 'Action',
|
||||||
|
'x-component-props': {
|
||||||
|
type: 'primary',
|
||||||
|
useAction: '{{ useBulkDestroyAction }}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
@ -0,0 +1,77 @@
|
|||||||
|
/**
|
||||||
|
* 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 { NAMESPACE } from '../constants';
|
||||||
|
import { useContext } from 'react';
|
||||||
|
import { NewTemplateFormContext } from '../components/AddNewTemplate';
|
||||||
|
|
||||||
|
export const createActionSchema = {
|
||||||
|
type: 'object',
|
||||||
|
'x-component': 'Action',
|
||||||
|
title: `{{t("Add new", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
'x-align': 'right',
|
||||||
|
'x-component-props': {
|
||||||
|
type: 'primary',
|
||||||
|
icon: 'PlusOutlined',
|
||||||
|
openMode: 'drawer',
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
drawer: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Action.Drawer',
|
||||||
|
title: `{{t("Add new", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
'x-decorator': 'FormV2',
|
||||||
|
'x-use-decorator-props': function useCreateFormProps() {
|
||||||
|
const form = useContext(NewTemplateFormContext);
|
||||||
|
return { form };
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
form: {
|
||||||
|
type: 'void',
|
||||||
|
properties: {
|
||||||
|
title: {
|
||||||
|
type: 'string',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'CollectionField',
|
||||||
|
},
|
||||||
|
key: {
|
||||||
|
type: 'string',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'CollectionField',
|
||||||
|
'x-validator': 'uid',
|
||||||
|
description:
|
||||||
|
"{{t('Randomly generated and can be modified. Support letters, numbers and underscores, must start with an letter.')}}",
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: 'string',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'CollectionField',
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: 'string',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'CollectionField',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Action.Drawer.Footer',
|
||||||
|
properties: {
|
||||||
|
submit: {
|
||||||
|
title: 'Submit',
|
||||||
|
'x-component': 'Action',
|
||||||
|
'x-use-component-props': 'useCreateActionProps',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
@ -0,0 +1,69 @@
|
|||||||
|
/**
|
||||||
|
* 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 { tStr } from '../locale';
|
||||||
|
|
||||||
|
export const editActionSchema = {
|
||||||
|
type: 'void',
|
||||||
|
title: tStr('Edit'),
|
||||||
|
'x-component': 'Action.Link',
|
||||||
|
'x-component-props': {
|
||||||
|
openMode: 'drawer',
|
||||||
|
icon: 'EditOutlined',
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
drawer: {
|
||||||
|
type: 'void',
|
||||||
|
title: tStr('Edit'),
|
||||||
|
'x-component': 'Action.Drawer',
|
||||||
|
'x-decorator': 'FormV2',
|
||||||
|
'x-use-decorator-props': 'useEditFormProps',
|
||||||
|
properties: {
|
||||||
|
form: {
|
||||||
|
type: 'void',
|
||||||
|
properties: {
|
||||||
|
title: {
|
||||||
|
type: 'string',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'CollectionField',
|
||||||
|
},
|
||||||
|
key: {
|
||||||
|
type: 'string',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'CollectionField',
|
||||||
|
'x-disabled': true,
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: 'string',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'CollectionField',
|
||||||
|
'x-disabled': true,
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: 'string',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'CollectionField',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Action.Drawer.Footer',
|
||||||
|
properties: {
|
||||||
|
submit: {
|
||||||
|
title: tStr('Submit'),
|
||||||
|
'x-component': 'Action',
|
||||||
|
'x-use-component-props': 'useEditActionProps',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from './blockTemplates';
|
||||||
|
export * from './createActionSchema';
|
||||||
|
export * from './editActionSchema';
|
||||||
|
export * from './bulkDestroySchema';
|
@ -0,0 +1,19 @@
|
|||||||
|
/**
|
||||||
|
* 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 { useIsInTemplate } from '../hooks/useIsInTemplate';
|
||||||
|
import { DisabledDeleteItem } from '../components/DisabledDeleteItem';
|
||||||
|
import { tStr } from '../locale';
|
||||||
|
|
||||||
|
export const disabledDeleteSettingItem = {
|
||||||
|
name: 'template-disabledDeleteItem',
|
||||||
|
title: tStr('Delete'),
|
||||||
|
Component: DisabledDeleteItem,
|
||||||
|
useVisible: () => useIsInTemplate(false),
|
||||||
|
};
|
@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* 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 _ from 'lodash';
|
||||||
|
import { RevertSetting } from '../components/RevertSetting';
|
||||||
|
import { tStr } from '../locale';
|
||||||
|
import { useIsInTemplate } from '../hooks/useIsInTemplate';
|
||||||
|
|
||||||
|
export const revertSettingItem = {
|
||||||
|
name: 'template-revertSettingItem',
|
||||||
|
title: tStr('Revert to template'),
|
||||||
|
Component: RevertSetting,
|
||||||
|
useVisible: () => {
|
||||||
|
const fieldSchema = useFieldSchema();
|
||||||
|
const isInTemplate = useIsInTemplate();
|
||||||
|
// in steps form, the schema is not the one saved in server side, so we need to hide the revert setting item
|
||||||
|
return isInTemplate && fieldSchema['x-settings'] !== 'settings:stepsFormStepTitleSettings';
|
||||||
|
},
|
||||||
|
};
|
@ -0,0 +1,128 @@
|
|||||||
|
/**
|
||||||
|
* 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 { AxiosRequestConfig } from 'axios';
|
||||||
|
import {
|
||||||
|
findSchemaCache,
|
||||||
|
findFirstVirtualSchema,
|
||||||
|
convertToCreateSchema,
|
||||||
|
collectSchemaFirstVirtualUids,
|
||||||
|
findSchemaByUid,
|
||||||
|
} from './template';
|
||||||
|
import { ISchema } from '@nocobase/client';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register template block related interceptors for axios
|
||||||
|
* Handles schema removal and patching operations for template blocks
|
||||||
|
* @param apiClient The API client instance
|
||||||
|
* @param pageBlocks The template blocks cache
|
||||||
|
*/
|
||||||
|
export function registerTemplateBlockInterceptors(
|
||||||
|
apiClient: any,
|
||||||
|
pageBlocks: Record<string, any>,
|
||||||
|
savedSchemaUids: Set<string>,
|
||||||
|
) {
|
||||||
|
const setToTrueSchema = (uid: string) => {
|
||||||
|
const cacheSchema = findSchemaCache(pageBlocks, uid);
|
||||||
|
const deleteVirtual = (schema: ISchema) => {
|
||||||
|
if (schema?.['x-virtual']) {
|
||||||
|
savedSchemaUids.add(schema['x-uid']);
|
||||||
|
}
|
||||||
|
if (schema?.properties) {
|
||||||
|
for (const key in schema.properties) {
|
||||||
|
deleteVirtual(schema.properties[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const schema = findSchemaByUid(cacheSchema, uid);
|
||||||
|
deleteVirtual(schema);
|
||||||
|
};
|
||||||
|
|
||||||
|
apiClient.axios.interceptors.request.use(async (config: AxiosRequestConfig) => {
|
||||||
|
// Handle schema patching
|
||||||
|
if (config.url?.includes('uiSchemas:patch') || config.url?.includes('uiSchemas:initializeActionContext')) {
|
||||||
|
const xUid = config.data?.['x-uid'];
|
||||||
|
const currentPageSchema = findSchemaCache(pageBlocks, xUid);
|
||||||
|
const currentSchema = findSchemaByUid(currentPageSchema, xUid);
|
||||||
|
const uids = collectSchemaFirstVirtualUids(currentSchema);
|
||||||
|
for (const uid of uids) {
|
||||||
|
const virtualSchema = findFirstVirtualSchema(currentPageSchema, uid);
|
||||||
|
if (virtualSchema) {
|
||||||
|
const newSchema = convertToCreateSchema(virtualSchema.schema);
|
||||||
|
await apiClient.request({
|
||||||
|
url: `/blockTemplates:saveSchema/${virtualSchema.insertTarget}?position=${virtualSchema.insertPosition}`,
|
||||||
|
method: 'post',
|
||||||
|
data: {
|
||||||
|
schema: newSchema,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setToTrueSchema(virtualSchema.schema['x-uid']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle schema batch patch
|
||||||
|
if (config.url?.includes('uiSchemas:batchPatch')) {
|
||||||
|
const schemas = config.data;
|
||||||
|
for (const schema of schemas) {
|
||||||
|
const currentSchema = findSchemaCache(pageBlocks, schema['x-uid']);
|
||||||
|
const virtualSchema = findFirstVirtualSchema(currentSchema, schema['x-uid']);
|
||||||
|
if (virtualSchema) {
|
||||||
|
await apiClient.request({
|
||||||
|
url: `/blockTemplates:saveSchema/${virtualSchema.insertTarget}?position=${virtualSchema.insertPosition}`,
|
||||||
|
method: 'post',
|
||||||
|
data: {
|
||||||
|
schema: convertToCreateSchema(virtualSchema.schema),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setToTrueSchema(virtualSchema.schema['x-uid']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.url?.includes('uiSchemas:insertAdjacent')) {
|
||||||
|
const uidWithQuery = config.url.split('/').pop();
|
||||||
|
const wrap = config.data?.wrap;
|
||||||
|
const schema = config.data?.schema;
|
||||||
|
const uid = uidWithQuery?.split('?')[0];
|
||||||
|
const schemaId = schema['x-uid'] || schema;
|
||||||
|
const currentSchema = findSchemaCache(pageBlocks, uid);
|
||||||
|
const virtualSchema = findFirstVirtualSchema(currentSchema, uid, wrap);
|
||||||
|
|
||||||
|
if (virtualSchema) {
|
||||||
|
await apiClient.request({
|
||||||
|
url: `/blockTemplates:saveSchema/${virtualSchema.insertTarget}?position=${virtualSchema.insertPosition}`,
|
||||||
|
method: 'post',
|
||||||
|
data: {
|
||||||
|
schema: convertToCreateSchema(virtualSchema.schema, [schemaId, wrap?.['x-uid']].filter(Boolean)),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setToTrueSchema(virtualSchema.schema['x-uid']);
|
||||||
|
}
|
||||||
|
const cs = findSchemaCache(pageBlocks, schemaId);
|
||||||
|
const vs = findFirstVirtualSchema(cs, schemaId, wrap);
|
||||||
|
if (vs && vs.insertTarget) {
|
||||||
|
await apiClient.request({
|
||||||
|
url: `/blockTemplates:saveSchema/${vs.insertTarget}?position=${vs.insertPosition}`,
|
||||||
|
method: 'post',
|
||||||
|
data: {
|
||||||
|
schema: convertToCreateSchema(vs.schema, [schemaId, wrap?.['x-uid']].filter(Boolean)),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setToTrueSchema(vs.schema['x-uid']);
|
||||||
|
if (wrap) {
|
||||||
|
setToTrueSchema(wrap['x-uid']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
});
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user