diff --git a/apps/tego/src/plugin-presets.ts b/apps/tego/src/plugin-presets.ts index 6f8d464d50..d53fa6166f 100644 --- a/apps/tego/src/plugin-presets.ts +++ b/apps/tego/src/plugin-presets.ts @@ -15,12 +15,12 @@ export class PluginPresets extends Plugin { 'error-handler', 'event-source', 'file', + 'web', 'workflow', 'message', 'pdf', 'ui-schema', 'user', - 'web', 'worker-thread', 'env-secrets', ]; diff --git a/package.json b/package.json index cc3dced317..9e64af93ee 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,9 @@ "oxlint --fix" ] }, + "dependencies": { + "ts-morph": "^26.0.0" + }, "devDependencies": { "@commitlint/cli": "^19.8.1", "@commitlint/config-conventional": "^19.8.1", diff --git a/packages/client/src/built-in/index.tsx b/packages/client/src/built-in/index.tsx index 5df273e29f..6e806102a7 100644 --- a/packages/client/src/built-in/index.tsx +++ b/packages/client/src/built-in/index.tsx @@ -434,7 +434,6 @@ export class BuiltInPlugin extends Plugin { }, }); await this.app.pm.add(SchemaComponentPlugin, { name: 'schema-component' }); - await this.app.pm.add(SchemaInitializerPlugin, { name: 'schema-initializer' }); await this.app.pm.add(SchemaSettingsPlugin, { name: 'schema-settings' }); await this.app.pm.add(PluginBlockSchemaComponent, { name: 'block-schema-component' }); await this.app.pm.add(AntdSchemaComponentPlugin, { name: 'antd-schema-component' }); diff --git a/packages/client/src/schema-component/antd/action/index.tsx b/packages/client/src/schema-component/antd/action/index.tsx index 4b3240fdf2..9d92485a47 100644 --- a/packages/client/src/schema-component/antd/action/index.tsx +++ b/packages/client/src/schema-component/antd/action/index.tsx @@ -8,3 +8,4 @@ export * from './hooks/useGetAriaLabelOfModal'; export * from './hooks/useGetAriaLabelOfPopover'; export * from './Action.Designer'; export * from './Action.Area'; +export { linkageAction } from './utils'; diff --git a/packages/client/src/schema-component/common/index.ts b/packages/client/src/schema-component/common/index.ts index 3f18bf9717..dc3394a2cc 100644 --- a/packages/client/src/schema-component/common/index.ts +++ b/packages/client/src/schema-component/common/index.ts @@ -1,2 +1,3 @@ export * from './dnd-context'; export * from './sortable-item'; +export { parseVariables } from './utils'; diff --git a/packages/client/src/schema-initializer/components/assigned-field/AssignedField.tsx b/packages/client/src/schema-initializer/components/assigned-field/AssignedField.tsx index 7e31365a44..f93467c7c2 100644 --- a/packages/client/src/schema-initializer/components/assigned-field/AssignedField.tsx +++ b/packages/client/src/schema-initializer/components/assigned-field/AssignedField.tsx @@ -13,7 +13,7 @@ import { import { CollectionFieldProvider } from '../../../data-source'; import { useRecord } from '../../../record-provider'; import { useCompile, useComponent } from '../../../schema-component'; -import { Option } from '../../../schema-settings/VariableInput/type'; +import { VariableInputOption } from '../../../schema-settings/VariableInput/type'; import { formatVariableScop } from '../../../schema-settings/VariableInput/utils/formatVariableScop'; import { getShouldChange, VariableInput } from '../../../schema-settings/VariableInput/VariableInput'; import { useLocalVariables, useVariables } from '../../../variables'; @@ -112,7 +112,7 @@ export const AssignedField = (props: AssignedFieldProps) => { ); const returnScope = useCallback( - (scope: Option[]) => { + (scope: VariableInputOption[]) => { const currentForm = scope.find((item) => item.value === '$nForm'); const fields = getCollectionFields(name); diff --git a/packages/client/src/schema-settings/SchemaSettingsDefaultValue.tsx b/packages/client/src/schema-settings/SchemaSettingsDefaultValue.tsx index 91cdb1c5e1..b2c4d4af6f 100644 --- a/packages/client/src/schema-settings/SchemaSettingsDefaultValue.tsx +++ b/packages/client/src/schema-settings/SchemaSettingsDefaultValue.tsx @@ -21,7 +21,7 @@ import { FlagProvider, useFlag } from '../flag-provider'; import { useLocalVariables, useVariables } from '../variables'; import { isVariable } from '../variables/utils/isVariable'; import { findParentFieldSchema, getFieldDefaultValue, SchemaSettingsModalItem } from './SchemaSettings'; -import { Option } from './VariableInput/type'; +import { VariableInputOption } from './VariableInput/type'; import { formatVariableScop } from './VariableInput/utils/formatVariableScop'; import { getShouldChange, VariableInput } from './VariableInput/VariableInput'; @@ -77,7 +77,7 @@ export const SchemaSettingsDefaultValue = function DefaultValueConfigure(props: (parentCollectionField?.type === 'hasMany' && collectionField?.interface === 'm2o')); const returnScope = useCallback( - (scope: Option[]) => { + (scope: VariableInputOption[]) => { const currentForm = scope.find((item) => item.value === '$nForm'); const fields = getCollectionFields(name); @@ -114,7 +114,7 @@ export const SchemaSettingsDefaultValue = function DefaultValueConfigure(props: 'x-decorator': 'FormItem', 'x-component': 'VariableInput', 'x-component-props': { - ...(fieldSchema?.['x-component-props'] || {}), + ...fieldSchema?.['x-component-props'], collectionField, contextCollectionName: isAllowContextVariable && tableCtx.collection, schema: collectionField?.uiSchema, @@ -155,7 +155,7 @@ export const SchemaSettingsDefaultValue = function DefaultValueConfigure(props: } const schema = { - ...(s || {}), + ...s, 'x-decorator': 'FormItem', 'x-component-props': { ...s['x-component-props'], diff --git a/packages/client/src/schema-settings/VariableInput/VariableInput.tsx b/packages/client/src/schema-settings/VariableInput/VariableInput.tsx index 65e7956dd7..55d05cfce8 100644 --- a/packages/client/src/schema-settings/VariableInput/VariableInput.tsx +++ b/packages/client/src/schema-settings/VariableInput/VariableInput.tsx @@ -14,7 +14,7 @@ import { useContextAssociationFields } from './hooks/useContextAssociationFields import { useCurrentRecordVariable } from './hooks/useRecordVariable'; import { useCurrentUserVariable } from './hooks/useUserVariable'; import { useVariableOptions } from './hooks/useVariableOptions'; -import { Option } from './type'; +import { VariableInputOption } from './type'; interface GetShouldChangeProps { collectionField: CollectionFieldOptions_deprecated; @@ -57,7 +57,7 @@ type Props = { * @param scope * @returns */ - returnScope?: (scope: Option[]) => any[]; + returnScope?: (scope: VariableInputOption[]) => any[]; }; /** @@ -243,14 +243,14 @@ export function useCompatOldVariables(props: { }); const compatOldVariables = useCallback( - (variables: Option[], { value }) => { + (variables: VariableInputOption[], { value }) => { if (!isVariable(value)) { return variables; } variables = _.cloneDeep(variables); - const systemVariable: Option = { + const systemVariable: VariableInputOption = { value: '$system', key: '$system', label: t('System variables'), diff --git a/packages/client/src/schema-settings/VariableInput/hooks/useBaseVariable.tsx b/packages/client/src/schema-settings/VariableInput/hooks/useBaseVariable.tsx index 4003fbe08a..48c737e06b 100644 --- a/packages/client/src/schema-settings/VariableInput/hooks/useBaseVariable.tsx +++ b/packages/client/src/schema-settings/VariableInput/hooks/useBaseVariable.tsx @@ -4,7 +4,7 @@ import { ISchema, Schema } from '@tachybase/schema'; import { CollectionFieldOptions_deprecated, useCollectionManager_deprecated } from '../../../collection-manager'; import { useCompile, useGetFilterOptions } from '../../../schema-component'; import { isSpecialCaseField } from '../../../schema-component/antd/form-item/hooks/useSpecialCase'; -import { FieldOption, Option } from '../type'; +import { FieldOption, VariableInputOption } from '../type'; export interface IsDisabledParams { option: FieldOption; @@ -26,7 +26,7 @@ interface GetOptionsParams { * 不需要禁用选项,一般会在表达式中使用 */ noDisabled?: boolean; - loadChildren?: (option: Option) => Promise; + loadChildren?: (option: VariableInputOption) => Promise; compile: (value: string) => any; isDisabled?: (params: IsDisabledParams) => boolean; getCollectionField?: (name: string) => CollectionFieldOptions_deprecated; @@ -59,7 +59,7 @@ interface BaseProps { * @param fields * @param option */ - returnFields?(fields: FieldOption[], option: Option): FieldOption[]; + returnFields?(fields: FieldOption[], option: VariableInputOption): FieldOption[]; dataSource?: string; } @@ -91,9 +91,9 @@ const getChildren = ( targetFieldSchema, getCollectionField, }: GetOptionsParams, -): Option[] => { +): VariableInputOption[] => { const result = options - .map((option): Option => { + .map((option): VariableInputOption => { if (!option.target) { return { key: option.name, @@ -148,7 +148,7 @@ export const useBaseVariable = ({ const { isDisabled } = useContext(BaseVariableContext) || {}; const { getCollectionField } = useCollectionManager_deprecated(dataSource); - const loadChildren = (option: Option): Promise => { + const loadChildren = (option: VariableInputOption): Promise => { if (!option.field?.target) { return Promise.resolve(void 0); } @@ -217,7 +217,7 @@ export const useBaseVariable = ({ depth: 0, loadChildren, children: [], - } as Option; + } as VariableInputOption; }, [uiSchema?.['x-component']]); return result; diff --git a/packages/client/src/schema-settings/VariableInput/hooks/useContextAssociationFields.tsx b/packages/client/src/schema-settings/VariableInput/hooks/useContextAssociationFields.tsx index 10eee6b757..63e36ae9ea 100644 --- a/packages/client/src/schema-settings/VariableInput/hooks/useContextAssociationFields.tsx +++ b/packages/client/src/schema-settings/VariableInput/hooks/useContextAssociationFields.tsx @@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next'; import { CollectionFieldOptions_deprecated, useCollectionManager_deprecated } from '../../../collection-manager'; import { useCompile, useGetFilterOptions } from '../../../schema-component'; -import { FieldOption, Option } from '../type'; +import { FieldOption, VariableInputOption } from '../type'; export const useIsSameOrChildCollection = () => { const { getChildrenCollections } = useCollectionManager_deprecated(); @@ -22,7 +22,7 @@ interface GetOptionsParams { schema: any; depth: number; maxDepth?: number; - loadChildren?: (option: Option) => Promise; + loadChildren?: (option: VariableInputOption) => Promise; compile: (value: string) => any; } @@ -31,9 +31,9 @@ const getChildren = ( { schema, depth, maxDepth, loadChildren, compile }: GetOptionsParams, collectionField, getIsSameOrChildCollection, -): Option[] => { +): VariableInputOption[] => { const result = options - .map((option): Option => { + .map((option): VariableInputOption => { const disabled = !getIsSameOrChildCollection(option.target, collectionField?.target); if (!option.target) { return { @@ -79,7 +79,7 @@ export const useContextAssociationFields = ({ const compile = useCompile(); const getFilterOptions = useGetFilterOptions(); const getIsSameOrChildCollection = useIsSameOrChildCollection(); - const loadChildren = (option: Option): Promise => { + const loadChildren = (option: VariableInputOption): Promise => { if (!option.field?.target) { return new Promise((resolve) => { error('Must be set field target'); @@ -133,7 +133,7 @@ export const useContextAssociationFields = ({ }, depth: 0, loadChildren, - } as Option; + } as VariableInputOption; }, [schema?.['x-component']]); return result; diff --git a/packages/client/src/schema-settings/VariableInput/index.ts b/packages/client/src/schema-settings/VariableInput/index.ts index ff5267fd6d..e6eb7c9554 100644 --- a/packages/client/src/schema-settings/VariableInput/index.ts +++ b/packages/client/src/schema-settings/VariableInput/index.ts @@ -1,2 +1,4 @@ export * from './hooks'; export * from './VariableInput'; +export * from './utils/formatVariableScop'; +export type { VariableInputOption } from './type'; diff --git a/packages/client/src/schema-settings/VariableInput/type.ts b/packages/client/src/schema-settings/VariableInput/type.ts index bbaa584d23..17745b5d67 100644 --- a/packages/client/src/schema-settings/VariableInput/type.ts +++ b/packages/client/src/schema-settings/VariableInput/type.ts @@ -2,17 +2,17 @@ import { Schema } from '@tachybase/schema'; import type { DefaultOptionType } from 'antd/lib/cascader'; -export interface Option extends DefaultOptionType { +export interface VariableInputOption extends DefaultOptionType { key?: string | number; value?: string | number; label: React.ReactNode; disabled?: boolean; - children?: Option[]; + children?: VariableInputOption[]; // 标记是否为叶子节点,设置了 `loadData` 时有效 // 设为 `false` 时会强制标记为父节点,即使当前节点没有 children,也会显示展开图标 isLeaf?: boolean; /** 当开启异步加载时有效,用于加载当前 node 的 children */ - loadChildren?(option: Option): Promise; + loadChildren?(option: VariableInputOption): Promise; field?: FieldOption; depth?: number; } diff --git a/packages/module-web/src/client/index.tsx b/packages/module-web/src/client/index.tsx index b2db18a1c0..524fe3ea47 100644 --- a/packages/module-web/src/client/index.tsx +++ b/packages/module-web/src/client/index.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { createRouterManager, Plugin, RouterManager } from '@tachybase/client'; +import { createRouterManager, Plugin, RouterManager, SchemaInitializerPlugin } from '@tachybase/client'; import { Navigate } from 'react-router-dom'; @@ -25,10 +25,11 @@ export class ModuleWeb extends Plugin { public mobileRouter: RouterManager; async afterAdd() { - this.pm.add(PluginDataSelect, { name: 'plugin-data-select' }); - this.app.pm.add(PluginSearchAndJump, { name: 'SearchAndJump' }); - this.app.pm.add(PluginDesignableButton, { name: 'Designable' }); - this.app.pm.add(PluginCalculator, { name: 'calculator' }); + await this.app.pm.add(SchemaInitializerPlugin, { name: 'schema-initializer' }); + await this.app.pm.add(PluginDataSelect, { name: 'plugin-data-select' }); + await this.app.pm.add(PluginSearchAndJump, { name: 'SearchAndJump' }); + await this.app.pm.add(PluginDesignableButton, { name: 'Designable' }); + await this.app.pm.add(PluginCalculator, { name: 'calculator' }); } async load() { this.setMobileRouter(); diff --git a/packages/module-web/src/client/schema-initializer/SchemaInitializerPlugin.ts b/packages/module-web/src/client/schema-initializer/SchemaInitializerPlugin.ts new file mode 100644 index 0000000000..11e4212852 --- /dev/null +++ b/packages/module-web/src/client/schema-initializer/SchemaInitializerPlugin.ts @@ -0,0 +1,151 @@ +import { Plugin } from '@tachybase/client'; + +// import { CreateChildInitializer } from '../modules/actions/add-child/CreateChildInitializer'; +// import { CreateActionInitializer } from '../modules/actions/add-new/CreateActionInitializer'; +// import { createFormBlockInitializers } from '../modules/actions/add-new/createFormBlockInitializers'; +// import { CustomizeAddRecordActionInitializer } from '../modules/actions/add-record/CustomizeAddRecordActionInitializer'; +// import { customizeCreateFormBlockInitializers } from '../modules/actions/add-record/customizeCreateFormBlockInitializers'; +// import { AssociateActionInitializer } from '../modules/actions/associate/AssociateActionInitializer'; +// import { AssociateActionProvider } from '../modules/actions/associate/AssociateActionProvider'; +// import { BulkDestroyActionInitializer } from '../modules/actions/bulk-destroy/BulkDestroyActionInitializer'; +// import { DestroyActionInitializer } from '../modules/actions/delete/DestroyActionInitializer'; +// import { DisassociateActionInitializer } from '../modules/actions/disassociate/DisassociateActionInitializer'; +// import { ExpandableActionInitializer } from '../modules/actions/expand-collapse/ExpandableActionInitializer'; +// import { FilterActionInitializer } from '../modules/actions/filter/FilterActionInitializer'; +// import { RefreshActionInitializer } from '../modules/actions/refresh/RefreshActionInitializer'; +// import { SaveRecordActionInitializer } from '../modules/actions/save-record/SaveRecordActionInitializer'; +// import { CreateSubmitActionInitializer } from '../modules/actions/submit/CreateSubmitActionInitializer'; +// import { UpdateSubmitActionInitializer } from '../modules/actions/submit/UpdateSubmitActionInitializer'; +// import { UpdateRecordActionInitializer } from '../modules/actions/update-record/UpdateRecordActionInitializer'; +// import { PopupActionInitializer } from '../modules/actions/view-edit-popup/PopupActionInitializer'; +// import { recordFormBlockInitializers } from '../modules/actions/view-edit-popup/RecordFormBlockInitializers'; +// import { UpdateActionInitializer } from '../modules/actions/view-edit-popup/UpdateActionInitializer'; +// import { ViewActionInitializer } from '../modules/actions/view-edit-popup/ViewActionInitializer'; +// import { detailsActionInitializers } from '../modules/blocks/data-blocks/details-multi/DetailsActionInitializers'; +// import { DetailsBlockInitializer } from '../modules/blocks/data-blocks/details-multi/DetailsBlockInitializer'; +// import { readPrettyFormActionInitializers } from '../modules/blocks/data-blocks/details-single/ReadPrettyFormActionInitializers'; +// import { readPrettyFormItemInitializers } from '../modules/blocks/data-blocks/details-single/ReadPrettyFormItemInitializers'; +// import { RecordReadPrettyFormBlockInitializer } from '../modules/blocks/data-blocks/details-single/RecordReadPrettyFormBlockInitializer'; +// import { +// createFormActionInitializers, +// formActionInitializers, +// formItemInitializers, +// updateFormActionInitializers, +// } from '../modules/blocks/data-blocks/form'; +// import { CreateFormBlockInitializer } from '../modules/blocks/data-blocks/form/CreateFormBlockInitializer'; +// import { FormBlockInitializer } from '../modules/blocks/data-blocks/form/FormBlockInitializer'; +// import { RecordFormBlockInitializer } from '../modules/blocks/data-blocks/form/RecordFormBlockInitializer'; +// import { gridCardActionInitializers } from '../modules/blocks/data-blocks/grid-card/GridCardActionInitializers'; +// import { GridCardBlockInitializer } from '../modules/blocks/data-blocks/grid-card/GridCardBlockInitializer'; +// import { gridCardItemActionInitializers } from '../modules/blocks/data-blocks/grid-card/gridCardItemActionInitializers'; +// import { listActionInitializers } from '../modules/blocks/data-blocks/list/ListActionInitializers'; +// import { ListBlockInitializer } from '../modules/blocks/data-blocks/list/ListBlockInitializer'; +// import { listItemActionInitializers } from '../modules/blocks/data-blocks/list/listItemActionInitializers'; +// import { +// tableActionColumnInitializers, +// tableActionInitializers, +// tableColumnInitializers, +// } from '../modules/blocks/data-blocks/table'; +// import { TableSelectorInitializer } from '../modules/blocks/data-blocks/table-selector/TableSelectorInitializer'; +// import { TableBlockInitializer } from '../modules/blocks/data-blocks/table/TableBlockInitializer'; +// import { FilterCollapseBlockInitializer } from '../modules/blocks/filter-blocks/collapse/FilterCollapseBlockInitializer'; +// import { filterFormActionInitializers } from '../modules/blocks/filter-blocks/form/FilterFormActionInitializers'; +// import { FilterFormBlockInitializer } from '../modules/blocks/filter-blocks/form/FilterFormBlockInitializer'; +// import { filterFormItemInitializers } from '../modules/blocks/filter-blocks/form/filterFormItemInitializers'; +// import { filterTreeActionInitializers } from '../modules/blocks/filter-blocks/tree/FilterTreeActionInitializers'; +// import { FilterTreeBlockInitializer } from '../modules/blocks/filter-blocks/tree/FilterTreeBlockInitializer'; +// import { MarkdownBlockInitializer } from '../modules/blocks/other-blocks/markdown/MarkdownBlockInitializer'; +// import { MarkdownFormItemInitializer } from '../modules/blocks/other-blocks/markdown/MarkdownFormItemInitializer'; +// import { tableSelectorInitializers } from '../modules/fields/component/Picker/TableSelectorInitializers'; +// import { CollectionFieldInitializer } from '../modules/fields/initializer/CollectionFieldInitializer'; +// import { TableCollectionFieldInitializer } from '../modules/fields/initializer/TableCollectionFieldInitializer'; +// import { menuItemInitializer } from '../modules/menu/menuItemInitializer'; +// import { blockInitializers } from '../modules/page/BlockInitializers'; +import { + customFormItemInitializers, + recordBlockInitializers, + subTableActionInitializers, + tabPaneInitializers, + tabPaneInitializers_deprecated, + tabPaneInitializersForBulkEditFormBlock, + tabPaneInitializersForRecordBlock, +} from './buttons'; +import * as initializerComponents from './components'; +import * as items from './items'; +import { FilterFormItemCustom } from './items'; + +export class SchemaInitializerPlugin extends Plugin { + async load() { + this.app.addComponents({ + ...initializerComponents, + ...items, + DestroyActionInitializer, + CreateFormBlockInitializer, + FormBlockInitializer, + RecordFormBlockInitializer, + TableBlockInitializer, + TableSelectorInitializer, + RecordReadPrettyFormBlockInitializer, + DetailsBlockInitializer, + ListBlockInitializer, + GridCardBlockInitializer, + FilterFormBlockInitializer, + FilterTreeBlockInitializer, + FilterCollapseBlockInitializer, + MarkdownBlockInitializer, + MarkdownFormItemInitializer, + TableCollectionFieldInitializer, + CollectionFieldInitializer, + CreateActionInitializer, + CustomizeAddRecordActionInitializer, + CreateChildInitializer, + ViewActionInitializer, + UpdateActionInitializer, + PopupActionInitializer, + SaveRecordActionInitializer, + UpdateRecordActionInitializer, + CreateSubmitActionInitializer, + UpdateSubmitActionInitializer, + BulkDestroyActionInitializer, + ExpandableActionInitializer, + DisassociateActionInitializer, + FilterActionInitializer, + RefreshActionInitializer, + FilterFormItemCustom, + AssociateActionInitializer, + AssociateActionProvider, + } as any); + + this.app.schemaInitializerManager.add(blockInitializers); + this.app.schemaInitializerManager.add(tableActionInitializers); + this.app.schemaInitializerManager.add(tableColumnInitializers); + this.app.schemaInitializerManager.add(tableActionColumnInitializers); + this.app.schemaInitializerManager.add(formItemInitializers); + this.app.schemaInitializerManager.add(formActionInitializers); + this.app.schemaInitializerManager.add(detailsActionInitializers); + this.app.schemaInitializerManager.add(readPrettyFormItemInitializers); + this.app.schemaInitializerManager.add(readPrettyFormActionInitializers); + this.app.schemaInitializerManager.add(createFormBlockInitializers); + this.app.schemaInitializerManager.add(customizeCreateFormBlockInitializers); + this.app.schemaInitializerManager.add(customFormItemInitializers); + this.app.schemaInitializerManager.add(filterFormActionInitializers); + this.app.schemaInitializerManager.add(filterTreeActionInitializers); + this.app.schemaInitializerManager.add(createFormActionInitializers); + this.app.schemaInitializerManager.add(updateFormActionInitializers); + this.app.schemaInitializerManager.add(filterFormItemInitializers); + this.app.schemaInitializerManager.add(gridCardActionInitializers); + this.app.schemaInitializerManager.add(gridCardItemActionInitializers); + + this.app.schemaInitializerManager.add(listActionInitializers); + this.app.schemaInitializerManager.add(listItemActionInitializers); + this.app.schemaInitializerManager.add(recordBlockInitializers); + this.app.schemaInitializerManager.add(recordFormBlockInitializers); + this.app.schemaInitializerManager.add(subTableActionInitializers); + this.app.schemaInitializerManager.add(tableSelectorInitializers); + this.app.schemaInitializerManager.add(tabPaneInitializers_deprecated); + this.app.schemaInitializerManager.add(tabPaneInitializersForRecordBlock); + this.app.schemaInitializerManager.add(tabPaneInitializersForBulkEditFormBlock); + this.app.schemaInitializerManager.add(menuItemInitializer); + this.app.schemaInitializerManager.add(tabPaneInitializers); + } +} diff --git a/packages/module-web/src/client/schema-initializer/buttons/CustomFormItemInitializers.tsx b/packages/module-web/src/client/schema-initializer/buttons/CustomFormItemInitializers.tsx new file mode 100644 index 0000000000..7d314853e0 --- /dev/null +++ b/packages/module-web/src/client/schema-initializer/buttons/CustomFormItemInitializers.tsx @@ -0,0 +1,46 @@ +import React from 'react'; + +import { useTranslation } from 'react-i18next'; + +import { SchemaInitializerChildren } from '../../application/schema-initializer/components/SchemaInitializerChildren'; +import { SchemaInitializer } from '../../application/schema-initializer/SchemaInitializer'; +import { useCompile } from '../../schema-component'; +import { gridRowColWrap, useCustomFormItemInitializerFields, useInheritsFormItemInitializerFields } from '../utils'; + +// 表单里配置字段 +const ParentCollectionFields = () => { + const inheritFields = useInheritsFormItemInitializerFields({ component: 'AssignedField' }); + const { t } = useTranslation(); + const compile = useCompile(); + if (!inheritFields?.length) return null; + const res = []; + inheritFields.forEach((inherit) => { + Object.values(inherit)[0].length && + res.push({ + type: 'itemGroup', + divider: true, + title: t(`Parent collection fields`) + '(' + compile(`${Object.keys(inherit)[0]}`) + ')', + children: Object.values(inherit)[0], + }); + }); + return {res}; +}; + +export const customFormItemInitializers = new SchemaInitializer({ + name: 'assignFieldValuesForm:configureFields', + wrap: gridRowColWrap, + icon: 'SettingOutlined', + title: '{{t("Configure fields")}}', + items: [ + { + type: 'itemGroup', + title: '{{t("Configure fields")}}', + name: 'configureFields', + useChildren: useCustomFormItemInitializerFields, + }, + { + name: 'parentCollectionFields', + Component: ParentCollectionFields, + }, + ], +}); diff --git a/packages/module-web/src/client/schema-initializer/buttons/FormItemInitializers.tsx b/packages/module-web/src/client/schema-initializer/buttons/FormItemInitializers.tsx new file mode 100644 index 0000000000..a8010f680c --- /dev/null +++ b/packages/module-web/src/client/schema-initializer/buttons/FormItemInitializers.tsx @@ -0,0 +1,162 @@ +import React from 'react'; +import { useForm } from '@tachybase/schema'; + +import { useTranslation } from 'react-i18next'; + +import { SchemaInitializerChildren, SchemaInitializerItemType } from '../../application'; +import { useCollectionManager_deprecated } from '../../collection-manager'; +import { useCollectionManager, useExtendCollections } from '../../data-source'; +import { useActionContext, useCompile } from '../../schema-component'; +import { + removeGridFormItem, + useAssociatedFormItemInitializerFields, + useFilterAssociatedFormItemInitializerFields, + useFilterInheritsFormItemInitializerFields, + useInheritsFormItemInitializerFields, +} from '../utils'; + +export const ParentCollectionFields = () => { + const inheritFields = useInheritsFormItemInitializerFields(); + const { t } = useTranslation(); + const compile = useCompile(); + if (!inheritFields?.length) return null; + const res = []; + inheritFields.forEach((inherit) => { + Object.values(inherit)[0].length && + res.push({ + type: 'itemGroup', + divider: true, + title: t(`Parent collection fields`) + '(' + compile(`${Object.keys(inherit)[0]}`) + ')', + children: Object.values(inherit)[0], + }); + }); + return {res}; +}; + +export const ExtendCollectionFields = (props) => { + const collections = useExtendCollections(); + const form = useForm(); + const compile = useCompile(); + const { getInterface, getCollection } = useCollectionManager_deprecated(); + const { fieldSchema } = useActionContext(); + if (!collections) { + return null; + } + const children = collections.map((collection) => { + // FIXME + const readPretty = form.readPretty; + const block = 'Form'; + const action = fieldSchema?.['x-action']; + + return { + type: 'subMenu', + title: compile(collection.title), + children: collection.fields + ?.filter((field) => field?.interface && !field?.isForeignKey && !field?.treeChildren) + ?.map((field) => { + const interfaceConfig = getInterface(field.interface); + const targetCollection = getCollection(field.target); + const isFileCollection = field?.target && getCollection(field?.target)?.template === 'file'; + const isAssociationField = targetCollection; + const fieldNames = field?.uiSchema['x-component-props']?.['fieldNames']; + const schema = { + type: 'string', + name: field.name, + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': 'fieldSettings:FormItem', + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-collection-field': `${collection.name}.${field.name}`, + 'x-component-props': isFileCollection + ? { + fieldNames: { + label: 'preview', + value: 'id', + }, + } + : isAssociationField && fieldNames + ? { + fieldNames: { ...fieldNames, label: targetCollection?.titleField || fieldNames.label }, + } + : {}, + 'x-read-pretty': field?.uiSchema?.['x-read-pretty'], + }; + const resultItem = { + type: 'item', + name: field.name, + title: field?.uiSchema?.title || field.name, + Component: 'CollectionFieldInitializer', + remove: removeGridFormItem, + schemaInitialize: (s) => { + interfaceConfig?.schemaInitialize?.(s, { + field, + block, + readPretty, + action, + targetCollection, + }); + }, + schema, + } as SchemaInitializerItemType; + return resultItem; + }), + }; + }); + const res = []; + res.push({ + type: 'itemGroup', + title: '{{ t("Extend collections") }}', + children, + }); + return {res}; +}; + +export const AssociatedFields = () => { + const associationFields = useAssociatedFormItemInitializerFields({ + readPretty: true, + block: 'Form', + }); + const { t } = useTranslation(); + if (associationFields.length === 0) return null; + const schema: any = [ + { + type: 'itemGroup', + title: t('Display association fields'), + children: associationFields, + }, + ]; + return {schema}; +}; + +export const FilterParentCollectionFields = () => { + const inheritFields = useFilterInheritsFormItemInitializerFields(); + const { t } = useTranslation(); + const compile = useCompile(); + const res = []; + if (inheritFields?.length > 0) { + inheritFields.forEach((inherit) => { + Object.values(inherit)[0].length && + res.push({ + divider: true, + type: 'itemGroup', + title: t(`Parent collection fields`) + '(' + compile(`${Object.keys(inherit)[0]}`) + ')', + children: Object.values(inherit)[0], + }); + }); + } + + return {res}; +}; + +export const FilterAssociatedFields = () => { + const associationFields = useFilterAssociatedFormItemInitializerFields(); + const { t } = useTranslation(); + const res: any[] = [ + { + type: 'itemGroup', + title: t('Display association fields'), + children: associationFields, + }, + ]; + return {res}; +}; diff --git a/packages/module-web/src/client/schema-initializer/buttons/RecordBlockInitializers.tsx b/packages/module-web/src/client/schema-initializer/buttons/RecordBlockInitializers.tsx new file mode 100644 index 0000000000..ee3df38519 --- /dev/null +++ b/packages/module-web/src/client/schema-initializer/buttons/RecordBlockInitializers.tsx @@ -0,0 +1,380 @@ +import { useCallback, useMemo } from 'react'; +import { Schema } from '@tachybase/schema'; + +import { + useCollection, + useCollection_deprecated, + useCollectionManager_deprecated, + useCreateAssociationDetailsBlock, + useCreateAssociationDetailsWithoutPagination, + useCreateAssociationFormBlock, + useCreateAssociationGridCardBlock, + useCreateAssociationListBlock, + useCreateAssociationTableBlock, + useCreateEditFormBlock, + useCreateFormBlock, + useCreateTableBlock, +} from '../..'; +import { SchemaInitializer } from '../../application/schema-initializer/SchemaInitializer'; +import { useCreateDetailsBlock } from '../../modules/blocks/data-blocks/details-multi/DetailsBlockInitializer'; +import { useCreateSingleDetailsSchema } from '../../modules/blocks/data-blocks/details-single/RecordReadPrettyFormBlockInitializer'; +import { useCreateGridCardBlock } from '../../modules/blocks/data-blocks/grid-card/GridCardBlockInitializer'; +import { useCreateListBlock } from '../../modules/blocks/data-blocks/list/ListBlockInitializer'; +import { gridRowColWrap } from '../utils'; + +const recursiveParent = (schema: Schema) => { + if (!schema) return null; + + if (schema['x-decorator']?.endsWith('BlockProvider')) { + return schema['x-decorator-props']?.['collection']; + } else { + return recursiveParent(schema.parent); + } +}; + +export const canMakeAssociationBlock = (field) => { + return ['linkTo', 'subTable', 'o2m', 'm2m', 'obo', 'oho', 'o2o', 'm2o'].includes(field.interface); +}; + +function useRecordBlocks() { + const collection = useCollection(); + const { getChildrenCollections } = useCollectionManager_deprecated(); + const collectionsWithView = getChildrenCollections(collection.name, true, collection.dataSource).filter( + (v) => v?.filterTargetKey, + ); + + const res: any[] = [ + { + name: 'details', + title: '{{t("Details")}}', + Component: 'DetailsBlockInitializer', + collectionName: collection.name, + dataSource: collection.dataSource, + useComponentProps() { + const currentCollection = useCollection_deprecated(); + const { createSingleDetailsSchema, templateWrap } = useCreateSingleDetailsSchema(); + const { createAssociationDetailsBlock } = useCreateAssociationDetailsBlock(); + const { + createAssociationDetailsWithoutPagination, + templateWrap: templateWrapOfAssociationDetailsWithoutPagination, + } = useCreateAssociationDetailsWithoutPagination(); + const { createDetailsBlock } = useCreateDetailsBlock(); + const collectionsNeedToDisplay = [currentCollection, ...collectionsWithView]; + const createBlockSchema = useCallback( + ({ item, fromOthersInPopup }) => { + if (fromOthersInPopup) { + return createDetailsBlock({ item }); + } + if (item.associationField) { + if (['hasOne', 'belongsTo'].includes(item.associationField.type)) { + return createAssociationDetailsWithoutPagination({ item }); + } + return createAssociationDetailsBlock({ item }); + } + return createSingleDetailsSchema({ item }); + }, + [ + createAssociationDetailsBlock, + createAssociationDetailsWithoutPagination, + createDetailsBlock, + createSingleDetailsSchema, + ], + ); + return { + filterCollections({ collection, associationField }) { + if (collection) { + return collectionsNeedToDisplay.some((c) => c.name === collection.name); + } + if (associationField) { + return true; + } + return false; + }, + onlyCurrentDataSource: true, + hideSearch: true, + componentType: 'ReadPrettyFormItem', + createBlockSchema, + templateWrap: useCallback( + (templateSchema, { item }) => { + if (item.associationField) { + if (['hasOne', 'belongsTo'].includes(item.associationField.type)) { + return templateWrapOfAssociationDetailsWithoutPagination(templateSchema, { item }); + } + return templateSchema; + } + return templateWrap(templateSchema, { item }); + }, + [templateWrap, templateWrapOfAssociationDetailsWithoutPagination], + ), + showAssociationFields: true, + }; + }, + }, + { + name: 'editForm', + title: '{{t("Form (Edit)")}}', + Component: 'FormBlockInitializer', + collectionName: collection.name, + dataSource: collection.dataSource, + useComponentProps() { + const currentCollection = useCollection_deprecated(); + const { createEditFormBlock, templateWrap: templateWrapEdit } = useCreateEditFormBlock(); + const collectionsNeedToDisplay = [currentCollection, ...collectionsWithView]; + + return { + filterCollections({ collection }) { + if (collection) { + return collectionsNeedToDisplay.some((c) => c.name === collection.name); + } + return true; + }, + onlyCurrentDataSource: true, + hideSearch: true, + componentType: 'FormItem', + createBlockSchema: createEditFormBlock, + templateWrap: templateWrapEdit, + hideOtherRecordsInPopup: true, + showAssociationFields: true, + }; + }, + useVisible() { + return (collection.template !== 'view' || collection?.writableView) && collection.template !== 'sql'; + }, + }, + { + name: 'createForm', + title: '{{t("Form (Add new)")}}', + Component: 'FormBlockInitializer', + collectionName: collection.name, + dataSource: collection.dataSource, + useComponentProps() { + const { createAssociationFormBlock, templateWrap } = useCreateAssociationFormBlock(); + const { createFormBlock, templateWrap: templateWrapCollection } = useCreateFormBlock(); + return { + filterCollections({ collection, associationField }) { + if (associationField) { + return ['hasMany', 'belongsToMany'].includes(associationField.type); + } + return false; + }, + onlyCurrentDataSource: true, + hideSearch: true, + componentType: 'FormItem', + createBlockSchema: ({ item, fromOthersInPopup }) => { + if (fromOthersInPopup) { + return createFormBlock({ item, fromOthersInPopup }); + } + createAssociationFormBlock({ item }); + }, + templateWrap: (templateSchema, { item, fromOthersInPopup }) => { + if (fromOthersInPopup) { + return templateWrapCollection(templateSchema, { item }); + } + templateWrap(templateSchema, { item }); + }, + showAssociationFields: true, + }; + }, + }, + { + name: 'table', + title: '{{t("Table")}}', + Component: 'TableBlockInitializer', + useComponentProps() { + const { createAssociationTableBlock } = useCreateAssociationTableBlock(); + const { createTableBlock } = useCreateTableBlock(); + + return { + hideSearch: true, + onlyCurrentDataSource: true, + filterCollections({ associationField }) { + if (associationField) { + return ['hasMany', 'belongsToMany'].includes(associationField.type); + } + return false; + }, + createBlockSchema: ({ item, fromOthersInPopup }) => { + if (fromOthersInPopup) { + return createTableBlock({ item }); + } + createAssociationTableBlock({ item }); + }, + showAssociationFields: true, + }; + }, + }, + { + name: 'list', + title: '{{t("List")}}', + Component: 'ListBlockInitializer', + useComponentProps() { + const { createAssociationListBlock } = useCreateAssociationListBlock(); + const { createListBlock } = useCreateListBlock(); + + return { + hideSearch: true, + onlyCurrentDataSource: true, + filterCollections({ associationField }) { + if (associationField) { + return ['hasMany', 'belongsToMany'].includes(associationField.type); + } + return false; + }, + createBlockSchema: ({ item, fromOthersInPopup }) => { + if (fromOthersInPopup) { + return createListBlock({ item }); + } + createAssociationListBlock({ item }); + }, + showAssociationFields: true, + }; + }, + }, + { + name: 'gridCard', + title: '{{t("Grid Card")}}', + Component: 'GridCardBlockInitializer', + useComponentProps() { + const { createAssociationGridCardBlock } = useCreateAssociationGridCardBlock(); + const { createGridCardBlock } = useCreateGridCardBlock(); + + return { + hideSearch: true, + onlyCurrentDataSource: true, + filterCollections({ associationField }) { + if (associationField) { + return ['hasMany', 'belongsToMany'].includes(associationField.type); + } + return false; + }, + createBlockSchema: ({ item, fromOthersInPopup }) => { + if (fromOthersInPopup) { + return createGridCardBlock({ item }); + } + createAssociationGridCardBlock({ item }); + }, + showAssociationFields: true, + }; + }, + }, + ]; + + return res; +} + +export const recordBlockInitializers = new SchemaInitializer({ + name: 'popup:common:addBlock', + wrap: gridRowColWrap, + title: '{{t("Add block")}}', + icon: 'PlusOutlined', + items: [ + { + type: 'itemGroup', + name: 'dataBlocks', + title: '{{t("Data blocks")}}', + useChildren: useRecordBlocks, + }, + { + name: 'filterBlocks', + title: '{{t("Filter blocks")}}', + type: 'itemGroup', + useVisible() { + const collection = useCollection(); + return useMemo( + () => + collection.fields.some( + (field) => canMakeAssociationBlock(field) && ['hasMany', 'belongsToMany'].includes(field.type), + ), + [collection.fields], + ); + }, + children: [ + { + name: 'filterForm', + title: '{{t("Form")}}', + Component: 'FilterFormBlockInitializer', + useComponentProps() { + const collection = useCollection(); + const toManyField = useMemo( + () => collection.fields.filter((field) => ['hasMany', 'belongsToMany'].includes(field.type)), + [collection.fields], + ); + + return { + filterCollections({ collection }) { + if (collection) { + return toManyField.some((field) => field.target === collection.name); + } + }, + onlyCurrentDataSource: true, + }; + }, + }, + { + name: 'filterCollapse', + title: '{{t("Collapse")}}', + Component: 'FilterCollapseBlockInitializer', + useComponentProps() { + const collection = useCollection(); + const toManyField = useMemo( + () => collection.fields.filter((field) => ['hasMany', 'belongsToMany'].includes(field.type)), + [collection.fields], + ); + + return { + filterCollections({ collection }) { + if (collection) { + return toManyField.some((field) => field.target === collection.name); + } + }, + onlyCurrentDataSource: true, + }; + }, + }, + { + name: 'filterTree', + title: '{{t("Tree")}}', + Component: 'FilterTreeBlockInitializer', + useComponentProps() { + const collection = useCollection(); + const toManyField = useMemo( + () => collection.fields.filter((field) => ['hasMany', 'belongsToMany'].includes(field.type)), + [collection.fields], + ); + + return { + filterCollections({ collection }) { + if (collection) { + return toManyField.some((field) => field.target === collection.name); + } + }, + onlyCurrentDataSource: true, + }; + }, + }, + ], + }, + // { + // type: 'itemGroup', + // name: 'relationshipBlocks', + // title: '{{t("Relationship blocks")}}', + // useChildren: useRelationFields, + // useVisible() { + // const res = useRelationFields(); + // return res.length > 0; + // }, + // }, + { + type: 'itemGroup', + name: 'otherBlocks', + title: '{{t("Other blocks")}}', + children: [ + { + name: 'markdown', + title: '{{t("Demonstration text")}}', + Component: 'MarkdownBlockInitializer', + }, + ], + }, + ], +}); diff --git a/packages/module-web/src/client/schema-initializer/buttons/SubTableActionInitializers.tsx b/packages/module-web/src/client/schema-initializer/buttons/SubTableActionInitializers.tsx new file mode 100644 index 0000000000..12541a78b8 --- /dev/null +++ b/packages/module-web/src/client/schema-initializer/buttons/SubTableActionInitializers.tsx @@ -0,0 +1,35 @@ +import { SchemaInitializer } from '@tachybase/client'; + +export const subTableActionInitializers = new SchemaInitializer({ + name: 'subTable:configureActions', + title: "{{t('Configure actions')}}", + icon: 'SettingOutlined', + style: { + marginLeft: 8, + }, + items: [ + { + type: 'itemGroup', + title: "{{t('Enable actions')}}", + name: 'enableActions', + children: [ + { + name: 'addNew', + title: "{{t('Add new')}}", + Component: 'CreateActionInitializer', + schema: { + 'x-align': 'right', + }, + }, + { + name: 'delete', + title: "{{t('Delete')}}", + Component: 'BulkDestroyActionInitializer', + schema: { + 'x-align': 'right', + }, + }, + ], + }, + ], +}); diff --git a/packages/module-web/src/client/schema-initializer/buttons/TabPaneInitializers.tsx b/packages/module-web/src/client/schema-initializer/buttons/TabPaneInitializers.tsx new file mode 100644 index 0000000000..cec003b08f --- /dev/null +++ b/packages/module-web/src/client/schema-initializer/buttons/TabPaneInitializers.tsx @@ -0,0 +1,222 @@ +import { useMemo } from 'react'; +import { + SchemaComponent, + SchemaInitializer, + useActionContext, + useAPIClient, + useDesignable, + useRecord, +} from '@tachybase/client'; +import { ISchema, useFieldSchema, useForm } from '@tachybase/schema'; + +import { App } from 'antd'; +import { saveAs } from 'file-saver'; + +import { useGetAriaLabelOfSchemaInitializer } from '../hooks/useGetAriaLabelOfSchemaInitializer'; + +export const TabPaneInitializers = (props?: any) => { + const { designable, insertBeforeEnd } = useDesignable(); + const fieldSchema = useFieldSchema(); + const { message } = App.useApp(); + const { isCreate, isBulkEdit, options } = props; + const { gridInitializer } = options; + const { getAriaLabel } = useGetAriaLabelOfSchemaInitializer(); + const record = useRecord(); + + const useDumpAction = () => { + const api = useAPIClient(); + return { + async run() { + const deleteUid = (s: ISchema) => { + delete s['name']; + delete s['x-uid']; + Object.keys(s.properties || {}).forEach((key) => { + deleteUid(s.properties[key]); + }); + }; + const { data } = await api.request({ + url: `/uiSchemas:getJsonSchema/${fieldSchema['x-uid']}?includeAsyncNode=true`, + }); + const s = data?.data || {}; + deleteUid(s); + const blob = new Blob([JSON.stringify(s, null, 2)], { type: 'application/json' }); + saveAs(blob, fieldSchema['x-uid'] + '.schema.json'); + message.success('Save successful!'); + }, + }; + }; + + const useSubmitAction = () => { + const form = useForm(); + const ctx = useActionContext(); + let initializer = gridInitializer; + if (!initializer) { + initializer = 'popup:common:addBlock'; + if (isCreate || !record) { + initializer = 'popup:addNew:addBlock'; + } else if (isBulkEdit) { + initializer = 'popup:bulkEdit:addBlock'; + } + } + return { + async run() { + await form.submit(); + const { title, icon } = form.values; + insertBeforeEnd({ + type: 'void', + title, + 'x-component': 'Tabs.TabPane', + 'x-designer': 'Tabs.Designer', + 'x-component-props': { + icon, + }, + properties: { + grid: { + type: 'void', + 'x-component': 'Grid', + 'x-initializer': initializer, + properties: {}, + }, + }, + }); + await form.reset(); + ctx.setVisible(false); + }, + }; + }; + const schema = useMemo(() => { + return { + type: 'void', + properties: { + fixStyle: { + type: 'void', + 'x-component': 'div', + 'x-component-props': { + style: { + display: 'flex', + }, + }, + properties: { + action1: { + type: 'void', + 'x-component': 'Action', + 'x-component-props': { + icon: 'PlusOutlined', + style: { + borderColor: 'var(--colorSettings)', + color: 'var(--colorSettings)', + }, + type: 'dashed', + 'aria-label': getAriaLabel(), + }, + title: '{{t("Add tab")}}', + properties: { + drawer1: { + 'x-decorator': 'Form', + 'x-component': 'Action.Modal', + 'x-component-props': { + width: 520, + }, + type: 'void', + title: '{{t("Add tab")}}', + properties: { + title: { + title: '{{t("Tab name")}}', + required: true, + 'x-component': 'Input', + 'x-decorator': 'FormItem', + }, + icon: { + title: '{{t("Icon")}}', + 'x-component': 'IconPicker', + 'x-decorator': 'FormItem', + }, + footer: { + 'x-component': 'Action.Modal.Footer', + type: 'void', + properties: { + cancel: { + title: '{{t("Cancel")}}', + 'x-component': 'Action', + 'x-component-props': { + useAction: () => { + const ctx = useActionContext(); + return { + async run() { + ctx.setVisible(false); + }, + }; + }, + }, + }, + submit: { + title: '{{t("Submit")}}', + 'x-component': 'Action', + 'x-component-props': { + type: 'primary', + useAction: useSubmitAction, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + }, []); + + if (!designable) { + return null; + } + + return ; +}; + +export const TabPaneInitializersForCreateFormBlock = (props) => { + return ; +}; + +export const TabPaneInitializersForBulkEditFormBlock = (props) => { + return ; +}; + +const commonOptions = { + Component: TabPaneInitializers, + popover: false, +}; + +/** + * @deprecated + */ +export const tabPaneInitializers_deprecated = new SchemaInitializer({ + name: 'TabPaneInitializers', + Component: TabPaneInitializers, + popover: false, +}); + +export const tabPaneInitializers = new SchemaInitializer({ + name: 'popup:addTab', + ...commonOptions, +}); + +/** + * @deprecated + */ +export const tabPaneInitializersForRecordBlock = new SchemaInitializer({ + name: 'TabPaneInitializersForCreateFormBlock', + Component: TabPaneInitializersForCreateFormBlock, + popover: false, +}); + +/** + * @deprecated + */ +export const tabPaneInitializersForBulkEditFormBlock = new SchemaInitializer({ + name: 'TabPaneInitializersForBulkEditFormBlock', + Component: TabPaneInitializersForBulkEditFormBlock, + popover: false, +}); diff --git a/packages/module-web/src/client/schema-initializer/buttons/index.ts b/packages/module-web/src/client/schema-initializer/buttons/index.ts new file mode 100644 index 0000000000..23171b79ea --- /dev/null +++ b/packages/module-web/src/client/schema-initializer/buttons/index.ts @@ -0,0 +1,7 @@ +export * from './CustomFormItemInitializers'; +export * from './FormItemInitializers'; +export * from './RecordBlockInitializers'; +export * from './SubTableActionInitializers'; +export * from './TabPaneInitializers'; +// association filter +export * from '../../schema-component/antd/association-filter/AssociationFilter'; diff --git a/packages/module-web/src/client/schema-initializer/components/CreateRecordAction.tsx b/packages/module-web/src/client/schema-initializer/components/CreateRecordAction.tsx new file mode 100644 index 0000000000..50379cff25 --- /dev/null +++ b/packages/module-web/src/client/schema-initializer/components/CreateRecordAction.tsx @@ -0,0 +1,421 @@ +import React, { createRef, forwardRef, useEffect, useMemo, useState } from 'react'; +import { + ActionContextProvider, + CollectionProvider_deprecated, + linkageAction, + parseVariables, + useACLRolesCheck, + useActionContext, + useCollection_deprecated, + useCollectionManager_deprecated, + useCompile, + useDesignable, + useLocalVariables, + useRecord, + useRecordPkValue, + useVariables, +} from '@tachybase/client'; +import { observer, RecursionField, useField, useFieldSchema, useForm } from '@tachybase/schema'; + +import { DownOutlined } from '@ant-design/icons'; +import { Button, Dropdown, MenuProps } from 'antd'; +import { createStyles } from 'antd-style'; +import { composeRef } from 'rc-util/lib/ref'; + +const useStyles = createStyles(({ css }) => { + return { + designer: css` + position: relative; + &:hover { + .general-schema-designer { + display: block; + } + } + .general-schema-designer { + position: absolute; + z-index: 999; + top: 0; + bottom: 0; + left: 0; + right: 0; + display: none; + background: var(--colorBgSettingsHover); + border: 0; + pointer-events: none; + > .general-schema-designer-icons { + position: absolute; + right: 2px; + top: 2px; + line-height: 16px; + pointer-events: all; + .ant-space-item { + background-color: var(--colorSettings); + color: #fff; + line-height: 16px; + width: 16px; + padding-left: 1px; + align-self: stretch; + } + } + } + `, + actionDesigner: css` + position: relative; + &:hover { + .general-schema-designer { + display: block; + } + } + .general-schema-designer { + position: absolute; + z-index: 999; + top: 0; + bottom: 0; + left: 0; + right: 0; + display: none; + background: var(--colorBgSettingsHover); + border: 0; + top: 0; + bottom: 0; + left: 0; + right: 0; + pointer-events: none; + > .general-schema-designer-icons { + position: absolute; + right: 2px; + top: 2px; + line-height: 16px; + pointer-events: all; + .ant-space-item { + background-color: var(--colorSettings); + color: #fff; + line-height: 16px; + width: 16px; + padding-left: 1px; + align-self: stretch; + } + } + } + `, + }; +}); + +export function useAclCheck(actionPath) { + const aclCheck = useAclCheckFn(); + return aclCheck(actionPath); +} + +function useAclCheckFn() { + const { data, inResources, getResourceActionParams, getStrategyActionParams } = useACLRolesCheck(); + const recordPkValue = useRecordPkValue(); + const collection = useCollection_deprecated(); + function actionAclCheck(actionPath: string) { + const resource = collection.resource; + const parseAction = (actionPath: string, options: any = {}) => { + const [resourceName] = actionPath.split(':'); + if (data?.allowAll) { + return {}; + } + if (inResources(resourceName)) { + return getResourceActionParams(actionPath); + } + return getStrategyActionParams(actionPath); + }; + if (!actionPath && resource) { + actionPath = `${resource}:create}`; + } + if (!actionPath?.includes(':')) { + actionPath = `${resource}:${actionPath}`; + } + if (!actionPath) { + return true; + } + const params = parseAction(actionPath, { recordPkValue }); + if (!params) { + return false; + } + return true; + } + + return actionAclCheck; +} + +const InternalCreateRecordAction = (props: any, ref) => { + const [visible, setVisible] = useState(false); + const collection = useCollection_deprecated(); + const fieldSchema = useFieldSchema(); + const field: any = useField(); + const [currentCollection, setCurrentCollection] = useState(collection.name); + const [currentCollectionDataSource, setCurrentCollectionDataSource] = useState(collection.dataSource); + const linkageRules: any[] = fieldSchema?.['x-linkage-rules'] || []; + const values = useRecord(); + const ctx = useActionContext(); + const variables = useVariables(); + const { styles } = useStyles(); + const localVariables = useLocalVariables({ currentForm: { values } as any }); + useEffect(() => { + field.stateOfLinkageRules = {}; + linkageRules + .filter((k) => !k.disabled) + .forEach((v) => { + v.actions?.forEach((h) => { + linkageAction({ + operator: h.operator, + field, + condition: v.condition, + variables, + localVariables, + }); + }); + }); + }, [field, linkageRules, localVariables, variables]); + const internalRef = createRef(); + const buttonRef = composeRef(ref, internalRef); + return ( + //@ts-ignore +
}> + + { + setVisible(true); + setCurrentCollection(collectionData.name); + setCurrentCollectionDataSource(collectionData.dataSource); + }} + /> + + + + +
+ ); +}; + +function getLinkageCollection(str, form, field) { + const variablesCtx = { $form: form.values, $iteration: form.values }; + if (str.includes('$iteration')) { + const path = field.path.segments.concat([]); + path.splice(-2); + str = str.replace('$iteration.', `$iteration.${path.join('.')}.`); + const data = parseVariables(str, variablesCtx); + return data; + } else { + const data = parseVariables(str, { $form: form.values }); + return data; + } +} +export const CreateAction = observer( + (props: any) => { + const { onClick } = props; + const collection = useCollection_deprecated(); + const fieldSchema = useFieldSchema(); + const field: any = useField(); + const form = useForm(); + const variables = useVariables(); + const aclCheck = useAclCheckFn(); + + const enableChildren = fieldSchema['x-enable-children'] || []; + const allowAddToCurrent = fieldSchema?.['x-allow-add-to-current']; + const linkageFromForm = fieldSchema?.['x-component-props']?.['linkageFromForm']; + // antd v5 danger type is deprecated + const componentType = field.componentProps.type === 'danger' ? undefined : field.componentProps.type || 'primary'; + const { getChildrenCollections } = useCollectionManager_deprecated(); + const totalChildCollections = getChildrenCollections(collection.name); + const inheritsCollections = useMemo(() => { + return enableChildren + .map((k) => { + if (!k) { + return; + } + const childCollection = totalChildCollections.find((j) => j.name === k.collection); + if (!childCollection) { + return; + } + return { + ...childCollection.getOptions(), + title: k.title || childCollection.title, + }; + }) + .filter((v) => { + return v && aclCheck(`${v.name}:create`); + }); + }, [enableChildren, totalChildCollections]); + const linkageRules: any[] = fieldSchema?.['x-linkage-rules'] || []; + const values = useRecord(); + const localVariables = useLocalVariables({ currentForm: { values } as any }); + const compile = useCompile(); + const { designable } = useDesignable(); + const { styles } = useStyles(); + const icon = props.icon || null; + const menuItems = useMemo(() => { + return inheritsCollections.map((option) => ({ + key: option.name, + label: compile(option.title), + onClick: () => onClick?.(option), + })); + }, [inheritsCollections, onClick]); + + const menu = useMemo(() => { + return { + items: menuItems, + }; + }, [menuItems]); + + useEffect(() => { + field.stateOfLinkageRules = {}; + linkageRules + .filter((k) => !k.disabled) + .forEach((v) => { + v.actions?.forEach((h) => { + linkageAction({ + operator: h.operator, + field, + condition: v.condition, + variables, + localVariables, + }); + }); + }); + }, [field, linkageRules, localVariables, variables]); + return ( +
+ +
+ ); + }, + { displayName: 'CreateAction' }, +); + +function FinallyButton({ + inheritsCollections, + linkageFromForm, + allowAddToCurrent, + props, + componentType, + menu, + onClick, + collection, + icon, + field, + form, + designable, +}: { + inheritsCollections: any; + linkageFromForm: any; + allowAddToCurrent: any; + props: any; + componentType: any; + menu: MenuProps; + onClick: any; + collection; + icon: any; + field: any; + form; + designable: boolean; +}) { + const { getCollection } = useCollectionManager_deprecated(); + + if (inheritsCollections?.length > 0) { + if (!linkageFromForm) { + return allowAddToCurrent === undefined || allowAddToCurrent ? ( + } + buttonsRender={([leftButton, rightButton]) => [ + React.cloneElement(leftButton as React.ReactElement, { + style: props?.style, + }), + React.cloneElement(rightButton as React.ReactElement, { + loading: false, + style: props?.style, + }), + ]} + menu={menu} + onClick={(info) => { + onClick?.(collection); + }} + > + {icon} + {props.children} + + ) : ( + + { + + } + + ); + } + return ( + + ); + } + return ( + + ); +} + +export const CreateRecordAction = forwardRef(InternalCreateRecordAction); diff --git a/packages/module-web/src/client/schema-initializer/components/DeletedField.tsx b/packages/module-web/src/client/schema-initializer/components/DeletedField.tsx new file mode 100644 index 0000000000..14a7320184 --- /dev/null +++ b/packages/module-web/src/client/schema-initializer/components/DeletedField.tsx @@ -0,0 +1,6 @@ +import { useTranslation } from 'react-i18next'; + +export const DeletedField = () => { + const { t } = useTranslation(); + return
{t('The field has bee deleted')}
; +}; diff --git a/packages/module-web/src/client/schema-initializer/components/assigned-field/AssignedField.tsx b/packages/module-web/src/client/schema-initializer/components/assigned-field/AssignedField.tsx new file mode 100644 index 0000000000..a92c1d6899 --- /dev/null +++ b/packages/module-web/src/client/schema-initializer/components/assigned-field/AssignedField.tsx @@ -0,0 +1,151 @@ +import React, { useCallback, useEffect, useMemo } from 'react'; +import { + CollectionFieldProvider, + formatVariableScop, + getShouldChange, + useCollection_deprecated, + useCollectionField_deprecated, + useCollectionFilterOptions, + useCollectionManager_deprecated, + useCompile, + useComponent, + useFormBlockContext, + useLocalVariables, + useRecord, + useVariables, + VariableInput, + VariableInputOption, +} from '@tachybase/client'; +import { Field, merge, useField, useFieldSchema } from '@tachybase/schema'; + +import _ from 'lodash'; + +interface AssignedFieldProps { + value: any; + onChange: (value: any) => void; + [key: string]: any; +} + +const InternalField = (props) => { + const field = useField(); + const fieldSchema = useFieldSchema(); + const { uiSchema } = useCollectionField_deprecated(); + const component = useComponent(uiSchema?.['x-component']); + const compile = useCompile(); + const setFieldProps = (key, value) => { + field[key] = typeof field[key] === 'undefined' ? value : field[key]; + }; + const setRequired = () => { + if (typeof fieldSchema['required'] === 'undefined') { + field.required = !!uiSchema['required']; + } + }; + const ctx = useFormBlockContext(); + + useEffect(() => { + if (ctx?.field) { + ctx.field.added = ctx.field.added || new Set(); + ctx.field.added.add(fieldSchema.name); + } + }); + + useEffect(() => { + if (!uiSchema) { + return; + } + setFieldProps('content', uiSchema['x-content']); + setFieldProps('title', uiSchema.title); + setFieldProps('description', uiSchema.description); + setFieldProps('initialValue', uiSchema.default); + // if (!field.validator && uiSchema['x-validator']) { + // field.validator = uiSchema['x-validator']; + // } + if (fieldSchema['x-disabled'] === true) { + field.disabled = true; + } + if (fieldSchema['x-read-pretty'] === true) { + field.readPretty = true; + } + setRequired(); + // @ts-ignore + field.dataSource = uiSchema.enum; + const originalProps = compile(uiSchema['x-component-props']) || {}; + const componentProps = merge(originalProps, field.componentProps || {}); + field.componentProps = componentProps; + // field.component = [component, componentProps]; + }, [JSON.stringify(uiSchema)]); + if (!uiSchema) { + return null; + } + return React.createElement(component, props, props.children); +}; + +const CollectionField = (props) => { + const fieldSchema = useFieldSchema(); + return ( + + + + ); +}; + +export enum AssignedFieldValueType { + ConstantValue = 'constantValue', + DynamicValue = 'dynamicValue', +} + +export const AssignedField = (props: AssignedFieldProps) => { + const { value, onChange } = props; + const { getCollectionFields, getAllCollectionsInheritChain } = useCollectionManager_deprecated(); + const collection = useCollection_deprecated(); + const { form } = useFormBlockContext(); + const fieldSchema = useFieldSchema(); + const record = useRecord(); + const variables = useVariables(); + const localVariables = useLocalVariables(); + const currentFormFields = useCollectionFilterOptions(collection); + + const { name, getField } = collection; + const collectionField = getField(fieldSchema.name); + + const shouldChange = useMemo( + () => getShouldChange({ collectionField, variables, localVariables, getAllCollectionsInheritChain }), + [collectionField, getAllCollectionsInheritChain, localVariables, variables], + ); + + const returnScope = useCallback( + (scope: VariableInputOption[]) => { + const currentForm = scope.find((item) => item.value === '$nForm'); + const fields = getCollectionFields(name); + + // fix + // 工作流人工节点的 `自定义表单` 卡片,与其它表单卡片不同,根据它的数据表名称,获取到的字段列表为空,所以需要在这里特殊处理一下 + if (!fields?.length && currentForm) { + currentForm.children = formatVariableScop(currentFormFields); + } + + return scope; + }, + [currentFormFields, name], + ); + + const renderSchemaComponent = useCallback( + ({ value, onChange }): React.JSX.Element => { + return ; + }, + [JSON.stringify(_.omit(props, 'value'))], + ); + return ( + + ); +}; diff --git a/packages/module-web/src/client/schema-initializer/components/assigned-field/index.ts b/packages/module-web/src/client/schema-initializer/components/assigned-field/index.ts new file mode 100644 index 0000000000..ffbb3e51d1 --- /dev/null +++ b/packages/module-web/src/client/schema-initializer/components/assigned-field/index.ts @@ -0,0 +1 @@ +export * from './AssignedField'; diff --git a/packages/module-web/src/client/schema-initializer/components/index.ts b/packages/module-web/src/client/schema-initializer/components/index.ts new file mode 100644 index 0000000000..adc976fa4b --- /dev/null +++ b/packages/module-web/src/client/schema-initializer/components/index.ts @@ -0,0 +1,2 @@ +export * from './assigned-field'; +export * from './CreateRecordAction'; diff --git a/packages/module-web/src/client/schema-initializer/hooks/useGetAriaLabelOfSchemaInitializer.ts b/packages/module-web/src/client/schema-initializer/hooks/useGetAriaLabelOfSchemaInitializer.ts new file mode 100644 index 0000000000..6c971bfa2a --- /dev/null +++ b/packages/module-web/src/client/schema-initializer/hooks/useGetAriaLabelOfSchemaInitializer.ts @@ -0,0 +1,26 @@ +import { useCallback } from 'react'; +import { useCollection_deprecated } from '@tachybase/client'; +import { useFieldSchema } from '@tachybase/schema'; + +/** + * label = 'schema-initializer' + x-component + [x-initializer] + [collectionName] + [postfix] + * @returns + */ + +export const useGetAriaLabelOfSchemaInitializer = () => { + const fieldSchema = useFieldSchema(); + const { name } = useCollection_deprecated(); + const getAriaLabel = useCallback( + (postfix?: string) => { + if (!fieldSchema) return ''; + const initializer = fieldSchema['x-initializer'] ? `-${fieldSchema['x-initializer']}` : ''; + const collectionName = name ? `-${name}` : ''; + postfix = postfix ? `-${postfix}` : ''; + + return `schema-initializer-${fieldSchema['x-component']}${initializer}${collectionName}${postfix}`; + }, + [fieldSchema, name], + ); + + return { getAriaLabel }; +}; diff --git a/packages/module-web/src/client/schema-initializer/index.md b/packages/module-web/src/client/schema-initializer/index.md new file mode 100644 index 0000000000..84cf7fbff3 --- /dev/null +++ b/packages/module-web/src/client/schema-initializer/index.md @@ -0,0 +1,71 @@ +--- +group: + title: Client + order: 1 +--- + +# SchemaInitializer + +用于各种 schema 的初始化。新增的 schema 可以插入到某个已有 schema 节点的任意位置,包括: + +```ts +{ + properties: { + // beforeBegin 在当前节点的前面插入 + node1: { + properties: { + // afterBegin 在当前节点的第一个子节点前面插入 + // ... + // beforeEnd 在当前节点的最后一个子节点后面 + }, + }, + // afterEnd 在当前节点的后面 + }, +} +``` + +## Examples + +### Basic + + + +### Nested items + + + +### Custom Items Component + +列表默认使用 `List` 组件,可以通过 `ItemsComponent` 属性自定义列表组件。 + + + +### Custom Button + + + +### Built Type + +TachyBase 提供了几个内置的组件,可以直接使用。 + + + +### Dynamic visible & children + +动态显示和隐藏 Item 项,以及动态加载 children。 + + + +### Insert schema + +#### Basic + + + +#### Action + + + +#### FormItem + + diff --git a/packages/module-web/src/client/schema-initializer/index.ts b/packages/module-web/src/client/schema-initializer/index.ts new file mode 100644 index 0000000000..ebacdab0a4 --- /dev/null +++ b/packages/module-web/src/client/schema-initializer/index.ts @@ -0,0 +1,20 @@ +export * from './buttons'; +export * from './items'; +export { + createDetailsBlockSchema, + createFormBlockSchema, + createReadPrettyFormBlockSchema, + createTableBlockSchema, + gridRowColWrap, + itemsMerge, + useAssociatedFormItemInitializerFields, + useAssociatedTableColumnInitializerFields, + useCollectionDataSourceItems, + useCurrentSchema, + useFormItemInitializerFields, + useInheritsTableColumnInitializerFields, + useRecordCollectionDataSourceItems, + useRemoveGridFormItem, + useTableColumnInitializerFields, +} from './utils'; +export * from './SchemaInitializerPlugin'; diff --git a/packages/module-web/src/client/schema-initializer/items/ActionInitializer.tsx b/packages/module-web/src/client/schema-initializer/items/ActionInitializer.tsx new file mode 100644 index 0000000000..1046e62ad5 --- /dev/null +++ b/packages/module-web/src/client/schema-initializer/items/ActionInitializer.tsx @@ -0,0 +1,9 @@ +import React from 'react'; + +import { useSchemaInitializerItem } from '../../application'; +import { InitializerWithSwitch } from './InitializerWithSwitch'; + +export const ActionInitializer = (props) => { + const itemConfig = useSchemaInitializerItem(); + return ; +}; diff --git a/packages/module-web/src/client/schema-initializer/items/BlockInitializer.tsx b/packages/module-web/src/client/schema-initializer/items/BlockInitializer.tsx new file mode 100644 index 0000000000..3f68f43d78 --- /dev/null +++ b/packages/module-web/src/client/schema-initializer/items/BlockInitializer.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { merge } from '@tachybase/schema'; + +import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '../../application'; + +// Block +export const BlockInitializer = (props) => { + const { item, schema, ...others } = props; + const { insert } = useSchemaInitializer(); + return ( + { + const s = merge(schema || {}, item.schema || {}); + item?.schemaInitialize?.(s); + insert(s); + props.onClick?.(s); + }} + /> + ); +}; + +export const BlockItemInitializer = () => { + const itemConfig = useSchemaInitializerItem(); + const { schema, ...others } = itemConfig; + return ; +}; diff --git a/packages/module-web/src/client/schema-initializer/items/CreateFilterActionInitializer.tsx b/packages/module-web/src/client/schema-initializer/items/CreateFilterActionInitializer.tsx new file mode 100644 index 0000000000..8e8708a670 --- /dev/null +++ b/packages/module-web/src/client/schema-initializer/items/CreateFilterActionInitializer.tsx @@ -0,0 +1,18 @@ +import React from 'react'; + +import { ActionInitializer } from './ActionInitializer'; + +export const CreateFilterActionInitializer = (props) => { + const schema = { + title: '{{ t("Filter") }}', + 'x-action': 'submit', + 'x-component': 'Action', + 'x-use-component-props': 'useFilterBlockActionProps', + 'x-designer': 'Action.Designer', + 'x-component-props': { + type: 'primary', + htmlType: 'submit', + }, + }; + return ; +}; diff --git a/packages/module-web/src/client/schema-initializer/items/CreateResetActionInitializer.tsx b/packages/module-web/src/client/schema-initializer/items/CreateResetActionInitializer.tsx new file mode 100644 index 0000000000..af9e9f0e49 --- /dev/null +++ b/packages/module-web/src/client/schema-initializer/items/CreateResetActionInitializer.tsx @@ -0,0 +1,13 @@ +import React from 'react'; + +import { ActionInitializer } from './ActionInitializer'; + +export const CreateResetActionInitializer = (props) => { + const schema = { + title: '{{ t("Reset") }}', + 'x-component': 'Action', + 'x-use-component-props': 'useResetBlockActionProps', + 'x-designer': 'Action.Designer', + }; + return ; +}; diff --git a/packages/module-web/src/client/schema-initializer/items/CustomFilterFormItemInitializer.tsx b/packages/module-web/src/client/schema-initializer/items/CustomFilterFormItemInitializer.tsx new file mode 100644 index 0000000000..a58cd3743d --- /dev/null +++ b/packages/module-web/src/client/schema-initializer/items/CustomFilterFormItemInitializer.tsx @@ -0,0 +1,448 @@ +import React, { memo, useCallback, useContext, useMemo } from 'react'; +import { ArrayItems, FormLayout } from '@tachybase/components'; +import { + Field, + observer, + onFieldValueChange, + Schema, + SchemaOptionsContext, + uid, + useField, + useFieldSchema, +} from '@tachybase/schema'; + +import { ConfigProvider, Space } from 'antd'; +import _ from 'lodash'; +import { useTranslation } from 'react-i18next'; + +import { useAPIClient } from '../../api-client'; +import { SchemaInitializerItem, useSchemaInitializerItem } from '../../application'; +import { SchemaSettings } from '../../application/schema-settings/SchemaSettings'; +import { useFormBlockContext } from '../../block-provider'; +import { ACLCollectionFieldProvider } from '../../built-in/acl'; +import { useCollectionManager_deprecated } from '../../collection-manager'; +import { useCollectionManager } from '../../data-source'; +import { i18n } from '../../i18n'; +import { + BlockItem, + EditDescription, + EditTitle, + EditTitleField, + FormDialog, + FormItem, + HTMLEncode, + SchemaComponent, + SchemaComponentOptions, + Select, + useCompile, + useDesignable, +} from '../../schema-component'; +import { + EditCustomDefaultValue, + EditFormulaTitleField, + GeneralSchemaDesigner, + SchemaSettingCollection, + SchemaSettingComponent, + SchemaSettingsDataScope, + SchemaSettingsDivider, + SchemaSettingsRemove, +} from '../../schema-settings'; +import { css, cx } from '../../style'; +import { useGlobalTheme } from '../../style/theme'; +import { gridRowColWrap } from '../utils'; + +const FieldComponentProps: React.FC = observer( + (props) => { + const { scope, components } = useContext(SchemaOptionsContext); + const schema = { + type: 'object', + properties: { + options: { + title: '{{t("Options")}}', + type: 'array', + 'x-decorator': 'FormItem', + 'x-component': 'ArrayItems', + items: { + type: 'object', + properties: { + space: { + type: 'void', + 'x-component': 'Space', + properties: { + sort: { + type: 'void', + 'x-decorator': 'FormItem', + 'x-component': 'ArrayItems.SortHandle', + }, + label: { + type: 'string', + 'x-decorator': 'FormItem', + 'x-component': 'Input', + 'x-component-props': { + placeholder: '{{t("Option label")}}', + }, + required: true, + }, + value: { + type: 'string', + 'x-decorator': 'FormItem', + 'x-component': 'Input', + 'x-component-props': { + placeholder: '{{t("Option value")}}', + }, + required: true, + }, + remove: { + type: 'void', + 'x-decorator': 'FormItem', + 'x-component': 'ArrayItems.Remove', + }, + }, + }, + }, + }, + properties: { + add: { + type: 'void', + title: '{{t("add")}}', + 'x-component': 'ArrayItems.Addition', + }, + }, + }, + }, + }; + + return ( + + + + ); + }, + { displayName: 'FieldComponentProps' }, +); + +export const useFieldComponents = () => { + const { t } = useTranslation(); + const options = [ + { label: t('Input'), value: 'Input' }, + { label: t('AutoComplete'), value: 'AutoComplete' }, + { label: t('Select'), value: 'Select' }, + { label: t('AssociationCascader'), value: 'AssociationCascader' }, + { label: t('DatePicker'), value: 'DatePicker' }, + { label: t('Radio group'), value: 'Radio.Group' }, + { label: t('Checkbox group'), value: 'Checkbox.Group' }, + ]; + return { + options, + values: options.map((option) => option.value), + }; +}; + +export const FilterCustomItemInitializer: React.FC<{ + insert?: any; +}> = memo((props) => { + const { locale } = useContext(ConfigProvider.ConfigContext); + const { t } = useTranslation(); + const { scope, components } = useContext(SchemaOptionsContext); + const { theme } = useGlobalTheme(); + const compile = useCompile(); + const { insert } = props; + const itemConfig = useSchemaInitializerItem(); + const { getInterface } = useCollectionManager_deprecated(); + const { collections, getCollectionFields } = useCollectionManager_deprecated(); + const allCollection = collections.map((value) => { + return { + label: value.title, + value: value.name, + }; + }); + const cm = useCollectionManager(); + const { options: fieldComponents, values: fieldComponentValues } = useFieldComponents(); + const api = useAPIClient(); + const handleClick = useCallback(async () => { + const values = await FormDialog( + t('Add custom field'), + () => ( + ({ + value: field.name, + label: compile(field.uiSchema?.title), + })) ?? [] + ); + }, + }} + components={{ ...components, FieldComponentProps, Select }} + > + + + + + + + ), + theme, + ).open({ + values: { + name: `f_${uid()}`, + }, + effects() { + onFieldValueChange('component', (field) => { + const name = field.value; + const collectionComponent = field.query('.collection').take() as Field; + const propsComponent = field.query('.props').take() as Field; + if (name === 'Select' || name === 'AutoComplete' || name === 'AssociationCascader') { + collectionComponent.setDisplay('visible'); + collectionComponent.setRequired(true); + propsComponent.setDisplay('none'); + propsComponent.setRequired(false); + } else if (name === 'CustomSelect') { + collectionComponent.setDisplay('none'); + collectionComponent.setRequired(false); + propsComponent.setDisplay('visible'); + propsComponent.setRequired(true); + } else { + collectionComponent.setDisplay('none'); + collectionComponent.setRequired(false); + propsComponent.setDisplay('none'); + propsComponent.setRequired(false); + } + }); + }, + }); + const { title, component, collection, associationField, props } = values; + const defaultSchema = getInterface(component)?.default?.uiSchema || {}; + const titleField = cm.getCollection(collection)?.titleField; + const name = uid(); + const schema = { + ...defaultSchema, + type: 'string', + title: title, + name: '__custom.' + name, + required: false, + 'x-component': component, + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': 'fieldSettings:FilterFormCustomSettings', + 'x-decorator': 'FilterFormItem', + 'x-decorator-props': collection, + 'x-component-props': { + ...defaultSchema['x-component-props'], + fieldNames: { + label: titleField, + value: titleField, + }, + associationField, + collection, + objectValue: true, + component: component, + ...props, + }, + collectionName: collection, + }; + if (component === 'DatePicker') { + schema['x-settings'] = 'fieldSettings:FilterFormItem'; + schema['x-designer-props'] = { interface: 'datetime' }; + } + insert(gridRowColWrap(schema)); + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [theme]); + return ; +}); + +export const FilterFormItemCustom = () => { + const { insertAdjacent } = useDesignable(); + return insertAdjacent('beforeEnd', s)} />; +}; + +export const FilterItemCustomDesigner: React.FC = () => { + const { t } = useTranslation(); + const fieldSchema = useFieldSchema(); + const fieldName = fieldSchema['name'] as string; + const name = fieldName.includes('__custom') ? fieldSchema['collectionName'] : fieldName; + const { form } = useFormBlockContext(); + const field = useField(); + const { dn } = useDesignable(); + const component = fieldSchema['x-component']; + return ( + + + + {component !== 'Input' ? ( + { + _.set(field.componentProps, 'params', { + ...field.componentProps?.params, + filter, + }); + fieldSchema['x-component-props']['params'] = field.componentProps.params; + dn.emit('patch', { + schema: { + ['x-uid']: fieldSchema['x-uid'], + 'x-component-props': fieldSchema['x-component-props'], + }, + }); + }} + /> + ) : null} + {component === 'Select' || component === 'AutoComplete' ? : null} + {component === 'Select' || component === 'AutoComplete' ? : null} + {component === 'Select' || component === 'AutoComplete' ? : null} + {component === 'Select' || component === 'AutoComplete' ? : null} + + + + + ); +}; + +export const useCollectionJoinFieldTitle = (name: string) => { + const { getCollection, getCollectionField } = useCollectionManager_deprecated(); + return useMemo(() => { + if (!name) { + return; + } + const [collectionName, ...fieldNames] = name.split('.'); + if (!fieldNames?.length) { + return; + } + const collection = getCollection(collectionName); + let cName: any = collectionName; + let field: any; + let title = Schema.compile(collection?.title, { t: i18n.t }); + while (cName && fieldNames.length > 0) { + const fileName = fieldNames.shift(); + field = getCollectionField(`${cName}.${fileName}`); + const fieldTitle = field?.uiSchema?.title || field?.name; + if (fieldTitle) { + title += ` / ${Schema.compile(fieldTitle, { t: i18n.t })}`; + } + if (field?.target) { + cName = field.target; + } else { + cName = null; + } + } + return title; + }, [name]); +}; + +export const FilterFormItem = observer( + (props: any) => { + const field = useField(); + const schema = useFieldSchema(); + const showTitle = schema['x-decorator-props']?.showTitle ?? true; + const extra = useMemo(() => { + return typeof field.description === 'string' ? ( +
'), + }} + /> + ) : ( + field.description + ); + }, [field.description]); + const className = useMemo(() => { + return cx( + css` + & .ant-space { + flex-wrap: wrap; + } + `, + { + [css` + > .ant-formily-item-label { + display: none; + } + `]: showTitle === false, + }, + ); + }, [showTitle]); + + return ( + + + + + + ); + }, + { displayName: 'FilterFormItem' }, +); diff --git a/packages/module-web/src/client/schema-initializer/items/CustomizeActionInitializer.tsx b/packages/module-web/src/client/schema-initializer/items/CustomizeActionInitializer.tsx new file mode 100644 index 0000000000..eb4113cfac --- /dev/null +++ b/packages/module-web/src/client/schema-initializer/items/CustomizeActionInitializer.tsx @@ -0,0 +1,9 @@ +import React from 'react'; + +import { BlockInitializer } from '.'; +import { useSchemaInitializerItem } from '../../application'; + +export const CustomizeActionInitializer = () => { + const itemConfig = useSchemaInitializerItem(); + return ; +}; diff --git a/packages/module-web/src/client/schema-initializer/items/DataBlockInitializer.tsx b/packages/module-web/src/client/schema-initializer/items/DataBlockInitializer.tsx new file mode 100644 index 0000000000..70d9fdaaad --- /dev/null +++ b/packages/module-web/src/client/schema-initializer/items/DataBlockInitializer.tsx @@ -0,0 +1,383 @@ +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; + +import { TableOutlined } from '@ant-design/icons'; +import { Divider, Empty, Input, MenuProps, Spin } from 'antd'; +import { useTranslation } from 'react-i18next'; + +import { + SchemaInitializerItem, + SchemaInitializerMenu, + useGetSchemaInitializerMenuItems, + useSchemaInitializer, +} from '../../application'; +import { DataSource } from '../../data-source'; +import { Collection, CollectionFieldOptions } from '../../data-source/collection/Collection'; +import { Icon } from '../../icon'; +import { useCompile } from '../../schema-component'; +import { useSchemaTemplateManager } from '../../schema-templates'; +import { useCollectionDataSourceItems } from '../utils'; + +const MENU_ITEM_HEIGHT = 40; +const STEP = 15; + +export const SearchCollections = ({ value: outValue, onChange }) => { + const { t } = useTranslation(); + const [value, setValue] = useState(outValue); + const inputRef = React.useRef(null); + + // 之所以要增加个内部的 value 是为了防止用户输入过快时造成卡顿的问题 + useEffect(() => { + setValue(outValue); + }, [outValue]); + + // TODO: antd 的 Input 的 autoFocus 有 BUG,会不生效,等待官方修复后再简化:https://github.com/ant-design/ant-design/issues/41239 + useEffect(() => { + // 1. 组件在第一次渲染时自动 focus,提高用户体验 + inputRef.current.input.focus(); + + // 2. 当组件已经渲染,并再次显示时,自动 focus + const observer = new IntersectionObserver((entries) => { + if (entries[0].isIntersecting) { + inputRef.current.input.focus(); + } + }); + + observer.observe(inputRef.current.input); + return () => { + observer.disconnect(); + }; + }, []); + + return ( +
e.stopPropagation()}> + { + e.stopPropagation(); + }} + onChange={(e) => { + onChange(e.target.value); + setValue(e.target.value); + }} + /> + +
+ ); +}; + +const LoadingItem = ({ loadMore, maxHeight }) => { + const spinRef = useRef(null); + useEffect(() => { + const el = spinRef.current; + if (!el) return; + + let container = el.parentElement; + while (container && container.tagName !== 'UL') { + container = container.parentElement; + } + + const checkLoadMore = function () { + if (!container) return; + // 判断滚动是否到达底部 + if (Math.abs(container.scrollHeight - container.scrollTop - container.clientHeight) <= MENU_ITEM_HEIGHT) { + // 到达底部,执行加载更多的操作 + loadMore(); + } + }; + + // 监听滚动,滚动到底部触发加载更多 + if (container) { + container.style.height = `${maxHeight - MENU_ITEM_HEIGHT}px`; + container.style.maxHeight = 'inherit'; + container.style.overflowY = 'scroll'; + container.addEventListener('scroll', checkLoadMore); + } + + return () => { + if (container) { + container.removeEventListener('scroll', checkLoadMore); + delete container.style.height; + } + }; + }, [loadMore, maxHeight]); + + return ( +
e.stopPropagation()}> + +
+ ); +}; + +export function useMenuSearch({ + data, + openKeys, + showType, + hideSearch, +}: { + data: any[]; + openKeys: string[]; + showType?: boolean; + hideSearch?: boolean; +}) { + const [searchValue, setSearchValue] = useState(''); + const [count, setCount] = useState(STEP); + + const isMuliSource = useMemo(() => data.length > 1, [data]); + const openKey = useMemo(() => { + return isMuliSource ? openKeys?.[1] : openKeys?.length > 0; + }, [openKeys]); + + useEffect(() => { + if (!openKey) { + setSearchValue(''); + } + }, [openKey]); + + const currentItems = useMemo(() => { + if (isMuliSource) { + if (!openKey) return []; + return data.find((item) => (item.key || item.name) === openKey)?.children || []; + } + return data[0]?.children || []; + }, [data, isMuliSource, openKey]); + + // 根据搜索的值进行处理 + const searchedItems = useMemo(() => { + if (!searchValue) return currentItems; + const lowerSearchValue = searchValue.toLocaleLowerCase(); + return currentItems.filter( + (item) => + (item.label || item.title) && + String(item.label || item.title) + .toLocaleLowerCase() + .includes(lowerSearchValue), + ); + }, [searchValue, currentItems]); + + const shouldLoadMore = useMemo(() => searchedItems.length > count, [count, searchedItems]); + + // 根据 count 进行懒加载处理 + const limitedSearchedItems = useMemo(() => { + return searchedItems.slice(0, count); + }, [searchedItems, count]); + + // 最终的返回结果 + const resultItems = useMemo(() => { + const res = []; + if (!hideSearch) { + // 开头:搜索框 + res.push( + Object.assign( + { + key: 'search', + label: ( + { + setCount(STEP); + setSearchValue(val); + }} + /> + ), + onClick({ domEvent }) { + domEvent.stopPropagation(); + }, + }, + // isMenuType 为了 `useSchemaInitializerMenuItems()` 里面处理判断标识的 + showType ? { isMenuType: true } : {}, + ), + ); + } + + // 中间:搜索的数据 + if (limitedSearchedItems.length > 0) { + // 有搜索结果 + res.push(...limitedSearchedItems); + if (shouldLoadMore) { + res.push( + Object.assign( + { + key: 'load-more', + label: ( + { + setCount((count) => count + STEP); + }} + /> + ), + }, + showType ? { isMenuType: true } : {}, + ), + ); + } + } else { + // 搜索结果为空 + res.push( + Object.assign( + { + key: 'empty', + style: { + height: 150, + }, + label: ( +
e.stopPropagation()}> + +
+ ), + }, + showType ? { isMenuType: true } : {}, + ), + ); + } + + return res; + }, [hideSearch, limitedSearchedItems, searchValue, shouldLoadMore, showType]); + + const res = useMemo(() => { + if (!isMuliSource) return resultItems; + return data.map((item) => { + if (openKey && item.key === openKey) { + return { + ...item, + children: resultItems, + }; + } else { + return { + ...item, + children: [], + }; + } + }); + }, [data, isMuliSource, openKey, resultItems]); + return res; +} + +export interface DataBlockInitializerProps { + templateWrap?: ( + templateSchema: any, + { + item, + fromOthersInPopup, + }: { + item: any; + fromOthersInPopup?: boolean; + }, + ) => any; + onCreateBlockSchema?: (args: any) => void; + createBlockSchema?: (args: any) => any; + icon?: string | React.ReactNode; + name: string; + title: string; + filter?: (options: { collection: Collection; associationField: CollectionFieldOptions }) => boolean; + filterDataSource?: (dataSource: DataSource) => boolean; + componentType: string; + onlyCurrentDataSource?: boolean; + hideSearch?: boolean; + showAssociationFields?: boolean; + /** 如果只有一项数据表时,不显示 children 列表 */ + hideChildrenIfSingleCollection?: boolean; + items?: ReturnType[]; + /** + * 是否是点击弹窗中的 Others 选项进入的 + */ + fromOthersInPopup?: boolean; + /** + * 隐藏弹窗中的 Other records 选项 + */ + hideOtherRecordsInPopup?: boolean; +} + +export const DataBlockInitializer = (props: DataBlockInitializerProps) => { + const { + templateWrap, + onCreateBlockSchema, + componentType, + icon = TableOutlined, + name, + title, + filter, + onlyCurrentDataSource, + hideSearch, + showAssociationFields, + hideChildrenIfSingleCollection, + filterDataSource, + items: itemsFromProps, + fromOthersInPopup, + hideOtherRecordsInPopup, + } = props; + const { insert, setVisible } = useSchemaInitializer(); + const compile = useCompile(); + const { getTemplateSchemaByMode } = useSchemaTemplateManager(); + const onClick = useCallback( + async ({ item }) => { + if (item.template) { + const s = await getTemplateSchemaByMode(item); + templateWrap ? insert(templateWrap(s, { item, fromOthersInPopup })) : insert(s); + } else { + if (onCreateBlockSchema) { + onCreateBlockSchema({ item, fromOthersInPopup }); + } + } + setVisible(false); + }, + [fromOthersInPopup, getTemplateSchemaByMode, insert, onCreateBlockSchema, setVisible, templateWrap], + ); + const items = + itemsFromProps || + // eslint-disable-next-line react-hooks/rules-of-hooks + useCollectionDataSourceItems({ + componentName: componentType, + filter, + filterDataSource, + onlyCurrentDataSource, + showAssociationFields, + dataBlockInitializerProps: props, + hideOtherRecordsInPopup, + }); + const getMenuItems = useGetSchemaInitializerMenuItems(onClick); + const childItems = useMemo(() => { + return getMenuItems(items, name); + }, [getMenuItems, items, name]); + const [openMenuKeys, setOpenMenuKeys] = useState([]); + const searchedChildren = useMenuSearch({ data: childItems, openKeys: openMenuKeys, hideSearch }); + const compiledMenuItems = useMemo(() => { + let children = searchedChildren.filter((item) => item.key !== 'search' && item.key !== 'empty'); + if (hideChildrenIfSingleCollection && children.length === 1) { + // 只有一项可选时,直接展开 + children = children[0].children; + } else { + children = searchedChildren; + } + return [ + { + key: name, + label: compile(title), + icon: typeof icon === 'string' ? : (icon as React.ReactNode), + onClick: (info) => { + if (info.key !== name) return; + onClick({ ...info, item: props }); + }, + children, + }, + ]; + }, [searchedChildren, hideChildrenIfSingleCollection, name, compile, title, icon, onClick, props]); + + if (childItems.length > 1 || (childItems.length === 1 && childItems[0].children?.length > 0)) { + return ( + { + setOpenMenuKeys(keys); + }} + items={compiledMenuItems} + /> + ); + } + + return ; +}; diff --git a/packages/module-web/src/client/schema-initializer/items/DeleteEventActionInitializer.tsx b/packages/module-web/src/client/schema-initializer/items/DeleteEventActionInitializer.tsx new file mode 100644 index 0000000000..cfcd275bed --- /dev/null +++ b/packages/module-web/src/client/schema-initializer/items/DeleteEventActionInitializer.tsx @@ -0,0 +1,21 @@ +import React from 'react'; + +import { ActionInitializer } from './ActionInitializer'; + +export const DeleteEventActionInitializer = (props) => { + const schema = { + title: '{{ t("Delete Event") }}', + 'x-action': 'deleteEvent', + 'x-component': 'Action', + 'x-designer': 'Action.Designer', + 'x-component-props': { + icon: 'DeleteOutlined', + }, + properties: { + modal: { + 'x-component': 'CalendarV2.DeleteEvent', + }, + }, + }; + return ; +}; diff --git a/packages/module-web/src/client/schema-initializer/items/FilterBlockInitializer.tsx b/packages/module-web/src/client/schema-initializer/items/FilterBlockInitializer.tsx new file mode 100644 index 0000000000..b980d50905 --- /dev/null +++ b/packages/module-web/src/client/schema-initializer/items/FilterBlockInitializer.tsx @@ -0,0 +1,3 @@ +import { DataBlockInitializer } from './DataBlockInitializer'; + +export const FilterBlockInitializer = DataBlockInitializer; diff --git a/packages/module-web/src/client/schema-initializer/items/G2PlotInitializer.tsx b/packages/module-web/src/client/schema-initializer/items/G2PlotInitializer.tsx new file mode 100644 index 0000000000..778aa7a93f --- /dev/null +++ b/packages/module-web/src/client/schema-initializer/items/G2PlotInitializer.tsx @@ -0,0 +1,18 @@ +import React from 'react'; + +import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '../../application'; + +export const G2PlotInitializer = () => { + const itemConfig = useSchemaInitializerItem(); + const { insert } = useSchemaInitializer(); + return ( + { + insert({ + ...itemConfig.schema, + }); + }} + /> + ); +}; diff --git a/packages/module-web/src/client/schema-initializer/items/InitializerWithSwitch.tsx b/packages/module-web/src/client/schema-initializer/items/InitializerWithSwitch.tsx new file mode 100644 index 0000000000..d64d7d1192 --- /dev/null +++ b/packages/module-web/src/client/schema-initializer/items/InitializerWithSwitch.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { merge } from '@tachybase/schema'; + +import { SchemaInitializerSwitch, useSchemaInitializer } from '../../application'; +import { useCurrentSchema } from '../utils'; + +export const InitializerWithSwitch = (props) => { + const { type, schema, item, remove: passInRemove, disabled } = props; + const { exists, remove } = useCurrentSchema( + schema?.[type] || item?.schema?.[type], + type, + item.find, + passInRemove ?? item.remove, + ); + const { insert } = useSchemaInitializer(); + return ( + { + if (disabled) { + return; + } + if (exists) { + return remove(); + } + const s = merge(schema || {}, item.schema || {}); + item?.schemaInitialize?.(s); + insert(s); + }} + /> + ); +}; diff --git a/packages/module-web/src/client/schema-initializer/items/RecordAssociationBlockInitializer.tsx b/packages/module-web/src/client/schema-initializer/items/RecordAssociationBlockInitializer.tsx new file mode 100644 index 0000000000..763ecf6536 --- /dev/null +++ b/packages/module-web/src/client/schema-initializer/items/RecordAssociationBlockInitializer.tsx @@ -0,0 +1,67 @@ +import React, { useCallback } from 'react'; + +import { TableOutlined } from '@ant-design/icons'; + +import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '../../application'; +import { useCollectionManager_deprecated } from '../../collection-manager'; +import { createTableBlockUISchema } from '../../modules/blocks/data-blocks/table/createTableBlockUISchema'; +import { useSchemaTemplateManager } from '../../schema-templates'; +import { useRecordCollectionDataSourceItems } from '../utils'; + +/** + * @deprecated + */ +export const RecordAssociationBlockInitializer = () => { + const itemConfig = useSchemaInitializerItem(); + const { onCreateBlockSchema, componentType, createBlockSchema, ...others } = itemConfig; + const { insert } = useSchemaInitializer(); + const { getTemplateSchemaByMode } = useSchemaTemplateManager(); + const { getCollection } = useCollectionManager_deprecated(); + const field = itemConfig.field; + const collection = getCollection(field.target); + const association = `${field.collectionName}.${field.name}`; + return ( + } + {...others} + onClick={async ({ item }) => { + if (item.template) { + const s = await getTemplateSchemaByMode(item); + insert(s); + } else { + insert( + createTableBlockUISchema({ + rowKey: collection.filterTargetKey, + dataSource: collection.dataSource, + association: association, + }), + ); + } + }} + items={useRecordCollectionDataSourceItems('Table', itemConfig, field.target, association)} + /> + ); +}; + +export function useCreateAssociationTableBlock() { + const { insert } = useSchemaInitializer(); + const { getCollection } = useCollectionManager_deprecated(); + + const createAssociationTableBlock = useCallback( + ({ item }) => { + const field = item.associationField; + const collection = getCollection(field.target); + + insert( + createTableBlockUISchema({ + rowKey: collection.filterTargetKey, + dataSource: collection.dataSource, + association: `${field.collectionName}.${field.name}`, + }), + ); + }, + [getCollection, insert], + ); + + return { createAssociationTableBlock }; +} diff --git a/packages/module-web/src/client/schema-initializer/items/RecordAssociationDetailsBlockInitializer.tsx b/packages/module-web/src/client/schema-initializer/items/RecordAssociationDetailsBlockInitializer.tsx new file mode 100644 index 0000000000..6eebbf5072 --- /dev/null +++ b/packages/module-web/src/client/schema-initializer/items/RecordAssociationDetailsBlockInitializer.tsx @@ -0,0 +1,64 @@ +import React, { useCallback } from 'react'; + +import { FormOutlined } from '@ant-design/icons'; + +import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '../../application'; +import { useCollectionManager_deprecated } from '../../collection-manager'; +import { createDetailsWithPaginationUISchema } from '../../modules/blocks/data-blocks/details-multi/createDetailsWithPaginationUISchema'; +import { useSchemaTemplateManager } from '../../schema-templates'; +import { useRecordCollectionDataSourceItems } from '../utils'; + +export const RecordAssociationDetailsBlockInitializer = () => { + const itemConfig = useSchemaInitializerItem(); + const { onCreateBlockSchema, componentType, createBlockSchema, ...others } = itemConfig; + const { insert } = useSchemaInitializer(); + const { getTemplateSchemaByMode } = useSchemaTemplateManager(); + const { getCollection } = useCollectionManager_deprecated(); + const field = itemConfig.field; + const collection = getCollection(field.target); + const resource = `${field.collectionName}.${field.name}`; + return ( + } + {...others} + onClick={async ({ item }) => { + if (item.template) { + const s = await getTemplateSchemaByMode(item); + insert(s); + } else { + insert( + createDetailsWithPaginationUISchema({ + dataSource: collection.dataSource, + association: resource, + rowKey: collection.filterTargetKey || 'id', + }), + ); + } + }} + items={useRecordCollectionDataSourceItems('Details', itemConfig, field.target, resource)} + /> + ); +}; + +export function useCreateAssociationDetailsBlock() { + const { insert } = useSchemaInitializer(); + const { getCollection } = useCollectionManager_deprecated(); + + const createAssociationDetailsBlock = useCallback( + ({ item }) => { + const field = item.associationField; + const collection = getCollection(field.target); + + insert( + createDetailsWithPaginationUISchema({ + dataSource: collection.dataSource, + association: `${field.collectionName}.${field.name}`, + rowKey: collection.filterTargetKey || 'id', + }), + ); + }, + [getCollection, insert], + ); + + return { createAssociationDetailsBlock }; +} diff --git a/packages/module-web/src/client/schema-initializer/items/RecordAssociationFormBlockInitializer.tsx b/packages/module-web/src/client/schema-initializer/items/RecordAssociationFormBlockInitializer.tsx new file mode 100644 index 0000000000..d20325bb38 --- /dev/null +++ b/packages/module-web/src/client/schema-initializer/items/RecordAssociationFormBlockInitializer.tsx @@ -0,0 +1,104 @@ +import React, { useCallback, useMemo } from 'react'; + +import { FormOutlined } from '@ant-design/icons'; + +import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '../../application'; +import { useCollectionManager_deprecated } from '../../collection-manager'; +import { createCreateFormBlockUISchema } from '../../modules/blocks/data-blocks/form/createCreateFormBlockUISchema'; +import { useSchemaTemplateManager } from '../../schema-templates'; +import { useRecordCollectionDataSourceItems } from '../utils'; + +/** + * @deprecated + */ +export const RecordAssociationFormBlockInitializer = () => { + const itemConfig = useSchemaInitializerItem(); + const { onCreateBlockSchema, componentType, createBlockSchema, ...others } = itemConfig; + const { insert } = useSchemaInitializer(); + const { getCollection } = useCollectionManager_deprecated(); + const { getTemplateSchemaByMode } = useSchemaTemplateManager(); + const field = itemConfig.field; + const collectionName = field.target; + const collection = useMemo(() => getCollection(collectionName), [collectionName]); + return ( + } + {...others} + onClick={async ({ item }) => { + if (item.template) { + const template = await getTemplateSchemaByMode(item); + if (item.template.componentName === 'FormItem') { + const blockSchema = createCreateFormBlockUISchema({ + dataSource: collection.dataSource, + association: `${field.collectionName}.${field.name}`, + templateSchema: template, + }); + if (item.mode === 'reference') { + blockSchema['x-template-key'] = item.template.key; + } + insert(blockSchema); + } else { + insert(template); + } + } else { + insert( + createCreateFormBlockUISchema({ + dataSource: collection.dataSource, + association: `${field.collectionName}.${field.name}`, + }), + ); + } + }} + items={useRecordCollectionDataSourceItems( + 'FormItem', + itemConfig, + collection, + `${field.collectionName}.${field.name}`, + )} + /> + ); +}; + +export function useCreateAssociationFormBlock() { + const { insert } = useSchemaInitializer(); + const { getCollection } = useCollectionManager_deprecated(); + + const createAssociationFormBlock = useCallback( + ({ item }) => { + const field = item.associationField; + const collection = getCollection(field.target); + + insert( + createCreateFormBlockUISchema({ + dataSource: collection.dataSource, + association: `${field.collectionName}.${field.name}`, + }), + ); + }, + [getCollection, insert], + ); + + const templateWrap = useCallback( + (templateSchema, { item }) => { + const field = item.associationField; + const collection = getCollection(field.target); + + if (item.template.componentName === 'FormItem') { + const blockSchema = createCreateFormBlockUISchema({ + dataSource: collection.dataSource, + association: `${field.collectionName}.${field.name}`, + templateSchema: templateSchema, + }); + if (item.mode === 'reference') { + blockSchema['x-template-key'] = item.template.key; + } + return blockSchema; + } else { + return templateSchema; + } + }, + [getCollection], + ); + + return { createAssociationFormBlock, templateWrap }; +} diff --git a/packages/module-web/src/client/schema-initializer/items/RecordAssociationGridCardBlockInitializer.tsx b/packages/module-web/src/client/schema-initializer/items/RecordAssociationGridCardBlockInitializer.tsx new file mode 100644 index 0000000000..8be5b698cf --- /dev/null +++ b/packages/module-web/src/client/schema-initializer/items/RecordAssociationGridCardBlockInitializer.tsx @@ -0,0 +1,68 @@ +import React, { useCallback } from 'react'; + +import { TableOutlined } from '@ant-design/icons'; + +import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '../../application'; +import { useCollectionManager_deprecated } from '../../collection-manager'; +import { createGridCardBlockUISchema } from '../../modules/blocks/data-blocks/grid-card/createGridCardBlockUISchema'; +import { useSchemaTemplateManager } from '../../schema-templates'; +import { useRecordCollectionDataSourceItems } from '../utils'; + +/** + * @deprecated + */ +export const RecordAssociationGridCardBlockInitializer = () => { + const itemConfig = useSchemaInitializerItem(); + const { onCreateBlockSchema, componentType, createBlockSchema, ...others } = itemConfig; + const { insert } = useSchemaInitializer(); + const { getTemplateSchemaByMode } = useSchemaTemplateManager(); + const { getCollection } = useCollectionManager_deprecated(); + const field = itemConfig.field; + const collection = getCollection(field.target); + const resource = `${field.collectionName}.${field.name}`; + + return ( + } + {...others} + onClick={async ({ item }) => { + if (item.template) { + const s = await getTemplateSchemaByMode(item); + insert(s); + } else { + insert( + createGridCardBlockUISchema({ + rowKey: collection.filterTargetKey, + dataSource: collection.dataSource, + association: resource, + }), + ); + } + }} + items={useRecordCollectionDataSourceItems('GridCard', itemConfig, field.target, resource)} + /> + ); +}; + +export function useCreateAssociationGridCardBlock() { + const { insert } = useSchemaInitializer(); + const { getCollection } = useCollectionManager_deprecated(); + + const createAssociationGridCardBlock = useCallback( + ({ item }) => { + const field = item.associationField; + const collection = getCollection(field.target); + + insert( + createGridCardBlockUISchema({ + rowKey: collection.filterTargetKey, + dataSource: collection.dataSource, + association: `${field.collectionName}.${field.name}`, + }), + ); + }, + [getCollection, insert], + ); + + return { createAssociationGridCardBlock }; +} diff --git a/packages/module-web/src/client/schema-initializer/items/RecordAssociationListBlockInitializer.tsx b/packages/module-web/src/client/schema-initializer/items/RecordAssociationListBlockInitializer.tsx new file mode 100644 index 0000000000..0240532ee7 --- /dev/null +++ b/packages/module-web/src/client/schema-initializer/items/RecordAssociationListBlockInitializer.tsx @@ -0,0 +1,65 @@ +import React, { useCallback } from 'react'; + +import { TableOutlined } from '@ant-design/icons'; + +import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '../../application'; +import { useCollectionManager_deprecated } from '../../collection-manager'; +import { createListBlockUISchema } from '../../modules/blocks/data-blocks/list/createListBlockUISchema'; +import { useSchemaTemplateManager } from '../../schema-templates'; +import { useRecordCollectionDataSourceItems } from '../utils'; + +export const RecordAssociationListBlockInitializer = () => { + const itemConfig = useSchemaInitializerItem(); + const { onCreateBlockSchema, componentType, createBlockSchema, ...others } = itemConfig; + const { insert } = useSchemaInitializer(); + const { getTemplateSchemaByMode } = useSchemaTemplateManager(); + const { getCollection } = useCollectionManager_deprecated(); + const field = itemConfig.field; + const collection = getCollection(field.target); + const resource = `${field.collectionName}.${field.name}`; + + return ( + } + {...others} + onClick={async ({ item }) => { + if (item.template) { + const s = await getTemplateSchemaByMode(item); + insert(s); + } else { + insert( + createListBlockUISchema({ + rowKey: collection.filterTargetKey, + dataSource: collection.dataSource, + association: resource, + }), + ); + } + }} + items={useRecordCollectionDataSourceItems('List', itemConfig, field.target, resource)} + /> + ); +}; + +export function useCreateAssociationListBlock() { + const { insert } = useSchemaInitializer(); + const { getCollection } = useCollectionManager_deprecated(); + + const createAssociationListBlock = useCallback( + ({ item }) => { + const field = item.associationField; + const collection = getCollection(field.target); + + insert( + createListBlockUISchema({ + rowKey: collection.filterTargetKey, + dataSource: collection.dataSource, + association: `${field.collectionName}.${field.name}`, + }), + ); + }, + [getCollection, insert], + ); + + return { createAssociationListBlock }; +} diff --git a/packages/module-web/src/client/schema-initializer/items/RecordReadPrettyAssociationFormBlockInitializer.tsx b/packages/module-web/src/client/schema-initializer/items/RecordReadPrettyAssociationFormBlockInitializer.tsx new file mode 100644 index 0000000000..a9b149c09c --- /dev/null +++ b/packages/module-web/src/client/schema-initializer/items/RecordReadPrettyAssociationFormBlockInitializer.tsx @@ -0,0 +1,103 @@ +import React, { useCallback } from 'react'; + +import { FormOutlined } from '@ant-design/icons'; + +import { SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerItem } from '../../application'; +import { useCollectionManager_deprecated } from '../../collection-manager'; +import { createDetailsUISchema } from '../../modules/blocks/data-blocks/details-single/createDetailsUISchema'; +import { useSchemaTemplateManager } from '../../schema-templates'; +import { useRecordCollectionDataSourceItems } from '../utils'; + +/** + * @deprecated + */ +export const RecordReadPrettyAssociationFormBlockInitializer = () => { + const itemConfig = useSchemaInitializerItem(); + const { onCreateBlockSchema, componentType, createBlockSchema, ...others } = itemConfig; + const { insert } = useSchemaInitializer(); + const { getCollection } = useCollectionManager_deprecated(); + const { getTemplateSchemaByMode } = useSchemaTemplateManager(); + + const field = itemConfig.field; + const collectionName = field.target; + const collection = getCollection(collectionName); + + const resource = `${field.collectionName}.${field.name}`; + + return ( + } + {...others} + onClick={async ({ item }) => { + if (item.template) { + const s = await getTemplateSchemaByMode(item); + if (item.template.componentName === 'ReadPrettyFormItem') { + const blockSchema = createDetailsUISchema({ + dataSource: collection.dataSource, + association: resource, + templateSchema: s, + }); + if (item.mode === 'reference') { + blockSchema['x-template-key'] = item.template.key; + } + insert(blockSchema); + } else { + insert(s); + } + } else { + insert( + createDetailsUISchema({ + association: resource, + dataSource: collection.dataSource, + }), + ); + } + }} + items={useRecordCollectionDataSourceItems('ReadPrettyFormItem', itemConfig, collection, resource)} + /> + ); +}; + +export function useCreateAssociationDetailsWithoutPagination() { + const { insert } = useSchemaInitializer(); + const { getCollection } = useCollectionManager_deprecated(); + + const createAssociationDetailsWithoutPagination = useCallback( + ({ item }) => { + const field = item.associationField; + const collection = getCollection(field.target); + + insert( + createDetailsUISchema({ + dataSource: collection.dataSource, + association: `${field.collectionName}.${field.name}`, + }), + ); + }, + [getCollection, insert], + ); + + const templateWrap = useCallback( + (templateSchema, { item }) => { + const field = item.associationField; + const collection = getCollection(field.target); + + if (item.template.componentName === 'ReadPrettyFormItem') { + const blockSchema = createDetailsUISchema({ + dataSource: collection.dataSource, + association: `${field.collectionName}.${field.name}`, + templateSchema: templateSchema, + }); + if (item.mode === 'reference') { + blockSchema['x-template-key'] = item.template.key; + } + return blockSchema; + } else { + return templateSchema; + } + }, + [getCollection], + ); + + return { createAssociationDetailsWithoutPagination, templateWrap }; +} diff --git a/packages/module-web/src/client/schema-initializer/items/SelectActionInitializer.tsx b/packages/module-web/src/client/schema-initializer/items/SelectActionInitializer.tsx new file mode 100644 index 0000000000..1fc2df0bad --- /dev/null +++ b/packages/module-web/src/client/schema-initializer/items/SelectActionInitializer.tsx @@ -0,0 +1,61 @@ +import React from 'react'; + +import { ActionInitializer } from './ActionInitializer'; + +export const SelectActionInitializer = (props) => { + const schema = { + type: 'void', + title: '{{ t("Select") }}', + 'x-action': 'update', + 'x-designer': 'Action.Designer', + 'x-component': 'Action', + 'x-component-props': { + openMode: 'drawer', + }, + properties: { + drawer: { + type: 'void', + 'x-component': 'AssociationField.Selector', + title: '{{ t("Select record") }}', + 'x-component-props': { + className: 'tb-record-picker-selector', + }, + properties: { + grid: { + type: 'void', + 'x-component': 'Grid', + 'x-initializer': 'popup:tableSelector:addBlock', + properties: {}, + }, + footer: { + 'x-component': 'Action.Container.Footer', + 'x-component-props': {}, + properties: { + actions: { + type: 'void', + 'x-component': 'ActionBar', + 'x-component-props': {}, + properties: { + submit: { + title: '{{ t("Submit") }}', + 'x-action': 'submit', + 'x-component': 'Action', + 'x-use-component-props': 'usePickActionProps', + // 'x-designer': 'Action.Designer', + 'x-toolbar': 'ActionSchemaToolbar', + 'x-settings': 'actionSettings:submit', + 'x-component-props': { + type: 'primary', + htmlType: 'submit', + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + return ; +}; diff --git a/packages/module-web/src/client/schema-initializer/items/SubmitActionInitializer.tsx b/packages/module-web/src/client/schema-initializer/items/SubmitActionInitializer.tsx new file mode 100644 index 0000000000..50ab05c772 --- /dev/null +++ b/packages/module-web/src/client/schema-initializer/items/SubmitActionInitializer.tsx @@ -0,0 +1,19 @@ +import React from 'react'; + +import { ActionInitializer } from './ActionInitializer'; + +export const SubmitActionInitializer = (props) => { + const schema = { + title: '{{ t("Submit") }}', + 'x-action': 'submit', + 'x-component': 'Action', + // 'x-designer': 'Action.Designer', + 'x-toolbar': 'ActionSchemaToolbar', + 'x-settings': 'actionSettings:submit', + 'x-component-props': { + type: 'primary', + htmlType: 'submit', + }, + }; + return ; +}; diff --git a/packages/module-web/src/client/schema-initializer/items/TableActionColumnInitializer.tsx b/packages/module-web/src/client/schema-initializer/items/TableActionColumnInitializer.tsx new file mode 100644 index 0000000000..115d092979 --- /dev/null +++ b/packages/module-web/src/client/schema-initializer/items/TableActionColumnInitializer.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { uid } from '@tachybase/schema'; + +import { useSchemaInitializerItem } from '../../application'; +import { InitializerWithSwitch } from './InitializerWithSwitch'; + +export const TableActionColumnInitializer = () => { + const schema = { + type: 'void', + title: '{{ t("Actions") }}', + 'x-decorator': 'TableV2.Column.ActionBar', + 'x-component': 'TableV2.Column', + 'x-component-props': { + width: 150, + fixed: 'right', + }, + 'x-designer': 'TableV2.ActionColumnDesigner', + 'x-initializer': 'table:configureItemActions', + 'x-action-column': 'actions', + properties: { + [uid()]: { + type: 'void', + 'x-decorator': 'DndContext', + 'x-component': 'Space', + 'x-component-props': { + split: '|', + }, + properties: {}, + }, + }, + }; + const itemConfig = useSchemaInitializerItem(); + return ; +}; diff --git a/packages/module-web/src/client/schema-initializer/items/index.tsx b/packages/module-web/src/client/schema-initializer/items/index.tsx new file mode 100644 index 0000000000..ac7f1736ca --- /dev/null +++ b/packages/module-web/src/client/schema-initializer/items/index.tsx @@ -0,0 +1,24 @@ +export * from '../../schema-component/antd/association-filter/ActionBarAssociationFilterAction'; +// association filter +export * from '../../schema-component/antd/association-filter/AssociationFilter'; +export * from '../../schema-component/antd/association-filter/AssociationFilterDesignerDelete'; +export * from '../../schema-component/antd/association-filter/AssociationFilterDesignerDisplayField'; +export * from './ActionInitializer'; +export * from './BlockInitializer'; +export * from './CreateFilterActionInitializer'; +export * from './CreateResetActionInitializer'; +export * from './CustomizeActionInitializer'; +export * from './DataBlockInitializer'; +export * from './DeleteEventActionInitializer'; +export * from './G2PlotInitializer'; +export * from './InitializerWithSwitch'; +export * from './RecordAssociationBlockInitializer'; +export * from './RecordAssociationDetailsBlockInitializer'; +export * from './RecordAssociationFormBlockInitializer'; +export * from './RecordAssociationGridCardBlockInitializer'; +export * from './RecordAssociationListBlockInitializer'; +export * from './RecordReadPrettyAssociationFormBlockInitializer'; +export * from './SelectActionInitializer'; +export * from './SubmitActionInitializer'; +export * from './TableActionColumnInitializer'; +export * from './CustomFilterFormItemInitializer'; diff --git a/packages/module-web/src/client/schema-initializer/style.ts b/packages/module-web/src/client/schema-initializer/style.ts new file mode 100644 index 0000000000..73971a9f87 --- /dev/null +++ b/packages/module-web/src/client/schema-initializer/style.ts @@ -0,0 +1,17 @@ +import { createStyles } from 'antd-style'; + +export const useStyles = createStyles(({ token }) => { + return { + nbMenuItemGroup: { + maxHeight: '50vh', + overflowY: 'auto', + }, + + nbMenuItemSubMenu: { + maxHeight: '50vh', + overflowY: 'auto', + boxShadow: token.boxShadowSecondary, + borderRadius: token.borderRadiusLG, + }, + }; +}); diff --git a/packages/module-web/src/client/schema-initializer/utils.ts b/packages/module-web/src/client/schema-initializer/utils.ts new file mode 100644 index 0000000000..896655400c --- /dev/null +++ b/packages/module-web/src/client/schema-initializer/utils.ts @@ -0,0 +1,1651 @@ +import { useMemo } from 'react'; +import { Field, Form, ISchema, Schema, uid, useFieldSchema, useForm } from '@tachybase/schema'; + +import _ from 'lodash'; +import { useTranslation } from 'react-i18next'; + +import { + DataBlockInitializer, + DataSource, + SchemaInitializerItemType, + useCollection, + useCollectionManager, + useDataSourceKey, + useFormActiveFields, + useFormBlockContext, +} from '../'; +import { FieldOptions, useCollection_deprecated, useCollectionManager_deprecated } from '../collection-manager'; +import { Collection, CollectionFieldOptions } from '../data-source/collection/Collection'; +import { useDataSourceManager } from '../data-source/data-source/DataSourceManagerProvider'; +import { isAssocField } from '../filter-provider/utils'; +import { useActionContext, useCompile, useDesignable } from '../schema-component'; +import { useSchemaTemplateManager } from '../schema-templates'; + +export const itemsMerge = (items1) => { + return items1; +}; + +export const gridRowColWrap = (schema: ISchema) => { + return { + type: 'void', + 'x-component': 'Grid.Row', + properties: { + [uid()]: { + type: 'void', + 'x-component': 'Grid.Col', + properties: { + [schema.name || uid()]: schema, + }, + }, + }, + }; +}; + +export const removeTableColumn = (schema, cb) => { + cb(schema.parent); +}; + +export const removeGridFormItem = (schema, cb) => { + cb(schema, { + removeParentsIfNoChildren: true, + breakRemoveOn: { + 'x-component': 'Grid', + }, + }); +}; + +export const useRemoveGridFormItem = () => { + const form = useForm(); + return (schema, cb) => { + cb(schema, { + removeParentsIfNoChildren: true, + breakRemoveOn: { + 'x-component': 'Grid', + }, + }); + delete form.values?.[schema.name]; + }; +}; + +export const findTableColumn = (schema: Schema, key: string, action: string, deepth = 0) => { + return schema.reduceProperties((buf, s) => { + if (s[key] === action) { + return s; + } + const c = s.reduceProperties((buf, s) => { + if (s[key] === action) { + return s; + } + return buf; + }); + if (c) { + return c; + } + return buf; + }); +}; +const quickEditField = [ + 'attachment', + 'textarea', + 'markdown', + 'json', + 'richText', + 'polygon', + 'circle', + 'point', + 'lineString', +]; + +export const useTableColumnInitializerFields = () => { + const { name, currentFields = [] } = useCollection_deprecated(); + const { getInterface, getCollection } = useCollectionManager_deprecated(); + const fieldSchema = useFieldSchema(); + const isSubTable = fieldSchema['x-component'] === 'AssociationField.SubTable'; + const form = useForm(); + const isReadPretty = isSubTable ? form.readPretty : true; + + return currentFields + .filter( + (field) => field?.interface && field?.interface !== 'subTable' && !field?.isForeignKey && !field?.treeChildren, + ) + .map((field) => { + const interfaceConfig = getInterface(field.interface); + const isFileCollection = field?.target && getCollection(field?.target)?.template === 'file'; + const schema = { + name: field.name, + 'x-collection-field': `${name}.${field.name}`, + 'x-component': 'CollectionField', + 'x-component-props': isFileCollection + ? { + fieldNames: { + label: 'preview', + value: 'id', + }, + } + : {}, + 'x-read-pretty': isReadPretty || field.uiSchema?.['x-read-pretty'], + 'x-decorator': isSubTable + ? quickEditField.includes(field.interface) || isFileCollection + ? 'QuickEdit' + : 'FormItem' + : null, + 'x-decorator-props': { + labelStyle: { + display: 'none', + }, + }, + }; + // interfaceConfig?.schemaInitialize?.(schema, { field, readPretty: true, block: 'Table' }); + return { + type: 'item', + name: field.name, + title: field?.uiSchema?.title || field.name, + Component: 'TableCollectionFieldInitializer', + find: findTableColumn, + remove: removeTableColumn, + schemaInitialize: (s) => { + interfaceConfig?.schemaInitialize?.(s, { + field, + readPretty: isReadPretty, + block: 'Table', + targetCollection: getCollection(field.target), + }); + }, + field, + schema, + } as SchemaInitializerItemType; + }); +}; + +export const useAssociatedTableColumnInitializerFields = () => { + const { name, fields } = useCollection_deprecated(); + const { getInterface, getCollectionFields, getCollection } = useCollectionManager_deprecated(); + const groups = fields + ?.filter((field) => { + return ['o2o', 'oho', 'obo', 'm2o'].includes(field.interface); + }) + ?.map((field) => { + const subFields = getCollectionFields(field.target); + const items = subFields + // ?.filter((subField) => subField?.interface && !['o2o', 'oho', 'obo', 'o2m', 'm2o', 'subTable', 'linkTo'].includes(subField?.interface)) + ?.filter( + (subField) => subField?.interface && !['subTable'].includes(subField?.interface) && !subField?.treeChildren, + ) + ?.map((subField) => { + const interfaceConfig = getInterface(subField.interface); + const schema = { + // type: 'string', + name: `${field.name}.${subField.name}`, + // title: subField?.uiSchema?.title || subField.name, + + 'x-component': 'CollectionField', + 'x-read-pretty': true, + 'x-collection-field': `${name}.${field.name}.${subField.name}`, + 'x-component-props': {}, + }; + + return { + type: 'item', + name: subField.name, + title: subField?.uiSchema?.title || subField.name, + Component: 'TableCollectionFieldInitializer', + find: findTableColumn, + remove: removeTableColumn, + schemaInitialize: (s) => { + interfaceConfig?.schemaInitialize?.(s, { + field: subField, + readPretty: true, + block: 'Table', + targetCollection: getCollection(field.target), + }); + }, + field: subField, + schema, + } as SchemaInitializerItemType; + }); + return { + type: 'subMenu', + name: field.uiSchema?.title, + title: field.uiSchema?.title, + children: items, + } as SchemaInitializerItemType; + }); + + return groups; +}; + +export const useInheritsTableColumnInitializerFields = () => { + const { name } = useCollection_deprecated(); + const { getInterface, getInheritCollections, getCollection, getParentCollectionFields } = + useCollectionManager_deprecated(); + const fieldSchema = useFieldSchema(); + const isSubTable = fieldSchema['x-component'] === 'AssociationField.SubTable'; + const form = useForm(); + const isReadPretty = isSubTable ? form.readPretty : true; + const inherits = getInheritCollections(name); + return inherits?.map((v) => { + const fields = getParentCollectionFields(v, name); + const targetCollection = getCollection(v); + return { + [targetCollection?.title]: fields + ?.filter((field) => { + return field?.interface; + }) + .map((k) => { + const interfaceConfig = getInterface(k.interface); + const isFileCollection = k?.target && getCollection(k?.target)?.template === 'file'; + const schema = { + name: `${k.name}`, + 'x-component': 'CollectionField', + 'x-read-pretty': isReadPretty || k.uiSchema?.['x-read-pretty'], + 'x-collection-field': `${name}.${k.name}`, + 'x-component-props': isFileCollection + ? { + fieldNames: { + label: 'preview', + value: 'id', + }, + } + : {}, + 'x-decorator': isSubTable + ? quickEditField.includes(k.interface) || isFileCollection + ? 'QuickEdit' + : 'FormItem' + : null, + 'x-decorator-props': { + labelStyle: { + display: 'none', + }, + }, + }; + return { + name: k?.uiSchema?.title || k.name, + type: 'item', + title: k?.uiSchema?.title || k.name, + Component: 'TableCollectionFieldInitializer', + find: findTableColumn, + remove: removeTableColumn, + schemaInitialize: (s) => { + interfaceConfig?.schemaInitialize?.(s, { + field: k, + readPretty: true, + block: 'Table', + targetCollection: getCollection(k?.target), + }); + }, + field: k, + schema, + } as SchemaInitializerItemType; + }), + }; + }); +}; + +export const useFormItemInitializerFields = (options?: any) => { + const { name, currentFields } = useCollection_deprecated(); + const { getInterface, getCollection } = useCollectionManager_deprecated(); + const form = useForm(); + const { readPretty = form.readPretty, block = 'Form' } = options || {}; + const { fieldSchema } = useActionContext(); + const action = fieldSchema?.['x-action']; + + return currentFields + ?.filter((field) => field?.interface && !field?.isForeignKey && !field?.treeChildren) + ?.map((field) => { + const interfaceConfig = getInterface(field.interface); + const targetCollection = getCollection(field.target); + const isFileCollection = field?.target && getCollection(field?.target)?.template === 'file'; + const isAssociationField = targetCollection; + const fieldNames = field?.uiSchema['x-component-props']?.['fieldNames']; + const schema = { + type: 'string', + name: field.name, + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': 'fieldSettings:FormItem', + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-collection-field': `${name}.${field.name}`, + 'x-component-props': isFileCollection + ? { + fieldNames: { + label: 'preview', + value: 'id', + }, + } + : isAssociationField && fieldNames + ? { + fieldNames: { ...fieldNames, label: targetCollection?.titleField || fieldNames.label }, + } + : {}, + 'x-read-pretty': field?.uiSchema?.['x-read-pretty'], + }; + const resultItem = { + type: 'item', + name: field.name, + title: field?.uiSchema?.title || field.name, + Component: 'CollectionFieldInitializer', + remove: removeGridFormItem, + schemaInitialize: (s) => { + interfaceConfig?.schemaInitialize?.(s, { + field, + block, + readPretty, + action, + targetCollection, + }); + }, + schema, + } as SchemaInitializerItemType; + if (block === 'Kanban') { + resultItem['find'] = (schema: Schema, key: string, action: string) => { + const s = findSchema(schema, 'x-component', block); + return findSchema(s, key, action); + }; + } + + return resultItem; + }); +}; + +// 筛选表单相关 +export const useFilterFormItemInitializerFields = (options?: any) => { + const { name, currentFields } = useCollection_deprecated(); + const { getInterface, getCollection } = useCollectionManager_deprecated(); + const form = useForm(); + const { readPretty = form.readPretty, block = 'FilterForm' } = options || {}; + const { snapshot, fieldSchema } = useActionContext(); + const action = fieldSchema?.['x-action']; + + return currentFields + ?.filter((field) => field?.interface && !field?.isForeignKey && getInterface(field.interface)?.filterable) + ?.map((field) => { + const interfaceConfig = getInterface(field.interface); + const targetCollection = getCollection(field.target); + let schema = { + type: 'string', + name: field.name, + required: false, + // 'x-designer': 'FormItem.FilterFormDesigner', + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': 'fieldSettings:FilterFormItem', + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-collection-field': `${name}.${field.name}`, + 'x-component-props': {}, + }; + if (isAssocField(field)) { + schema = { + type: 'string', + name: `${field.name}`, + required: false, + // 'x-designer': 'FormItem.FilterFormDesigner', + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': 'fieldSettings:FilterFormItem', + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-collection-field': `${name}.${field.name}`, + 'x-component-props': field.uiSchema?.['x-component-props'], + }; + } + const resultItem = { + name: field?.uiSchema?.title || field.name, + type: 'item', + title: field?.uiSchema?.title || field.name, + Component: 'CollectionFieldInitializer', + remove: removeGridFormItem, + schemaInitialize: (s) => { + interfaceConfig?.schemaInitialize?.(s, { + field, + block, + readPretty, + action, + targetCollection, + }); + }, + schema, + } as SchemaInitializerItemType; + + return resultItem; + }); +}; + +export const useAssociatedFormItemInitializerFields = (options?: any) => { + const { name, fields } = useCollection_deprecated(); + const { getInterface, getCollectionFields, getCollection } = useCollectionManager_deprecated(); + const form = useForm(); + const { readPretty = form.readPretty, block = 'Form' } = options || {}; + const interfaces = block === 'Form' ? ['m2o'] : ['o2o', 'oho', 'obo', 'm2o']; + const groups = fields + ?.filter((field) => { + return interfaces.includes(field.interface); + }) + ?.map((field) => { + const subFields = getCollectionFields(field.target); + const items = subFields + ?.filter( + (subField) => subField?.interface && !['subTable'].includes(subField?.interface) && !subField.treeChildren, + ) + ?.map((subField) => { + const interfaceConfig = getInterface(subField.interface); + const isFileCollection = field?.target && getCollection(field?.target)?.template === 'file'; + const schema = { + type: 'string', + name: `${field.name}.${subField.name}`, + // 'x-designer': 'FormItem.Designer', + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': 'fieldSettings:FormItem', + 'x-component': 'CollectionField', + 'x-read-pretty': readPretty, + 'x-component-props': { + 'pattern-disable': block === 'Form' && readPretty, + fieldNames: isFileCollection + ? { + label: 'preview', + value: 'id', + } + : undefined, + }, + 'x-decorator': 'FormItem', + 'x-collection-field': `${name}.${field.name}.${subField.name}`, + }; + return { + name: subField?.uiSchema?.title || subField.name, + type: 'item', + title: subField?.uiSchema?.title || subField.name, + Component: 'CollectionFieldInitializer', + remove: removeGridFormItem, + schemaInitialize: (s) => { + interfaceConfig?.schemaInitialize?.(s, { + field: subField, + block, + readPretty, + targetCollection: getCollection(field.target), + }); + }, + schema, + } as SchemaInitializerItemType; + }); + + return { + type: 'subMenu', + name: field.uiSchema?.title, + title: field.uiSchema?.title, + children: items, + } as SchemaInitializerItemType; + }); + return groups; +}; + +const getItem = ( + field: FieldOptions, + schemaName: string, + collectionName: string, + getCollectionFields, + processedCollections: string[], + level: number, + fieldPath?: string, +) => { + // TODO 懒加载的形式,这里不应该限制层数,因为实际上添加到界面上进查询条件后性能并不差,只有添加设计界面有性能问题 + if (level >= 3) { + return null; + } + + if (['m2o', 'obo', 'oho', 'o2m', 'm2m'].includes(field.interface)) { + if (processedCollections.includes(field.target)) return null; + const subFields = getCollectionFields(field.target); + const options = []; + subFields.forEach((subField) => { + if (['m2o', 'obo', 'oho', 'o2m', 'm2m'].includes(subField.interface)) { + options.push( + getResultSchema(`${schemaName}.${subField.name}`, subField, collectionName, (fieldPath || '') + field.key), + ); + } + options.push( + getItem( + subField, + `${schemaName}.${subField.name}`, + collectionName, + getCollectionFields, + [...processedCollections, field.target], + level + 1, + (fieldPath || '') + field.key, + ), + ); + }); + return { + type: 'subMenu', + name: field.uiSchema?.title, + title: field.uiSchema?.title, + key: (fieldPath || '') + field.key + 'menu', + children: options.filter(Boolean), + } as SchemaInitializerItemType; + } + + if (isAssocField(field)) return null; + + return getResultSchema(schemaName, field, collectionName, (fieldPath || '') + field.key); +}; + +const getResultSchema = (schemaName, field, collectionName, fieldPath) => { + const schema = { + type: 'string', + name: schemaName, + // 'x-designer': 'FormItem.FilterFormDesigner', + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': 'fieldSettings:FilterFormItem', + 'x-designer-props': { + // 在 useOperatorList 中使用,用于获取对应的操作符列表 + interface: field.interface, + }, + 'x-component': 'CollectionField', + 'x-read-pretty': false, + 'x-decorator': 'FormItem', + 'x-collection-field': `${collectionName}.${schemaName}`, + }; + const result = { + name: field.uiSchema?.title || field.name, + type: 'item', + key: (field.uiSchema?.title || field.name) + fieldPath, + title: field.uiSchema?.title || field.name, + Component: 'CollectionFieldInitializer', + remove: removeGridFormItem, + schema, + } as SchemaInitializerItemType; + return result; +}; + +// 筛选表单相关 +export const useFilterAssociatedFormItemInitializerFields = () => { + const { name, fields } = useCollection_deprecated(); + const { getCollectionFields } = useCollectionManager_deprecated(); + const interfaces = ['m2o', 'obo', 'oho', 'o2m', 'm2m']; + const groups = fields + ?.filter((field) => { + return interfaces.includes(field.interface); + }) + ?.map((field) => getItem(field, field.name, name, getCollectionFields, [], 0)); + return groups; +}; + +export const useInheritsFormItemInitializerFields = (options?) => { + const { name } = useCollection_deprecated(); + const { getInterface, getInheritCollections, getCollection, getParentCollectionFields } = + useCollectionManager_deprecated(); + const inherits = getInheritCollections(name); + const { snapshot } = useActionContext(); + const form = useForm(); + + return inherits?.map((v) => { + const fields = getParentCollectionFields(v, name); + const { readPretty = form.readPretty, block = 'Form', component = 'CollectionField' } = options || {}; + const targetCollection = getCollection(v); + return { + [targetCollection?.title]: fields + ?.filter((field) => field?.interface && !field?.isForeignKey) + ?.map((field) => { + const interfaceConfig = getInterface(field.interface); + const targetCollection = getCollection(field.target); + // const component = + // field.interface === 'o2m' && targetCollection?.template !== 'file' && !snapshot + // ? 'TableField' + // : 'CollectionField'; + const schema = { + type: 'string', + name: field.name, + title: field?.uiSchema?.title || field.name, + // 'x-designer': 'FormItem.Designer', + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': 'fieldSettings:FormItem', + 'x-component': component, + 'x-decorator': 'FormItem', + 'x-collection-field': `${name}.${field.name}`, + 'x-component-props': {}, + 'x-read-pretty': field?.uiSchema?.['x-read-pretty'], + }; + return { + name: field?.uiSchema?.title || field.name, + type: 'item', + title: field?.uiSchema?.title || field.name, + Component: 'CollectionFieldInitializer', + remove: removeGridFormItem, + schemaInitialize: (s) => { + interfaceConfig?.schemaInitialize?.(s, { + field, + block, + readPretty, + targetCollection, + }); + }, + schema, + } as SchemaInitializerItemType; + }), + }; + }); +}; + +// 筛选表单相关 +export const useFilterInheritsFormItemInitializerFields = (options?) => { + const { name } = useCollection_deprecated(); + const { getInterface, getInheritCollections, getCollection, getParentCollectionFields } = + useCollectionManager_deprecated(); + const inherits = getInheritCollections(name); + const { snapshot } = useActionContext(); + const form = useForm(); + + return inherits?.map((v) => { + const fields = getParentCollectionFields(v, name); + const { readPretty = form.readPretty, block = 'Form' } = options || {}; + const targetCollection = getCollection(v); + return { + [targetCollection.title]: fields + ?.filter((field) => field?.interface && !field?.isForeignKey && getInterface(field.interface)?.filterable) + ?.map((field) => { + const interfaceConfig = getInterface(field.interface); + const targetCollection = getCollection(field.target); + // const component = + // field.interface === 'o2m' && targetCollection?.template !== 'file' && !snapshot + // ? 'TableField' + // : 'CollectionField'; + const schema = { + type: 'string', + name: field.name, + title: field?.uiSchema?.title || field.name, + required: false, + // 'x-designer': 'FormItem.FilterFormDesigner', + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': 'fieldSettings:FilterFormItem', + 'x-component': 'CollectionField', + 'x-decorator': 'FormItem', + 'x-collection-field': `${name}.${field.name}`, + 'x-component-props': {}, + 'x-read-pretty': field?.uiSchema?.['x-read-pretty'], + }; + return { + name: field?.uiSchema?.title || field.name, + type: 'item', + title: field?.uiSchema?.title || field.name, + Component: 'CollectionFieldInitializer', + remove: removeGridFormItem, + schemaInitialize: (s) => { + interfaceConfig?.schemaInitialize?.(s, { + field, + block, + readPretty, + targetCollection, + }); + }, + schema, + } as SchemaInitializerItemType; + }), + }; + }); +}; +export const useCustomFormItemInitializerFields = (options?: any) => { + const { name, currentFields } = useCollection_deprecated(); + const { getInterface, getCollection } = useCollectionManager_deprecated(); + const form = useForm(); + const { readPretty = form.readPretty, block = 'Form' } = options || {}; + const remove = useRemoveGridFormItem(); + return currentFields + ?.filter((field) => { + return ( + field?.interface && + !field?.uiSchema?.['x-read-pretty'] && + field.interface !== 'snapshot' && + field.type !== 'sequence' + ); + }) + ?.map((field) => { + const interfaceConfig = getInterface(field.interface); + const schema = { + type: 'string', + name: field.name, + title: field?.uiSchema?.title || field.name, + // 'x-designer': 'FormItem.Designer', + 'x-toolbar': 'FormItemSchemaToolbar', + 'x-settings': 'fieldSettings:FormItem', + 'x-component': 'AssignedField', + 'x-decorator': 'FormItem', + 'x-collection-field': `${name}.${field.name}`, + }; + return { + name: field?.uiSchema?.title || field.name, + type: 'item', + title: field?.uiSchema?.title || field.name, + Component: 'CollectionFieldInitializer', + remove: remove, + schemaInitialize: (s) => { + interfaceConfig?.schemaInitialize?.(s, { + field, + block, + readPretty, + targetCollection: getCollection(field.target), + }); + }, + schema, + } as SchemaInitializerItemType; + }); +}; + +export const findSchema = (schema: Schema, key: string, action: string) => { + if (!Schema.isSchemaInstance(schema)) return null; + return schema.reduceProperties((buf, s) => { + if (s[key] === action) { + return s; + } + if (s['x-component'] !== 'Action.Container' && s['x-component'] !== 'AssociationField.Viewer') { + const c = findSchema(s, key, action); + if (c) { + return c; + } + } + + return buf; + }); +}; + +const removeSchema = (schema, cb) => { + return cb(schema); +}; + +const recursiveParent = (schema: Schema) => { + if (!schema.parent) return null; + + if (schema.parent['x-initializer']) return schema.parent; + + return recursiveParent(schema.parent); +}; + +export const useCurrentSchema = (action: string, key: string, find = findSchema, rm = removeSchema) => { + const { removeActiveFieldName } = useFormActiveFields() || {}; + const { form }: { form?: Form } = useFormBlockContext(); + let fieldSchema = useFieldSchema(); + if (!fieldSchema?.['x-initializer'] && fieldSchema?.['x-decorator'] === 'FormItem') { + const recursiveInitializerSchema = recursiveParent(fieldSchema); + if (recursiveInitializerSchema) { + fieldSchema = recursiveInitializerSchema; + } + } + const { remove } = useDesignable(); + const schema = find(fieldSchema, key, action); + return { + schema, + exists: !!schema, + remove() { + removeActiveFieldName?.(schema.name); + form?.query(schema.name).forEach((field: Field) => { + field.setInitialValue?.(null); + field.reset?.(); + }); + if (schema) { + rm(schema, remove); + } + }, + }; +}; + +/** + * @deprecated + * 待统一卡片的创建之后,将废弃该方法 + */ +export const useRecordCollectionDataSourceItems = ( + componentName, + item = null, + collectionName = null, + resourceName = null, +) => { + const { t } = useTranslation(); + const collection = useCollection_deprecated(); + const { getTemplatesByCollection } = useSchemaTemplateManager(); + const templates = getTemplatesByCollection(collection.dataSource, collectionName || collection.name) + .filter((template) => { + return componentName && template.componentName === componentName; + }) + .filter((template) => { + return ['FormItem', 'ReadPrettyFormItem'].includes(componentName) || template.resourceName === resourceName; + }); + if (!templates.length) { + return []; + } + const index = 0; + return [ + { + key: `${collectionName || componentName}_table_blank`, + type: 'item', + name: collection.name, + title: t('Blank block'), + item, + }, + { + type: 'divider', + }, + { + key: `${collectionName || componentName}_table_subMenu_${index}_copy`, + type: 'subMenu', + name: 'copy', + title: t('Duplicate template'), + children: templates.map((template) => { + const templateName = ['FormItem', 'ReadPrettyFormItem'].includes(template?.componentName) + ? `${template?.name} ${t('(Fields only)')}` + : template?.name; + return { + type: 'item', + mode: 'copy', + name: collection.name, + template, + item, + title: templateName || t('Untitled'), + }; + }), + }, + { + key: `${collectionName || componentName}_table_subMenu_${index}_ref`, + type: 'subMenu', + name: 'ref', + title: t('Reference template'), + children: templates.map((template) => { + const templateName = ['FormItem', 'ReadPrettyFormItem'].includes(template?.componentName) + ? `${template?.name} ${t('(Fields only)')}` + : template?.name; + return { + type: 'item', + mode: 'reference', + name: collection.name, + template, + item, + title: templateName || t('Untitled'), + }; + }), + }, + ]; +}; + +export const useCollectionDataSourceItems = ({ + componentName, + filter = () => true, + onlyCurrentDataSource = false, + showAssociationFields, + filterDataSource, + dataBlockInitializerProps, + hideOtherRecordsInPopup, +}: { + componentName; + filter?: (options: { collection?: Collection; associationField?: CollectionFieldOptions }) => boolean; + onlyCurrentDataSource?: boolean; + showAssociationFields?: boolean; + filterDataSource?: (dataSource?: DataSource) => boolean; + dataBlockInitializerProps?: any; + /** + * 隐藏弹窗中的 Other records 选项 + */ + hideOtherRecordsInPopup?: boolean; +}) => { + const { t } = useTranslation(); + const dm = useDataSourceManager(); + const dataSourceKey = useDataSourceKey(); + const collection = useCollection(); + const associationFields = useAssociationFields({ componentName, filterCollections: filter, showAssociationFields }); + + let allCollections = dm.getAllCollections({ + filterCollection: (collection) => { + if (onlyCurrentDataSource && collection.dataSource !== dataSourceKey) { + return false; + } + return filter({ collection }); + }, + filterDataSource, + }); + if (onlyCurrentDataSource) { + allCollections = allCollections.filter((collection) => collection.key === dataSourceKey); + } + + const { getTemplatesByCollection } = useSchemaTemplateManager(); + + const noAssociationMenu = useMemo(() => { + return allCollections.map(({ key, displayName, collections }) => ({ + name: key, + label: displayName, + type: 'subMenu', + children: [ + ...getChildren({ + collections, + componentName, + searchValue: '', + dataSource: key, + getTemplatesByCollection, + t, + }).sort((item) => { + // fix + const inherits = _.toArray(collection?.inherits || []); + if (item.name === collection?.name || inherits.some((inheritName) => inheritName === item.name)) return -1; + }), + ], + })); + }, [allCollections, collection?.inherits, collection?.name, componentName, getTemplatesByCollection, t]); + + // + // showAssociationFields 的值是固定不变的,所以在 if 语句里使用 hooks 是安全的 + if (showAssociationFields) { + // eslint-disable-next-line react-hooks/rules-of-hooks + return useMemo(() => { + const currentRecord = { + name: 'currentRecord', + collectionName: collection.name, + dataSource: collection.dataSource, + Component: DataBlockInitializer, + // 目的是使点击无效 + onClick() {}, + componentProps: { + ...dataBlockInitializerProps, + icon: null, + title: t('Current record'), + name: 'currentRecord', + hideSearch: false, + hideChildrenIfSingleCollection: true, + items: noAssociationMenu, + }, + }; + const associationRecords = { + name: 'associationRecords', + Component: DataBlockInitializer, + // 目的是使点击无效 + onClick() {}, + componentProps: { + ...dataBlockInitializerProps, + icon: null, + title: t('Associated records'), + name: 'associationRecords', + hideSearch: false, + items: [ + { + name: 'associationFields', + label: t('Association fields'), + type: 'subMenu', // 这里套一层 subMenu 是因为 DataBlockInitializer 组件需要这样的数据结构,其实这层 subMenu 最终是不会渲染出来的 + children: associationFields, + }, + ], + }, + }; + const componentTypeMap = { + ReadPrettyFormItem: 'Details', + }; + const otherRecords = { + name: 'otherRecords', + Component: DataBlockInitializer, + // 目的是使点击无效 + onClick() {}, + componentProps: { + icon: null, + title: t('Other records'), + name: 'otherRecords', + showAssociationFields: false, + onlyCurrentDataSource: false, + hideChildrenIfSingleCollection: false, + onCreateBlockSchema: dataBlockInitializerProps.onCreateBlockSchema, + fromOthersInPopup: true, + componentType: componentTypeMap[componentName] || componentName, + filter({ collection: c, associationField }) { + return true; + }, + }, + }; + + let children; + + const _associationRecords = associationFields.length ? associationRecords : null; + if (noAssociationMenu[0].children.length && associationFields.length) { + if (hideOtherRecordsInPopup) { + children = [currentRecord, _associationRecords]; + } else { + children = [currentRecord, _associationRecords, otherRecords]; + } + } else if (noAssociationMenu[0].children.length) { + if (hideOtherRecordsInPopup) { + // 当可选数据表只有一个时,实现只点击一次卡片 menu 就能添加卡片 + if (noAssociationMenu[0].children.length <= 1) { + noAssociationMenu[0].children = (noAssociationMenu[0].children[0]?.children as any) || []; + return noAssociationMenu; + } + children = [currentRecord]; + } else { + children = [currentRecord, otherRecords]; + } + } else { + if (hideOtherRecordsInPopup) { + children = [_associationRecords]; + } else { + children = [_associationRecords, otherRecords]; + } + } + + return [ + { + name: 'records', + label: t('Records'), + type: 'subMenu', + children: children.filter(Boolean), + }, + ]; + }, [ + associationFields, + collection.dataSource, + collection.name, + componentName, + dataBlockInitializerProps, + hideOtherRecordsInPopup, + noAssociationMenu, + t, + ]); + } + + return noAssociationMenu; +}; + +/** + * @deprecated + * 已弃用,请使用 createDetailsUISchema 和 createDetailsWithPaginationUISchema 替代 + * @param options + * @returns + */ +export const createDetailsBlockSchema = (options: { + collection: string; + dataSource: string; + rowKey?: string; + formItemInitializers?: string; + actionInitializers?: string; + association?: string; + template?: any; + settings?: string; + action?: string; + [key: string]: any; +}) => { + const { + formItemInitializers = 'details:configureFields', + actionInitializers = 'detailsWithPaging:configureActions', + collection, + dataSource, + association, + template, + settings, + action = 'list', + ...others + } = options; + const resourceName = association || collection; + const schema: ISchema = { + type: 'void', + 'x-acl-action': action === 'get' ? `${resourceName}:get` : `${resourceName}:view`, + 'x-decorator': 'DetailsBlockProvider', + 'x-decorator-props': { + dataSource, + collection, + association, + readPretty: true, + action, + ...(action === 'list' + ? { + params: { + pageSize: 1, + }, + } + : {}), + ...others, + }, + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': settings, + 'x-component': 'CardItem', + properties: { + [uid()]: { + type: 'void', + 'x-component': 'Details', + 'x-use-component-props': 'useDetailsBlockProps', + 'x-read-pretty': true, + properties: { + [uid()]: { + type: 'void', + 'x-initializer': actionInitializers, + 'x-component': 'ActionBar', + 'x-component-props': { + style: { + marginBottom: 24, + }, + }, + properties: {}, + }, + grid: template || { + type: 'void', + 'x-component': 'Grid', + 'x-initializer': formItemInitializers, + properties: {}, + }, + ...(action === 'list' + ? { + pagination: { + type: 'void', + 'x-component': 'Pagination', + 'x-use-component-props': 'useDetailsPaginationProps', + }, + } + : {}), + }, + }, + }, + }; + return schema; +}; + +/** + * @deprecated + * 已弃用,请使用 createCreateFormBlockUISchema 或者 createEditFormBlockUISchema 替代 + * @param options + * @returns + */ +export const createFormBlockSchema = (options: { + formItemInitializers?: string; + actionInitializers?: string; + collection: string; + resource?: string; + dataSource?: string; + association?: string; + action?: string; + actions?: Record; + template?: any; + title?: string; + settings?: any; + 'x-designer'?: string; + [key: string]: any; +}) => { + const { + formItemInitializers = 'form:configureFields', + actionInitializers = 'createForm:configureActions', + collection, + resource, + dataSource, + association, + action, + actions = {}, + 'x-designer': designer = 'FormV2.Designer', + template, + title, + settings, + ...others + } = options; + const resourceName = resource || association || collection; + const schema: ISchema = { + type: 'void', + 'x-acl-action-props': { + skipScopeCheck: !action, + }, + 'x-acl-action': action ? `${resourceName}:update` : `${resourceName}:create`, + 'x-decorator': 'FormBlockProvider', + 'x-decorator-props': { + ...others, + action, + dataSource, + resource: resourceName, + collection, + association, + }, + 'x-toolbar': 'BlockSchemaToolbar', + ...(settings ? { 'x-settings': settings } : { 'x-designer': designer }), + 'x-component': 'CardItem', + 'x-component-props': { + title, + }, + properties: { + [uid()]: { + type: 'void', + 'x-component': 'FormV2', + 'x-use-component-props': 'useFormBlockProps', + properties: { + [uid()]: { + type: 'void', + 'x-initializer': actionInitializers, + 'x-component': 'ActionBar', + 'x-component-props': { + style: { + marginBottom: 'var(--tb-spacing)', + }, + }, + properties: actions, + }, + grid: template || { + type: 'void', + 'x-component': 'Grid', + 'x-initializer': formItemInitializers, + properties: {}, + }, + }, + }, + }, + }; + return schema; +}; + +/** + * @deprecated + * 已弃用,可以使用 createDetailsBlockSchema 替换 + * @param options + * @returns + */ +export const createReadPrettyFormBlockSchema = (options) => { + const { + formItemInitializers = 'details:configureFields', + actionInitializers = 'details:configureActions', + collection, + association, + dataSource, + resource, + template, + settings, + ...others + } = options; + const resourceName = resource || association || collection; + const schema: ISchema = { + type: 'void', + 'x-acl-action': `${resourceName}:get`, + 'x-decorator': 'FormBlockProvider', + 'x-decorator-props': { + resource: resourceName, + collection, + association, + dataSource, + readPretty: true, + action: 'get', + useParams: '{{ useParamsFromRecord }}', + ...others, + }, + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': settings, + 'x-component': 'CardItem', + properties: { + [uid()]: { + type: 'void', + 'x-component': 'FormV2', + 'x-use-component-props': 'useFormBlockProps', + 'x-read-pretty': true, + properties: { + [uid()]: { + type: 'void', + 'x-initializer': actionInitializers, + 'x-component': 'ActionBar', + 'x-component-props': { + style: { + marginBottom: 24, + }, + }, + properties: {}, + }, + grid: template || { + type: 'void', + 'x-component': 'Grid', + 'x-initializer': formItemInitializers, + properties: {}, + }, + }, + }, + }, + }; + + return schema; +}; + +/** + * @deprecated + * 已弃用,可以使用 createTableBlockUISchema 替换 + * @param options + * @returns + */ +export const createTableBlockSchema = (options) => { + const { + collection, + rowKey, + tableActionInitializers, + tableColumnInitializers, + tableActionColumnInitializers, + tableBlockProvider, + disableTemplate, + dataSource, + blockType, + pageSize = 20, + ...others + } = options; + const schema: ISchema = { + type: 'void', + 'x-decorator': tableBlockProvider ?? 'TableBlockProvider', + 'x-acl-action': `${collection}:list`, + 'x-decorator-props': { + collection, + dataSource, + action: 'list', + params: { + pageSize, + }, + rowKey, + showIndex: true, + dragSort: false, + disableTemplate: disableTemplate ?? false, + blockType, + ...others, + }, + 'x-toolbar': 'BlockSchemaToolbar', + 'x-settings': 'blockSettings:table', + 'x-component': 'CardItem', + 'x-filter-targets': [], + properties: { + actions: { + type: 'void', + 'x-initializer': tableActionInitializers ?? 'table:configureActions', + 'x-component': 'ActionBar', + 'x-component-props': { + style: { + marginBottom: 'var(--tb-spacing)', + }, + }, + properties: {}, + }, + [uid()]: { + type: 'array', + 'x-initializer': tableColumnInitializers ?? 'table:configureColumns', + 'x-component': 'TableV2', + 'x-use-component-props': 'useTableBlockProps', + 'x-component-props': { + rowKey: 'id', + rowSelection: { + type: 'checkbox', + }, + }, + properties: { + actions: { + type: 'void', + title: '{{ t("Actions") }}', + 'x-action-column': 'actions', + 'x-decorator': 'TableV2.Column.ActionBar', + 'x-component': 'TableV2.Column', + 'x-component-props': { + width: 150, + fixed: 'right', + }, + 'x-designer': 'TableV2.ActionColumnDesigner', + 'x-initializer': tableActionColumnInitializers ?? 'table:configureItemActions', + properties: { + [uid()]: { + type: 'void', + 'x-decorator': 'DndContext', + 'x-component': 'Space', + 'x-component-props': { + split: '|', + }, + properties: {}, + }, + }, + }, + }, + }, + }, + }; + return schema; +}; + +const getChildren = ({ + collections, + dataSource, + componentName, + searchValue, + getTemplatesByCollection, + t, +}: { + collections: any[]; + componentName: string; + searchValue: string; + dataSource: string; + getTemplatesByCollection: (dataSource: string, collectionName: string, resourceName?: string) => any; + t; +}) => { + return collections + ?.filter((item) => { + if (item.inherit) { + return false; + } + if (!item.filterTargetKey) { + return false; + } else if ( + ['Kanban', 'FormItem'].includes(componentName) && + ((item.template === 'view' && !item.writableView) || item.template === 'sql') + ) { + return false; + } else if (item.template === 'file' && ['Kanban', 'FormItem', 'Calendar'].includes(componentName)) { + return false; + } else { + const title = item.title || item.tableName; + if (!title) { + return false; + } + return title.toUpperCase().includes(searchValue.toUpperCase()) && !(item?.isThrough && item?.autoCreate); + } + }) + ?.map((item, index) => { + const title = item.title || item.tableName || item.label; + const templates = getTemplatesByCollection(dataSource, item.name).filter((template) => { + return ( + componentName && + template.componentName === componentName && + (['FormItem', 'ReadPrettyFormItem'].includes(componentName) || + !template.resourceName || + template.resourceName === item.name) + ); + }); + if (!templates.length) { + return { + type: 'item', + name: item.name, + title, + dataSource, + }; + } + 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'), + }, + { + type: 'divider', + }, + { + key: `${componentName}_table_subMenu_${index}_copy`, + type: 'subMenu', + name: 'copy', + dataSource, + title: t('Duplicate template'), + children: templates.map((template) => { + const templateName = ['FormItem', 'ReadPrettyFormItem'].includes(template?.componentName) + ? `${template?.name} ${t('(Fields only)')}` + : template?.name; + return { + type: 'item', + mode: 'copy', + name: item.name, + template, + dataSource, + title: templateName || t('Untitled'), + }; + }), + }, + { + key: `${componentName}_table_subMenu_${index}_ref`, + type: 'subMenu', + name: 'ref', + dataSource, + title: t('Reference template'), + children: templates.map((template) => { + const templateName = ['FormItem', 'ReadPrettyFormItem'].includes(template?.componentName) + ? `${template?.name} ${t('(Fields only)')}` + : template?.name; + return { + type: 'item', + mode: 'reference', + name: item.name, + template, + dataSource, + title: templateName || t('Untitled'), + }; + }), + }, + ], + }; + }); +}; + +function useAssociationFields({ + componentName, + filterCollections, + showAssociationFields, +}: { + componentName: string; + filterCollections: (options: { collection?: Collection; associationField?: CollectionFieldOptions }) => boolean; + showAssociationFields?: boolean; +}) { + const fieldSchema = useFieldSchema(); + const { getCollectionFields } = useCollectionManager_deprecated(); + const collection = useCollection_deprecated(); + const cm = useCollectionManager(); + const dataSource = useDataSourceKey(); + const { getTemplatesByCollection } = useSchemaTemplateManager(); + const { t } = useTranslation(); + const compile = useCompile(); + + return useMemo(() => { + if (!showAssociationFields) { + return []; + } + + let fields: CollectionFieldOptions[] = []; + + if (fieldSchema['x-initializer']) { + fields = collection.fields; + } else { + const collection = recursiveParent(fieldSchema.parent); + if (collection) { + fields = getCollectionFields(collection); + } + } + + return fields + .filter((field) => ['linkTo', 'subTable', 'o2m', 'm2m', 'obo', 'oho', 'o2o', 'm2o'].includes(field.interface)) + .filter((field) => filterCollections({ associationField: field })) + .map((field, index) => { + const title = compile(field.uiSchema.title || field.name); + const templates = getTemplatesByCollection(dataSource, field.target).filter((template) => { + // 针对弹窗中的详情卡片 + if (componentName === 'ReadPrettyFormItem') { + if (['hasOne', 'belongsTo'].includes(field.type)) { + return template.componentName === 'ReadPrettyFormItem'; + } else { + return template.componentName === 'Details'; + } + } + + return ( + componentName && + template.componentName === componentName && + (['FormItem', 'ReadPrettyFormItem'].includes(componentName) || + !template.resourceName || + template.resourceName === `${field.collectionName}.${field.name}`) + ); + }); + if (!templates.length) { + return { + type: 'item', + name: `${field.collectionName}.${field.name}`, + collectionName: field.target, + title, + dataSource, + associationField: field, + }; + } + 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, + }, + { + type: 'divider', + }, + { + key: `associationFiled_${componentName}_table_subMenu_${index}_copy`, + type: 'subMenu', + name: 'copy', + dataSource, + title: t('Duplicate template'), + children: templates.map((template) => { + const templateName = ['FormItem', 'ReadPrettyFormItem'].includes(template?.componentName) + ? `${template?.name} ${t('(Fields only)')}` + : template?.name; + return { + type: 'item', + mode: 'copy', + name: `${field.collectionName}.${field.name}`, + collectionName: field.target, + template, + dataSource, + title: templateName || t('Untitled'), + associationField: field, + }; + }), + }, + { + key: `associationFiled_${componentName}_table_subMenu_${index}_ref`, + type: 'subMenu', + name: 'ref', + dataSource, + title: t('Reference template'), + children: templates.map((template) => { + const templateName = ['FormItem', 'ReadPrettyFormItem'].includes(template?.componentName) + ? `${template?.name} ${t('(Fields only)')}` + : template?.name; + return { + type: 'item', + mode: 'reference', + name: `${field.collectionName}.${field.name}`, + collectionName: field.target, + template, + dataSource, + title: templateName || t('Untitled'), + associationField: field, + }; + }), + }, + ], + }; + }); + }, [ + cm, + collection.fields, + compile, + componentName, + dataSource, + fieldSchema, + filterCollections, + getCollectionFields, + getTemplatesByCollection, + showAssociationFields, + t, + ]); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d814dc15c5..8685b62d53 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,10 @@ overrides: importers: .: + dependencies: + ts-morph: + specifier: ^26.0.0 + version: 26.0.0 devDependencies: '@commitlint/cli': specifier: ^19.8.1 @@ -5677,24 +5681,28 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@ast-grep/napi-linux-arm64-musl@0.37.0': resolution: {integrity: sha512-LF9sAvYy6es/OdyJDO3RwkX3I82Vkfsng1sqUBcoWC1jVb1wX5YVzHtpQox9JrEhGl+bNp7FYxB4Qba9OdA5GA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@ast-grep/napi-linux-x64-gnu@0.37.0': resolution: {integrity: sha512-TViz5/klqre6aSmJzswEIjApnGjJzstG/SE8VDWsrftMBMYt2PTu3MeluZVwzSqDao8doT/P+6U11dU05UOgxw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@ast-grep/napi-linux-x64-musl@0.37.0': resolution: {integrity: sha512-/BcCH33S9E3ovOAEoxYngUNXgb+JLg991sdyiNP2bSoYd30a9RHrG7CYwW6fMgua3ijQ474eV6cq9yZO1bCpXg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@ast-grep/napi-win32-arm64-msvc@0.37.0': resolution: {integrity: sha512-TjQA4cFoIEW2bgjLkaL9yqT4XWuuLa5MCNd0VCDhGRDMNQ9+rhwi9eLOWRaap3xzT7g+nlbcEHL3AkVCD2+b3A==} @@ -7821,24 +7829,28 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@node-rs/bcrypt-linux-arm64-musl@1.10.4': resolution: {integrity: sha512-rLvSMW/gVUBd2k2gAqQfuOReHWd9+jvz58E3i1TbkRE3a5ChvjOFc9qKPEmXuXuD9Mdj7gUwcYwpq8MdB5MtNw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@node-rs/bcrypt-linux-x64-gnu@1.10.4': resolution: {integrity: sha512-I++6bh+BIp70X/D/crlSgCq8K0s9nGvzmvAGFkqSG4h3LBtjJx4RKbygnoWvcBV9ErK1rvcjfMyjwZt1ukueFA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@node-rs/bcrypt-linux-x64-musl@1.10.4': resolution: {integrity: sha512-f9RPl/5n2NS0mMJXB4IYbodKnq5HzOK5x1b9eKbcjsY0rw3mJC3K0XRFc8iaw1a5chA+xV1TPXz5mkykmr2CQQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@node-rs/bcrypt-wasm32-wasi@1.10.4': resolution: {integrity: sha512-VaDOf+wic0yoHFimMkC5VMa/33BNqg6ieD+C/ibb7Av3NnVW4/W9YpDpqAWMR2w3fA40uTLWZ7FZSrcFck27oA==} @@ -8445,21 +8457,25 @@ packages: resolution: {integrity: sha512-Nr8oUAEo20WIBo/qi76dBo5IGw2bQcl7d2YbaigOSQoAGfHR9xE9hAySYhIsnv7W0jtAZu1YTn0So7Tg0idExw==} cpu: [arm64] os: [linux] + libc: [glibc] '@oxlint/linux-arm64-musl@1.5.0': resolution: {integrity: sha512-H2MBL0LZnl3GP09r12tFfcv3Y2fvetMD1TYbf5cetYCQ7hG7aIYTsuOTENjZ0SDK+L9Id35YWFNP3YES+3tsgw==} cpu: [arm64] os: [linux] + libc: [musl] '@oxlint/linux-x64-gnu@1.5.0': resolution: {integrity: sha512-pRu77WJ+Uy0l6OkCjljnHuzOlSbMsN1mFDeAyzh8R69O44Mc2C7f1Fe+fbbW2kjgIwFQq8JkUffvPWr1MCMa+A==} cpu: [x64] os: [linux] + libc: [glibc] '@oxlint/linux-x64-musl@1.5.0': resolution: {integrity: sha512-qHG2YR+06pEAF4Gfp3T8hSaiI/IchdbuditnnVYxy32pN43vNMVDO7fU5hnNzJpxh6HozAD3uRrskBNvxgeSrQ==} cpu: [x64] os: [linux] + libc: [musl] '@oxlint/win32-arm64@1.5.0': resolution: {integrity: sha512-/YRDPf1sPCF6B026buZTvh1vOF/pS+75aBDnxrDWIFhP1w3flqcI9v5QVjtq/sOZEWAoP2ee46CrxcxK5UuYag==} @@ -9030,161 +9046,193 @@ packages: resolution: {integrity: sha512-Z0TzhrsNqukTz3ISzrvyshQpFnFRfLunYiXxlCRvcrb3nvC5rVKI+ZXPFG/Aa4jhQa1gHgH3A0exHaRRN4VmdQ==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-gnueabihf@4.44.1': resolution: {integrity: sha512-n0edDmSHlXFhrlmTK7XBuwKlG5MbS7yleS1cQ9nn4kIeW+dJH+ExqNgQ0RrFRew8Y+0V/x6C5IjsHrJmiHtkxQ==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-gnueabihf@4.44.2': resolution: {integrity: sha512-+xmiDGGaSfIIOXMzkhJ++Oa0Gwvl9oXUeIiwarsdRXSe27HUIvjbSIpPxvnNsRebsNdUo7uAiQVgBD1hVriwSQ==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.34.7': resolution: {integrity: sha512-nkznpyXekFAbvFBKBy4nNppSgneB1wwG1yx/hujN3wRnhnkrYVugMTCBXED4+Ni6thoWfQuHNYbFjgGH0MBXtw==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm-musleabihf@4.44.1': resolution: {integrity: sha512-8WVUPy3FtAsKSpyk21kV52HCxB+me6YkbkFHATzC2Yd3yuqHwy2lbFL4alJOLXKljoRw08Zk8/xEj89cLQ/4Nw==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm-musleabihf@4.44.2': resolution: {integrity: sha512-bDHvhzOfORk3wt8yxIra8N4k/N0MnKInCW5OGZaeDYa/hMrdPaJzo7CSkjKZqX4JFUWjUGm88lI6QJLCM7lDrA==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.34.7': resolution: {integrity: sha512-KCjlUkcKs6PjOcxolqrXglBDcfCuUCTVlX5BgzgoJHw+1rWH1MCkETLkLe5iLLS9dP5gKC7mp3y6x8c1oGBUtA==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-gnu@4.44.1': resolution: {integrity: sha512-yuktAOaeOgorWDeFJggjuCkMGeITfqvPgkIXhDqsfKX8J3jGyxdDZgBV/2kj/2DyPaLiX6bPdjJDTu9RB8lUPQ==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-gnu@4.44.2': resolution: {integrity: sha512-NMsDEsDiYghTbeZWEGnNi4F0hSbGnsuOG+VnNvxkKg0IGDvFh7UVpM/14mnMwxRxUf9AdAVJgHPvKXf6FpMB7A==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.34.7': resolution: {integrity: sha512-uFLJFz6+utmpbR313TTx+NpPuAXbPz4BhTQzgaP0tozlLnGnQ6rCo6tLwaSa6b7l6gRErjLicXQ1iPiXzYotjw==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-musl@4.44.1': resolution: {integrity: sha512-W+GBM4ifET1Plw8pdVaecwUgxmiH23CfAUj32u8knq0JPFyK4weRy6H7ooxYFD19YxBulL0Ktsflg5XS7+7u9g==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-musl@4.44.2': resolution: {integrity: sha512-lb5bxXnxXglVq+7imxykIp5xMq+idehfl+wOgiiix0191av84OqbjUED+PRC5OA8eFJYj5xAGcpAZ0pF2MnW+A==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loongarch64-gnu@4.34.7': resolution: {integrity: sha512-ws8pc68UcJJqCpneDFepnwlsMUFoWvPbWXT/XUrJ7rWUL9vLoIN3GAasgG+nCvq8xrE3pIrd+qLX/jotcLy0Qw==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loongarch64-gnu@4.44.1': resolution: {integrity: sha512-1zqnUEMWp9WrGVuVak6jWTl4fEtrVKfZY7CvcBmUUpxAJ7WcSowPSAWIKa/0o5mBL/Ij50SIf9tuirGx63Ovew==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loongarch64-gnu@4.44.2': resolution: {integrity: sha512-Yl5Rdpf9pIc4GW1PmkUGHdMtbx0fBLE1//SxDmuf3X0dUC57+zMepow2LK0V21661cjXdTn8hO2tXDdAWAqE5g==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-powerpc64le-gnu@4.34.7': resolution: {integrity: sha512-vrDk9JDa/BFkxcS2PbWpr0C/LiiSLxFbNOBgfbW6P8TBe9PPHx9Wqbvx2xgNi1TOAyQHQJ7RZFqBiEohm79r0w==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-powerpc64le-gnu@4.44.1': resolution: {integrity: sha512-Rl3JKaRu0LHIx7ExBAAnf0JcOQetQffaw34T8vLlg9b1IhzcBgaIdnvEbbsZq9uZp3uAH+JkHd20Nwn0h9zPjA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-powerpc64le-gnu@4.44.2': resolution: {integrity: sha512-03vUDH+w55s680YYryyr78jsO1RWU9ocRMaeV2vMniJJW/6HhoTBwyyiiTPVHNWLnhsnwcQ0oH3S9JSBEKuyqw==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.34.7': resolution: {integrity: sha512-rB+ejFyjtmSo+g/a4eovDD1lHWHVqizN8P0Hm0RElkINpS0XOdpaXloqM4FBkF9ZWEzg6bezymbpLmeMldfLTw==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.44.1': resolution: {integrity: sha512-j5akelU3snyL6K3N/iX7otLBIl347fGwmd95U5gS/7z6T4ftK288jKq3A5lcFKcx7wwzb5rgNvAg3ZbV4BqUSw==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.44.2': resolution: {integrity: sha512-iYtAqBg5eEMG4dEfVlkqo05xMOk6y/JXIToRca2bAWuqjrJYJlx/I7+Z+4hSrsWU8GdJDFPL4ktV3dy4yBSrzg==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.44.1': resolution: {integrity: sha512-ppn5llVGgrZw7yxbIm8TTvtj1EoPgYUAbfw0uDjIOzzoqlZlZrLJ/KuiE7uf5EpTpCTrNt1EdtzF0naMm0wGYg==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-musl@4.44.2': resolution: {integrity: sha512-e6vEbgaaqz2yEHqtkPXa28fFuBGmUJ0N2dOJK8YUfijejInt9gfCSA7YDdJ4nYlv67JfP3+PSWFX4IVw/xRIPg==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.34.7': resolution: {integrity: sha512-nNXNjo4As6dNqRn7OrsnHzwTgtypfRA3u3AKr0B3sOOo+HkedIbn8ZtFnB+4XyKJojIfqDKmbIzO1QydQ8c+Pw==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-s390x-gnu@4.44.1': resolution: {integrity: sha512-Hu6hEdix0oxtUma99jSP7xbvjkUM/ycke/AQQ4EC5g7jNRLLIwjcNwaUy95ZKBJJwg1ZowsclNnjYqzN4zwkAw==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-s390x-gnu@4.44.2': resolution: {integrity: sha512-evFOtkmVdY3udE+0QKrV5wBx7bKI0iHz5yEVx5WqDJkxp9YQefy4Mpx3RajIVcM6o7jxTvVd/qpC1IXUhGc1Mw==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.34.7': resolution: {integrity: sha512-9kPVf9ahnpOMSGlCxXGv980wXD0zRR3wyk8+33/MXQIpQEOpaNe7dEHm5LMfyRZRNt9lMEQuH0jUKj15MkM7QA==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.44.1': resolution: {integrity: sha512-EtnsrmZGomz9WxK1bR5079zee3+7a+AdFlghyd6VbAjgRJDbTANJ9dcPIPAi76uG05micpEL+gPGmAKYTschQw==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.44.2': resolution: {integrity: sha512-/bXb0bEsWMyEkIsUL2Yt5nFB5naLAwyOWMEviQfQY1x3l5WsLKgvZf66TM7UTfED6erckUVUJQ/jJ1FSpm3pRQ==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.34.7': resolution: {integrity: sha512-7wJPXRWTTPtTFDFezA8sle/1sdgxDjuMoRXEKtx97ViRxGGkVQYovem+Q8Pr/2HxiHp74SSRG+o6R0Yq0shPwQ==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-linux-x64-musl@4.44.1': resolution: {integrity: sha512-iAS4p+J1az6Usn0f8xhgL4PaU878KEtutP4hqw52I4IO6AGoyOkHCxcc4bqufv1tQLdDWFx8lR9YlwxKuv3/3g==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-linux-x64-musl@4.44.2': resolution: {integrity: sha512-3D3OB1vSSBXmkGEZR27uiMRNiwN08/RVAcBKwhUYPaiZ8bcvdeEwWPvbnXvvXHY+A/7xluzcN+kaiOFNiOZwWg==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-win32-arm64-msvc@4.34.7': resolution: {integrity: sha512-MN7aaBC7mAjsiMEZcsJvwNsQVNZShgES/9SzWp1HC9Yjqb5OpexYnRjF7RmE4itbeesHMYYQiAtUAQaSKs2Rfw==} @@ -9299,21 +9347,25 @@ packages: resolution: {integrity: sha512-mJK9diM4Gd8RIGO90AZnl27WwUuAOoRplPQv9G+Vxu2baCt1xE1ccf8PntIJ70/rMgsUdnmkR5qQBaGxhAMJvA==} cpu: [arm64] os: [linux] + libc: [glibc] '@rspack/binding-linux-arm64-musl@1.4.8': resolution: {integrity: sha512-+n9QxeDDZKwVB4D6cwpNRJzsCeuwNqd/fwwbMQVTctJ+GhIHlUPsE8y5tXN7euU7kDci81wMBBFlt6LtXNcssA==} cpu: [arm64] os: [linux] + libc: [musl] '@rspack/binding-linux-x64-gnu@1.4.8': resolution: {integrity: sha512-rEypDlbIfv9B/DcZ2vYVWs56wo5VWE5oj/TvM9JT+xuqwvVWsN/A2TPMiU6QBgOKGXat3EM/MEgx8NhNZUpkXg==} cpu: [x64] os: [linux] + libc: [glibc] '@rspack/binding-linux-x64-musl@1.4.8': resolution: {integrity: sha512-o9OsvJ7olH0JPU9exyIaYTNQ+aaR5CNAiinkxr+LkV2i3DMIi/+pDVveDiodYjVhzZjWfsP/z8QPO4c6Z06bEw==} cpu: [x64] os: [linux] + libc: [musl] '@rspack/binding-wasm32-wasi@1.4.8': resolution: {integrity: sha512-hF5gqT0aQ66VUclM2A9MSB6zVdEJqzp++TAXaShBK/eVBI0R4vWrMfJ2TOdzEsSbg4gXgeG4swURpHva3PKbcA==} @@ -9756,6 +9808,9 @@ packages: resolution: {integrity: sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==} engines: {node: '>= 6'} + '@ts-morph/common@0.27.0': + resolution: {integrity: sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ==} + '@tufjs/canonical-json@2.0.0': resolution: {integrity: sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==} engines: {node: ^16.14.0 || >=18.0.0} @@ -11434,6 +11489,9 @@ packages: resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + code-block-writer@13.0.3: + resolution: {integrity: sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==} + codemirror-ssr@0.65.0: resolution: {integrity: sha512-ofTAfPkQV56SYFfymNMYJ1ELo3+Jnkw3mOLgnIiQjhonwNmNzX1OFvnihAnYRXL0PWl2kT7s0gKrLc2ExshK4g==} peerDependencies: @@ -14065,24 +14123,28 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.26.0: resolution: {integrity: sha512-XxoEL++tTkyuvu+wq/QS8bwyTXZv2y5XYCMcWL45b8XwkiS8eEEEej9BkMGSRwxa5J4K+LDeIhLrS23CpQyfig==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.26.0: resolution: {integrity: sha512-1dkTfZQAYLj8MUSkd6L/+TWTG8V6Kfrzfa0T1fSlXCXQHrt1HC1/UepXHtKHDt/9yFwyoeayivxXAsApVxn6zA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.26.0: resolution: {integrity: sha512-yX3Rk9m00JGCUzuUhFEojY+jf/6zHs3XU8S8Vk+FRbnr4St7cjyMXdNjuA2LjiT8e7j8xHRCH8hyZ4H/btRE4A==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.26.0: resolution: {integrity: sha512-X/597/cFnCogy9VItj/+7Tgu5VLbAtDF7KZDPdSw0MaL6FL940th1y3HiOzFIlziVvAtbo0RB3NAae1Oofr+Tw==} @@ -17764,6 +17826,9 @@ packages: ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + ts-morph@26.0.0: + resolution: {integrity: sha512-ztMO++owQnz8c/gIENcM9XfCEzgoGphTv+nKpYNM1bgsdOVC/jRZuEBf6N+mLLDNg68Kl+GgUZfOySaRiG1/Ug==} + tsconfig-paths@4.2.0: resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} engines: {node: '>=6'} @@ -24842,6 +24907,12 @@ snapshots: '@tootallnate/once@1.1.2': {} + '@ts-morph/common@0.27.0': + dependencies: + fast-glob: 3.3.3 + minimatch: 10.0.1 + path-browserify: 1.0.1 + '@tufjs/canonical-json@2.0.0': {} '@tufjs/models@3.0.1': @@ -27029,6 +27100,8 @@ snapshots: co@4.6.0: {} + code-block-writer@13.0.3: {} + codemirror-ssr@0.65.0(@types/codemirror@5.60.15): dependencies: '@types/codemirror': 5.60.15 @@ -34458,6 +34531,11 @@ snapshots: ts-interface-checker@0.1.13: {} + ts-morph@26.0.0: + dependencies: + '@ts-morph/common': 0.27.0 + code-block-writer: 13.0.3 + tsconfig-paths@4.2.0: dependencies: json5: 2.2.3