diff --git a/packages/core/client/src/i18n/constant.ts b/packages/core/client/src/i18n/constant.ts new file mode 100644 index 0000000000..e8dc4848b9 --- /dev/null +++ b/packages/core/client/src/i18n/constant.ts @@ -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. + */ + +const NAMESPACE_UI_SCHEMA = 'ui-schema-storage'; + +export { NAMESPACE_UI_SCHEMA }; diff --git a/packages/core/client/src/i18n/index.ts b/packages/core/client/src/i18n/index.ts index 5d83484458..4ca37cc1c6 100644 --- a/packages/core/client/src/i18n/index.ts +++ b/packages/core/client/src/i18n/index.ts @@ -8,3 +8,4 @@ */ export * from './i18n'; +export * from './constant'; diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/schemaSettings2.test.ts b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/schemaSettings2.test.ts index a3a6a6290e..7550324146 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/schemaSettings2.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-create/schemaSettings2.test.ts @@ -37,8 +37,8 @@ test.describe('linkage rules', () => { await page.getByText('Add condition', { exact: true }).click(); await page.getByTestId('select-filter-field').click(); await page.getByRole('menuitemcheckbox', { name: 'singleLineText' }).click(); - await page.getByLabel('Linkage rules').locator('input[type="text"]').click(); - await page.getByLabel('Linkage rules').locator('input[type="text"]').fill('123'); + await page.getByLabel('Linkage rules').getByRole('tabpanel').getByRole('textbox').click(); + await page.getByLabel('Linkage rules').getByRole('tabpanel').getByRole('textbox').fill('123'); // action:禁用 longText 字段 await page.getByText('Add property').click(); diff --git a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/schemaInitializer.test.ts b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/schemaInitializer.test.ts index cf11928201..7db78e37dc 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/schemaInitializer.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/form/__e2e__/form-edit/schemaInitializer.test.ts @@ -85,6 +85,7 @@ test.describe('configure fields', () => { await page.getByRole('menuitem', { name: 'manyToOne2 right' }).hover(); await page.getByRole('menuitem', { name: 'manyToOne3' }).click(); await page.mouse.move(600, 0); + await page.reload(); await expect(page.getByLabel('block-item-CollectionField-general-form-general.manyToOne1-manyToOne1')).toHaveText( `manyToOne1:${record.manyToOne1.id}`, diff --git a/packages/core/client/src/modules/blocks/data-blocks/list/__e2e__/schemaInitializer.test.ts b/packages/core/client/src/modules/blocks/data-blocks/list/__e2e__/schemaInitializer.test.ts index c9b0519b6b..272e2f3007 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/list/__e2e__/schemaInitializer.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/list/__e2e__/schemaInitializer.test.ts @@ -41,6 +41,7 @@ test.describe('where list block can be added', () => { await page.getByLabel('schema-initializer-Grid-').nth(1).hover(); await page.getByRole('menuitem', { name: 'Role name' }).click(); await page.mouse.move(300, 0); + await page.reload(); await expect(page.getByLabel('block-item-CollectionField-').getByText('Root')).toBeVisible(); await expect(page.getByLabel('block-item-CollectionField-').getByText('Admin')).toBeVisible(); await expect(page.getByLabel('block-item-CollectionField-').getByText('Member')).toBeVisible(); diff --git a/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/actions/linkage.test.ts b/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/actions/linkage.test.ts index 78a3680d6b..8c2a24c863 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/actions/linkage.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/actions/linkage.test.ts @@ -13,14 +13,16 @@ import { T4334 } from '../templatesOfBug'; // fix https://nocobase.height.app/T-2187 test('action linkage by row data', async ({ page, mockPage }) => { await mockPage(T4334).goto(); - const adminEditAction = page.getByLabel('action-Action.Link-Edit-update-roles-table-admin'); + const adminEditAction = page + .getByLabel('action-Action.Link-Edit-update-roles-table-admin') + .locator('.nb-action-title'); const adminEditActionStyle = await adminEditAction.evaluate((element) => { const computedStyle = window.getComputedStyle(element); return { opacity: computedStyle.opacity, }; }); - const rootEditAction = page.getByLabel('action-Action.Link-Edit-update-roles-table-root'); + const rootEditAction = page.getByLabel('action-Action.Link-Edit-update-roles-table-root').locator('.nb-action-title'); const rootEditActionStyle = await rootEditAction.evaluate((element) => { const computedStyle = window.getComputedStyle(element); return { diff --git a/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/schemaInitializer.test.ts b/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/schemaInitializer.test.ts index bcabdba6a9..3c0d2ab0da 100644 --- a/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/schemaInitializer.test.ts +++ b/packages/core/client/src/modules/blocks/data-blocks/table/__e2e__/schemaInitializer.test.ts @@ -156,6 +156,7 @@ test.describe('configure columns', () => { await page.getByRole('menuitem', { name: 'manyToOne2 right' }).hover(); await page.getByRole('menuitem', { name: 'manyToOne3' }).click(); await page.mouse.move(600, 0); + await page.reload(); // 2. Click on the association field, create a details block in the popup, display the ID field, and assert if it's correct await page @@ -194,6 +195,7 @@ test.describe('configure columns', () => { await page.getByLabel('schema-initializer-Grid-details:configureFields-emptyCollection').hover(); await page.getByRole('menuitem', { name: 'ID', exact: true }).click(); await page.mouse.move(600, 0); + await expect(page.getByLabel('block-item-CollectionField-')).toHaveText( `ID:${record.manyToOne1.manyToOne2.manyToOne3.id}`, ); @@ -212,6 +214,7 @@ test.describe('configure columns', () => { await page.getByRole('menuitem', { name: 'manyToOne2 right' }).hover(); await page.getByRole('menuitem', { name: 'manyToOne3' }).click(); await page.mouse.move(600, 0); + await page.reload(); // 2. 点击每一个关系字段,创建一个详情区块,显示 ID 字段,断言 ID 是否正确 await page @@ -307,8 +310,8 @@ test.describe('configure actions column', () => { await page.getByLabel('designer-schema-initializer-TableV2.Column-TableV2.ActionColumnDesigner-').hover(); await page.getByRole('menuitem', { name: 'Delete' }).click(); - await page.getByText('Actions', { exact: true }).hover({ force: true }); - await page.getByLabel('designer-schema-initializer-TableV2.Column-TableV2.ActionColumnDesigner-').hover(); + // await page.getByText('Actions', { exact: true }).hover({ force: true }); + // await page.getByLabel('designer-schema-initializer-TableV2.Column-TableV2.ActionColumnDesigner-').hover(); await page.getByRole('menuitem', { name: 'Duplicate' }).click(); await page.mouse.move(300, 0); diff --git a/packages/core/client/src/modules/popup/__e2e__/router.test.ts b/packages/core/client/src/modules/popup/__e2e__/router.test.ts index 1d534bc912..753d28d632 100644 --- a/packages/core/client/src/modules/popup/__e2e__/router.test.ts +++ b/packages/core/client/src/modules/popup/__e2e__/router.test.ts @@ -36,7 +36,8 @@ test.describe('popup router', () => { await page.locator('.ant-drawer-mask').click(); // expect to be back to the first page - await page.getByText('Users单层子页面Configure').hover(); + await page.getByLabel('block-item-CardItem-users-').getByText('Users 单层子页面Configure').hover(); + await expect( page.getByRole('button', { name: 'designer-schema-settings-CardItem-blockSettings:table-users' }), ).toBeVisible(); @@ -60,7 +61,7 @@ test.describe('popup router', () => { await page.locator('.ant-drawer-mask').click(); // expect to be back to the first page - await page.getByText('Users单层子页面Configure').hover(); + await page.getByLabel('block-item-CardItem-users-').getByText('Users 单层子页面Configure').hover(); await expect( page.getByRole('button', { name: 'designer-schema-settings-CardItem-blockSettings:table-users' }), ).toBeVisible(); diff --git a/packages/core/client/src/schema-component/antd/action/Action.Drawer.tsx b/packages/core/client/src/schema-component/antd/action/Action.Drawer.tsx index 49d5eaaf60..7ee5fa3a9a 100644 --- a/packages/core/client/src/schema-component/antd/action/Action.Drawer.tsx +++ b/packages/core/client/src/schema-component/antd/action/Action.Drawer.tsx @@ -10,6 +10,7 @@ import { observer, RecursionField, useField, useFieldSchema } from '@formily/react'; import { Drawer } from 'antd'; import classNames from 'classnames'; +import { useTranslation } from 'react-i18next'; import React, { FC, startTransition, useCallback, useEffect, useMemo, useState } from 'react'; import { ErrorBoundary, FallbackProps } from 'react-error-boundary'; import { NocoBaseRecursionField } from '../../../formily/NocoBaseRecursionField'; @@ -22,6 +23,7 @@ import { useActionContext } from './hooks'; import { useSetAriaLabelForDrawer } from './hooks/useSetAriaLabelForDrawer'; import { ActionDrawerProps, ComposedActionDrawer, OpenSize } from './types'; import { getZIndex, useZIndexContext, zIndexContext } from './zIndexContext'; +import { NAMESPACE_UI_SCHEMA } from '../../../i18n/constant'; const MemoizeRecursionField = React.memo(RecursionField); MemoizeRecursionField.displayName = 'MemoizeRecursionField'; @@ -81,6 +83,7 @@ export const InternalActionDrawer: React.FC = observer( const { visible, setVisible, openSize = 'middle', drawerProps } = useActionContext(); const schema = useFieldSchema(); const field = useField(); + const { t } = useTranslation(); const { componentCls, hashId } = useStyles(); const tabContext = useTabsContext(); const parentZIndex = useZIndexContext(); @@ -126,7 +129,7 @@ export const InternalActionDrawer: React.FC = observer( { @@ -584,7 +586,8 @@ const RenderButtonInner = observer( return null; } - const actionTitle = title || field?.title; + const rawTitle = title ?? field?.title; + const actionTitle = typeof rawTitle === 'string' ? t(rawTitle, { ns: NAMESPACE_UI_SCHEMA }) : rawTitle; const { opacity, ...restButtonStyle } = buttonStyle; const linkStyle = isLink && opacity ? { opacity } : undefined; return ( diff --git a/packages/core/client/src/schema-component/antd/block-item/BlockItemCard.tsx b/packages/core/client/src/schema-component/antd/block-item/BlockItemCard.tsx index e23bfb7b10..a6e0e804ca 100644 --- a/packages/core/client/src/schema-component/antd/block-item/BlockItemCard.tsx +++ b/packages/core/client/src/schema-component/antd/block-item/BlockItemCard.tsx @@ -9,8 +9,10 @@ import { Card, CardProps } from 'antd'; import React, { useMemo, useRef, useEffect, createContext, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { useToken } from '../../../style'; import { MarkdownReadPretty } from '../markdown'; +import { NAMESPACE_UI_SCHEMA } from '../../../i18n/constant'; export const BlockItemCardContext = createContext({}); @@ -22,6 +24,7 @@ export const BlockItemCard = React.forwardRef(( }, [token.marginBlock]); const [titleHeight, setTitleHeight] = useState(0); const titleRef = useRef(null); + const { t } = useTranslation(); useEffect(() => { const timer = setTimeout(() => { if (titleRef.current) { @@ -38,10 +41,10 @@ export const BlockItemCard = React.forwardRef(( }, [blockTitle, description]); const title = (blockTitle || description) && (
- {blockTitle} + {t(blockTitle, { ns: NAMESPACE_UI_SCHEMA })} {description && ( { addActiveFieldName?.(schema.name as string); }, [addActiveFieldName, schema.name]); - + field.title = t(field.title, { ns: NAMESPACE_UI_SCHEMA }); const showTitle = schema['x-decorator-props']?.showTitle ?? true; const extra = useMemo(() => { if (field.description && field.description !== '') { return typeof field.description === 'string' ? (
'), + __html: HTMLEncode(t(field.description, { ns: NAMESPACE_UI_SCHEMA })) + .split('\n') + .join('
'), }} /> ) : ( - field.description + t(field.description, { ns: NAMESPACE_UI_SCHEMA }) ); } }, [field.description]); diff --git a/packages/core/client/src/schema-component/antd/page/Page.tsx b/packages/core/client/src/schema-component/antd/page/Page.tsx index 5ee775bf93..dfad51a71e 100644 --- a/packages/core/client/src/schema-component/antd/page/Page.tsx +++ b/packages/core/client/src/schema-component/antd/page/Page.tsx @@ -52,6 +52,7 @@ import { useStyles } from './Page.style'; import { PageDesigner, PageTabDesigner } from './PageTabDesigner'; import { PopupRouteContextResetter } from './PopupRouteContextResetter'; import { transformMultiColumnToSingleColumn } from '@nocobase/utils/client'; +import { NAMESPACE_UI_SCHEMA } from '../../../i18n/constant'; interface PageProps { currentTabUid: string; @@ -431,7 +432,8 @@ const NocoBasePageHeader = React.memo(({ activeKey, className }: { activeKey: st const { token } = useToken(); useEffect(() => { - const title = t(fieldSchema.title) || t(currentRoute?.title); + const title = + t(fieldSchema.title, { ns: NAMESPACE_UI_SCHEMA }) || t(currentRoute?.title, { ns: NAMESPACE_UI_SCHEMA }); if (title) { setDocumentTitle(title); setPageTitle(title); diff --git a/packages/core/client/src/schema-component/antd/table-v2/Table.Column.tsx b/packages/core/client/src/schema-component/antd/table-v2/Table.Column.tsx index f0a15c8560..0c70932478 100644 --- a/packages/core/client/src/schema-component/antd/table-v2/Table.Column.tsx +++ b/packages/core/client/src/schema-component/antd/table-v2/Table.Column.tsx @@ -9,8 +9,11 @@ import { useField } from '@formily/react'; import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { NAMESPACE_UI_SCHEMA } from '../../../i18n/constant'; export const TableColumn = (props) => { const field = useField(); - return
{field.title}
; + const { t } = useTranslation(); + return
{t(field.title, { ns: NAMESPACE_UI_SCHEMA })}
; }; diff --git a/packages/core/client/src/schema-component/antd/table-v2/Table.tsx b/packages/core/client/src/schema-component/antd/table-v2/Table.tsx index 6fafba6452..1339c87c13 100644 --- a/packages/core/client/src/schema-component/antd/table-v2/Table.tsx +++ b/packages/core/client/src/schema-component/antd/table-v2/Table.tsx @@ -67,6 +67,7 @@ import { useAssociationFieldContext } from '../association-field/hooks'; import { TableSkeleton } from './TableSkeleton'; import { extractIndex, isCollectionFieldComponent, isColumnComponent } from './utils'; import { withTooltipComponent } from '../../../hoc/withTooltipComponent'; +import { NAMESPACE_UI_SCHEMA } from '../../../i18n/constant'; type BodyRowComponentProps = { rowIndex?: number; @@ -164,6 +165,7 @@ const useTableColumns = ( props: { showDel?: any; isSubTable?: boolean; optimizeTextCellRender: boolean }, paginationProps, ) => { + const { t } = useTranslation(); const { token } = useToken(); const field = useArrayField(props); const schema = useFieldSchema(); @@ -213,11 +215,10 @@ const useTableColumns = ( const dataIndex = collectionFields?.length > 0 ? collectionFields[0].name : columnSchema.name; const columnHidden = !!columnSchema['x-component-props']?.['columnHidden']; const { uiSchema, defaultValue, interface: _interface } = collection?.getField(dataIndex) || {}; - + columnSchema.title = t(columnSchema?.title, { ns: NAMESPACE_UI_SCHEMA }); if (uiSchema) { uiSchema.default = defaultValue; } - return { title: ( diff --git a/packages/core/client/src/schema-component/antd/tabs/Tabs.tsx b/packages/core/client/src/schema-component/antd/tabs/Tabs.tsx index 444b1d2824..2d0a9a4a3d 100644 --- a/packages/core/client/src/schema-component/antd/tabs/Tabs.tsx +++ b/packages/core/client/src/schema-component/antd/tabs/Tabs.tsx @@ -12,6 +12,7 @@ import { observer, RecursionField, Schema, useField, useFieldSchema } from '@for import { Tabs as AntdTabs, TabPaneProps, TabsProps } from 'antd'; import classNames from 'classnames'; import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; import { useSchemaInitializerRender } from '../../../application'; import { withDynamicSchemaProps } from '../../../hoc/withDynamicSchemaProps'; import { Icon } from '../../../icon'; @@ -22,6 +23,7 @@ import { useTabsContext } from './context'; import { TabsDesigner } from './Tabs.Designer'; import { useMobileLayout } from '../../../route-switch/antd/admin-layout'; import { transformMultiColumnToSingleColumn } from '@nocobase/utils/client'; +import { NAMESPACE_UI_SCHEMA } from '../../../i18n/constant'; const MemoizeRecursionField = React.memo(RecursionField); MemoizeRecursionField.displayName = 'MemoizeRecursionField'; @@ -136,14 +138,15 @@ Tabs.TabPane = withDynamicSchemaProps( (props: TabPaneProps & { icon?: any; hidden?: boolean }) => { const Designer = useDesigner(); const field = useField(); - + const { t } = useTranslation(); if (props.hidden) { return null; } return ( - {props.icon && } {props.tab || field.title} + {props.icon && }{' '} + {props.tab || t(field.title, { ns: NAMESPACE_UI_SCHEMA })} ); diff --git a/packages/plugins/@nocobase/plugin-action-bulk-update/src/client/__e2e__/schemaSettings.test.ts b/packages/plugins/@nocobase/plugin-action-bulk-update/src/client/__e2e__/schemaSettings.test.ts index 73bd22caac..c4cbfa3a5e 100644 --- a/packages/plugins/@nocobase/plugin-action-bulk-update/src/client/__e2e__/schemaSettings.test.ts +++ b/packages/plugins/@nocobase/plugin-action-bulk-update/src/client/__e2e__/schemaSettings.test.ts @@ -37,8 +37,8 @@ test.describe('data will be updated && Assign field values && after successful s await page.getByRole('menuitem', { name: 'After successful submission' }).click(); await page.getByLabel('Manually close').check(); await page.getByLabel('Redirect to').check(); - await page.locator('input[type="text"]').click(); - await page.locator('input[type="text"]').fill('/admin/pm/list/local/'); + await page.getByLabel('textbox').click(); + await page.getByLabel('textbox').fill('/admin/pm/list/local/'); await page.getByRole('button', { name: 'OK', exact: true }).click(); await page.getByLabel('action-Action-Bulk update-customize:bulkUpdate-general-table').click(); const [request] = await Promise.all([ diff --git a/packages/plugins/@nocobase/plugin-action-custom-request/src/client/__e2e__/schemaSettings.test.ts b/packages/plugins/@nocobase/plugin-action-custom-request/src/client/__e2e__/schemaSettings.test.ts index 8a2b0756ed..13c03555de 100644 --- a/packages/plugins/@nocobase/plugin-action-custom-request/src/client/__e2e__/schemaSettings.test.ts +++ b/packages/plugins/@nocobase/plugin-action-custom-request/src/client/__e2e__/schemaSettings.test.ts @@ -25,9 +25,7 @@ test.describe('custom request action', () => { await page.getByLabel('designer-schema-settings-CustomRequestAction-actionSettings:customRequest-').hover(); await page.getByRole('menuitem', { name: 'Edit button' }).click(); - // 应该只显示标题输入框 await expect(page.getByText('Button title')).toBeVisible(); - await expect(page.getByText('Button icon')).not.toBeVisible(); await expect(page.getByText('Button background color')).not.toBeVisible(); }); diff --git a/packages/plugins/@nocobase/plugin-block-workbench/src/client/WorkbenchAction.tsx b/packages/plugins/@nocobase/plugin-block-workbench/src/client/WorkbenchAction.tsx index b82a04c872..56814c760d 100644 --- a/packages/plugins/@nocobase/plugin-block-workbench/src/client/WorkbenchAction.tsx +++ b/packages/plugins/@nocobase/plugin-block-workbench/src/client/WorkbenchAction.tsx @@ -8,10 +8,18 @@ */ import { useFieldSchema } from '@formily/react'; -import { Action, Icon, useCompile, useComponent, withDynamicSchemaProps, ACLActionProvider } from '@nocobase/client'; +import { + Action, + Icon, + useComponent, + withDynamicSchemaProps, + ACLActionProvider, + NAMESPACE_UI_SCHEMA, +} from '@nocobase/client'; import { Avatar } from 'antd'; import { createStyles } from 'antd-style'; import React, { useContext } from 'react'; +import { useTranslation } from 'react-i18next'; import { WorkbenchBlockContext } from './WorkbenchBlock'; import { WorkbenchLayout } from './workbenchBlockSettings'; @@ -40,8 +48,8 @@ function Button() { const backgroundColor = fieldSchema['x-component-props']?.['iconColor']; const { layout, ellipsis = true } = useContext(WorkbenchBlockContext); const { styles, cx } = useStyles(); - const compile = useCompile(); - const title = compile(fieldSchema.title); + const { t } = useTranslation(); + const title = t(fieldSchema.title, { ns: NAMESPACE_UI_SCHEMA }); return layout === WorkbenchLayout.Grid ? (
} /> diff --git a/packages/plugins/@nocobase/plugin-ui-schema-storage/src/server/repository.ts b/packages/plugins/@nocobase/plugin-ui-schema-storage/src/server/repository.ts index f13461d128..17a3821d72 100644 --- a/packages/plugins/@nocobase/plugin-ui-schema-storage/src/server/repository.ts +++ b/packages/plugins/@nocobase/plugin-ui-schema-storage/src/server/repository.ts @@ -10,7 +10,7 @@ import { Cache } from '@nocobase/cache'; import { Repository, Transaction, Transactionable } from '@nocobase/database'; import { uid } from '@nocobase/utils'; -import lodash from 'lodash'; +import { default as _, default as lodash } from 'lodash'; import { ChildOptions, SchemaNode, TargetPosition } from './dao/ui_schema_node_dao'; export interface GetJsonSchemaOptions { @@ -297,6 +297,23 @@ export class UiSchemaRepository extends Repository { ); } + async emitAfterSaveEvent(s, options) { + if (!s?.schema) { + return; + } + const keys = ['title', 'description', 'x-component-props.title', 'x-decorator-props.title']; + let r = false; + for (const key of keys) { + if (_.get(s?.schema, key)) { + r = true; + break; + } + } + if (r) { + await this.database.emitAsync(`${this.collection.name}.afterSave`, s, options); + } + } + @transaction() async patch(newSchema: any, options?) { const { transaction } = options; @@ -305,8 +322,8 @@ export class UiSchemaRepository extends Repository { if (!newSchema['properties']) { const s = await this.model.findByPk(rootUid, { transaction }); s.set('schema', { ...s.toJSON(), ...newSchema }); - // console.log(s.toJSON()); await s.save({ transaction, hooks: false }); + await this.emitAfterSaveEvent(s, options); if (newSchema['x-server-hooks']) { await this.database.emitAsync(`${this.collection.name}.afterSave`, s, options); } @@ -488,8 +505,14 @@ export class UiSchemaRepository extends Repository { } const result = await this[`insert${lodash.upperFirst(position)}`](target, schema, options); + + const s = await this.model.findByPk(schema, { transaction }); + + await this.emitAfterSaveEvent(s, options); + // clear target schema path cache await this.clearXUidPathCache(result['x-uid'], transaction); + return result; } @@ -869,6 +892,8 @@ WHERE TreeTable.depth = 1 AND TreeTable.ancestor = :ancestor and TreeTable.sort }, ); + await this.emitAfterSaveEvent(nodeModel, { transaction }); + if (schema['x-server-hooks']) { await this.database.emitAsync(`${this.collection.name}.afterSave`, nodeModel, { transaction }); } diff --git a/packages/plugins/@nocobase/plugin-ui-schema-storage/src/server/server.ts b/packages/plugins/@nocobase/plugin-ui-schema-storage/src/server/server.ts index 1ad9a34e0a..befe842063 100644 --- a/packages/plugins/@nocobase/plugin-ui-schema-storage/src/server/server.ts +++ b/packages/plugins/@nocobase/plugin-ui-schema-storage/src/server/server.ts @@ -9,6 +9,8 @@ import { MagicAttributeModel } from '@nocobase/database'; import { Plugin } from '@nocobase/server'; +import PluginLocalizationServer from '@nocobase/plugin-localization'; +import { tval } from '@nocobase/utils'; import { uid } from '@nocobase/utils'; import path, { resolve } from 'path'; import { uiSchemaActions } from './actions/ui-schema-action'; @@ -17,6 +19,19 @@ import UiSchemaRepository from './repository'; import { ServerHooks } from './server-hooks'; import { ServerHookModel } from './server-hooks/model'; +export const compile = (title: string) => (title || '').replace(/{{\s*t\(["|'|`](.*)["|'|`]\)\s*}}/g, '$1'); + +function extractFields(obj) { + return [ + obj.title, + obj.description, + obj['x-component-props']?.title, + obj['x-component-props']?.description, + obj['x-decorator-props']?.title, + obj['x-decorator-props']?.description, + ].filter((value) => value !== undefined && value !== ''); +} + export class PluginUISchemaStorageServer extends Plugin { serverHooks: ServerHooks; @@ -28,7 +43,7 @@ export class PluginUISchemaStorageServer extends Plugin { async beforeLoad() { const db = this.app.db; - + const pm = this.app.pm; this.serverHooks = new ServerHooks(db); this.app.db.registerModels({ MagicAttributeModel, UiSchemaModel, ServerHookModel }); @@ -51,6 +66,19 @@ export class PluginUISchemaStorageServer extends Plugin { } }); + db.on('uiSchemas.afterSave', async function setUid(model, options) { + const localizationPlugin = pm.get('localization') as PluginLocalizationServer; + const texts = []; + const changedFields = extractFields(model.toJSON()); + if (!changedFields.length) { + return; + } + changedFields.forEach((field) => { + field && texts.push({ text: compile(field), module: `resources.ui-schema-storage` }); + }); + await localizationPlugin?.addNewTexts?.(texts, options); + }); + db.on('uiSchemas.afterCreate', async function insertSchema(model, options) { const { transaction } = options; const uiSchemaRepository = db.getCollection('uiSchemas').repository as UiSchemaRepository; @@ -125,6 +153,34 @@ export class PluginUISchemaStorageServer extends Plugin { ]); await this.importCollections(resolve(__dirname, 'collections')); + // this.registerLocalizationSource(); + } + + registerLocalizationSource() { + const localizationPlugin = this.app.pm.get('localization') as PluginLocalizationServer; + if (!localizationPlugin) { + return; + } + localizationPlugin.sourceManager.registerSource('ui-schema-storage', { + title: tval('UiSchema'), + sync: async (ctx) => { + const uiSchemas = await ctx.db.getRepository('uiSchemas').find({ + raw: true, + }); + const resources = {}; + uiSchemas.forEach((route: { schema?: any }) => { + const changedFields = extractFields(route.schema); + if (changedFields.length) { + changedFields.forEach((field) => { + resources[field] = ''; + }); + } + }); + return { + 'ui-schema-storage': resources, + }; + }, + }); } }