diff --git a/lerna.json b/lerna.json index 005143bf55..aa34119cb8 100644 --- a/lerna.json +++ b/lerna.json @@ -2,9 +2,7 @@ "version": "1.6.0-beta.8", "npmClient": "yarn", "useWorkspaces": true, - "npmClientArgs": [ - "--ignore-engines" - ], + "npmClientArgs": ["--ignore-engines"], "command": { "version": { "forcePublish": true, diff --git a/packages/core/client/src/application/Application.tsx b/packages/core/client/src/application/Application.tsx index 41b18e3f85..8d194d9d13 100644 --- a/packages/core/client/src/application/Application.tsx +++ b/packages/core/client/src/application/Application.tsx @@ -44,8 +44,14 @@ import type { CollectionFieldInterfaceFactory } from '../data-source'; import { OpenModeProvider } from '../modules/popup/OpenModeProvider'; import { AppSchemaComponentProvider } from './AppSchemaComponentProvider'; import type { Plugin } from './Plugin'; +import { getOperators } from './globalOperators'; import type { RequireJS } from './utils/requirejs'; +type JsonLogic = { + addOperation: (name: string, fn?: any) => void; + rmOperation: (name: string) => void; +}; + declare global { interface Window { define: RequireJS['define']; @@ -100,7 +106,7 @@ export class Application { public dataSourceManager: DataSourceManager; public name: string; public globalVars: Record = {}; - + public jsonLogic: JsonLogic; loading = true; maintained = false; maintaining = false; @@ -155,6 +161,7 @@ export class Application { this.apiClient.auth.locale = lng; }); this.initListeners(); + this.jsonLogic = getOperators(); } private initListeners() { diff --git a/packages/core/client/src/schema-component/common/utils/logic.js b/packages/core/client/src/application/globalOperators.js similarity index 90% rename from packages/core/client/src/schema-component/common/utils/logic.js rename to packages/core/client/src/application/globalOperators.js index 9acad63bbd..c3b16067e8 100644 --- a/packages/core/client/src/schema-component/common/utils/logic.js +++ b/packages/core/client/src/application/globalOperators.js @@ -9,13 +9,11 @@ /* globals define,module */ -import dayjs from 'dayjs'; - /* Using a Universal Module Loader that should be browser, require, and AMD friendly http://ricostacruz.com/cheatsheets/umdjs.html */ -export function getJsonLogic() { +export function getOperators() { 'use strict'; /* globals console:false */ @@ -307,11 +305,11 @@ export function getJsonLogic() { }, missing: function () { /* - Missing can receive many keys as many arguments, like {"missing:[1,2]} - Missing can also receive *one* argument that is an array of keys, - which typically happens if it's actually acting on the output of another command - (like 'if' or 'merge') - */ + Missing can receive many keys as many arguments, like {"missing:[1,2]} + Missing can also receive *one* argument that is an array of keys, + which typically happens if it's actually acting on the output of another command + (like 'if' or 'merge') + */ var missing = []; var keys = Array.isArray(arguments[0]) ? arguments[0] : arguments; @@ -348,10 +346,10 @@ export function getJsonLogic() { }; /* - This helper will defer to the JsonLogic spec as a tie-breaker when different language interpreters define different behavior for the truthiness of primitives. E.g., PHP considers empty arrays to be falsy, but Javascript considers them to be truthy. JsonLogic, as an ecosystem, needs one consistent answer. - - Spec and rationale here: http://jsonlogic.com/truthy - */ + This helper will defer to the JsonLogic spec as a tie-breaker when different language interpreters define different behavior for the truthiness of primitives. E.g., PHP considers empty arrays to be falsy, but Javascript considers them to be truthy. JsonLogic, as an ecosystem, needs one consistent answer. + + Spec and rationale here: http://jsonlogic.com/truthy + */ jsonLogic.truthy = function (value) { if (Array.isArray(value) && value.length === 0) { return false; @@ -359,12 +357,12 @@ export function getJsonLogic() { return !!value; }; - jsonLogic.get_operator = function (logic) { + jsonLogic.getOperator = function (logic) { return Object.keys(logic)[0]; }; - jsonLogic.get_values = function (logic) { - return logic[jsonLogic.get_operator(logic)]; + jsonLogic.getValues = function (logic) { + return logic[jsonLogic.getOperator(logic)]; }; jsonLogic.apply = function (logic, data) { @@ -379,7 +377,7 @@ export function getJsonLogic() { return logic; } - var op = jsonLogic.get_operator(logic); + var op = jsonLogic.getOperator(logic); var values = logic[op]; var i; var current; @@ -395,18 +393,18 @@ export function getJsonLogic() { // 'if', 'and', and 'or' violate the normal rule of depth-first calculating consequents, let each manage recursion as needed. if (op === 'if' || op == '?:') { /* 'if' should be called with a odd number of parameters, 3 or greater - This works on the pattern: - if( 0 ){ 1 }else{ 2 }; - if( 0 ){ 1 }else if( 2 ){ 3 }else{ 4 }; - if( 0 ){ 1 }else if( 2 ){ 3 }else if( 4 ){ 5 }else{ 6 }; - - The implementation is: - For pairs of values (0,1 then 2,3 then 4,5 etc) - If the first evaluates truthy, evaluate and return the second - If the first evaluates falsy, jump to the next pair (e.g, 0,1 to 2,3) - given one parameter, evaluate and return it. (it's an Else and all the If/ElseIf were false) - given 0 parameters, return NULL (not great practice, but there was no Else) - */ + This works on the pattern: + if( 0 ){ 1 }else{ 2 }; + if( 0 ){ 1 }else if( 2 ){ 3 }else{ 4 }; + if( 0 ){ 1 }else if( 2 ){ 3 }else if( 4 ){ 5 }else{ 6 }; + + The implementation is: + For pairs of values (0,1 then 2,3 then 4,5 etc) + If the first evaluates truthy, evaluate and return the second + If the first evaluates falsy, jump to the next pair (e.g, 0,1 to 2,3) + given one parameter, evaluate and return it. (it's an Else and all the If/ElseIf were false) + given 0 parameters, return NULL (not great practice, but there was no Else) + */ for (i = 0; i < values.length - 1; i += 2) { if (jsonLogic.truthy(jsonLogic.apply(values[i], data))) { return jsonLogic.apply(values[i + 1], data); @@ -543,7 +541,7 @@ export function getJsonLogic() { var collection = []; if (jsonLogic.is_logic(logic)) { - var op = jsonLogic.get_operator(logic); + var op = jsonLogic.getOperator(logic); var values = logic[op]; if (!Array.isArray(values)) { @@ -564,11 +562,11 @@ export function getJsonLogic() { return arrayUnique(collection); }; - jsonLogic.add_operation = function (name, code) { + jsonLogic.addOperation = function (name, code) { operations[name] = code; }; - jsonLogic.rm_operation = function (name) { + jsonLogic.rmOperation = function (name) { delete operations[name]; }; @@ -593,8 +591,8 @@ export function getJsonLogic() { if (jsonLogic.is_logic(pattern)) { if (jsonLogic.is_logic(rule)) { - var pattern_op = jsonLogic.get_operator(pattern); - var rule_op = jsonLogic.get_operator(rule); + var pattern_op = jsonLogic.getOperator(pattern); + var rule_op = jsonLogic.getOperator(rule); if (pattern_op === '@' || pattern_op === rule_op) { // echo "\nOperators match, go deeper\n"; @@ -610,8 +608,8 @@ export function getJsonLogic() { return false; } /* - Note, array order MATTERS, because we're using this array test logic to consider arguments, where order can matter. (e.g., + is commutative, but '-' or 'if' or 'var' are NOT) - */ + Note, array order MATTERS, because we're using this array test logic to consider arguments, where order can matter. (e.g., + is commutative, but '-' or 'if' or 'var' are NOT) + */ for (var i = 0; i < pattern.length; i += 1) { // If any fail, we fail if (!jsonLogic.rule_like(rule[i], pattern[i])) { diff --git a/packages/core/client/src/collection-manager/hooks/useCollectionManager_deprecated.ts b/packages/core/client/src/collection-manager/hooks/useCollectionManager_deprecated.ts index a632682698..b88ed2c907 100644 --- a/packages/core/client/src/collection-manager/hooks/useCollectionManager_deprecated.ts +++ b/packages/core/client/src/collection-manager/hooks/useCollectionManager_deprecated.ts @@ -133,7 +133,8 @@ export const useCollectionManager_deprecated = (dataSourceName?: string) => { const getCollectionFieldsOptions = useCallback( ( collectionName: string, - type: string | string[] = 'string', + type?: string | string[], + interfaces?: string | string[], opts?: { dataSource?: string; cached?: Record; @@ -183,9 +184,12 @@ export const useCollectionManager_deprecated = (dataSourceName?: string) => { return _.cloneDeep(cached[collectionName]); } - if (typeof type === 'string') { + if (type && typeof type === 'string') { type = [type]; } + if (interfaces && typeof interfaces === 'string') { + interfaces = [interfaces]; + } const fields = getCollectionFields(collectionName, customDataSourceNameValue); const options = fields ?.filter( @@ -193,7 +197,8 @@ export const useCollectionManager_deprecated = (dataSourceName?: string) => { field.interface && !exceptInterfaces.includes(field.interface) && (allowAllTypes || - type.includes(field.type) || + (type && type.includes(field.type)) || + (interfaces && interfaces.includes(field.interface)) || (association && field.target && field.target !== collectionName && Array.isArray(association) ? association.includes(field.interface) : false)), @@ -207,7 +212,7 @@ export const useCollectionManager_deprecated = (dataSourceName?: string) => { if (association && field.target) { result.children = collectionNames.includes(field.target) ? [] - : getCollectionFieldsOptions(field.target, type, { + : getCollectionFieldsOptions(field.target, type, interfaces, { ...opts, cached, dataSource: customDataSourceNameValue, diff --git a/packages/core/client/src/collection-manager/interfaces/createdAt.ts b/packages/core/client/src/collection-manager/interfaces/createdAt.ts index c6c18713a8..b474ba8137 100644 --- a/packages/core/client/src/collection-manager/interfaces/createdAt.ts +++ b/packages/core/client/src/collection-manager/interfaces/createdAt.ts @@ -28,6 +28,7 @@ export class CreatedAtFieldInterface extends CollectionFieldInterface { 'x-read-pretty': true, }, }; + description = '{{t("Store the creation time of each record")}}'; availableTypes = []; properties = { ...defaultProps, diff --git a/packages/core/client/src/collection-manager/interfaces/createdBy.ts b/packages/core/client/src/collection-manager/interfaces/createdBy.ts index 4acb8d217c..e368589542 100644 --- a/packages/core/client/src/collection-manager/interfaces/createdBy.ts +++ b/packages/core/client/src/collection-manager/interfaces/createdBy.ts @@ -76,4 +76,5 @@ export class CreatedByFieldInterface extends CollectionFieldInterface { schema['x-component-props']['ellipsis'] = true; } } + description = '{{t("Store the creation user of each record")}}'; } diff --git a/packages/core/client/src/collection-manager/interfaces/id.ts b/packages/core/client/src/collection-manager/interfaces/id.ts index f4180bec94..027cb85363 100644 --- a/packages/core/client/src/collection-manager/interfaces/id.ts +++ b/packages/core/client/src/collection-manager/interfaces/id.ts @@ -55,5 +55,7 @@ export class IdFieldInterface extends CollectionFieldInterface { filterable = { operators: operators.id, }; + + description = '{{t("Primary key, unique identifier, self growth") }}'; titleUsable = true; } diff --git a/packages/core/client/src/collection-manager/interfaces/updatedAt.ts b/packages/core/client/src/collection-manager/interfaces/updatedAt.ts index 533c008d7f..9f9c5a1f67 100644 --- a/packages/core/client/src/collection-manager/interfaces/updatedAt.ts +++ b/packages/core/client/src/collection-manager/interfaces/updatedAt.ts @@ -21,13 +21,14 @@ export class UpdatedAtFieldInterface extends CollectionFieldInterface { type: 'date', field: 'updatedAt', uiSchema: { - type: 'string', + type: 'datetime', title: '{{t("Last updated at")}}', 'x-component': 'DatePicker', 'x-component-props': {}, 'x-read-pretty': true, }, }; + description = '{{t("Store the last update time of each record")}}'; availableTypes = []; properties = { ...defaultProps, diff --git a/packages/core/client/src/collection-manager/interfaces/updatedBy.ts b/packages/core/client/src/collection-manager/interfaces/updatedBy.ts index cc03efcefe..fc79ada112 100644 --- a/packages/core/client/src/collection-manager/interfaces/updatedBy.ts +++ b/packages/core/client/src/collection-manager/interfaces/updatedBy.ts @@ -75,4 +75,5 @@ export class UpdatedByFieldInterface extends CollectionFieldInterface { schema['x-component-props']['ellipsis'] = true; } } + description = '{{t("Store the last update user of each record")}}'; } diff --git a/packages/core/client/src/collection-manager/templates/components/PresetFields.tsx b/packages/core/client/src/collection-manager/templates/components/PresetFields.tsx index 49ff139939..675617818e 100644 --- a/packages/core/client/src/collection-manager/templates/components/PresetFields.tsx +++ b/packages/core/client/src/collection-manager/templates/components/PresetFields.tsx @@ -9,106 +9,27 @@ import { observer, useForm } from '@formily/react'; import { Table, Tag } from 'antd'; -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useCollectionManager_deprecated } from '../../'; -import { useCompile } from '../../../'; +import { useCompile, useApp } from '../../../'; -const getDefaultCollectionFields = (presetFields, values) => { +const getDefaultCollectionFields = (presetFields, values, collectionPresetFields) => { if (values?.template === 'view' || values?.template === 'sql') { return values.fields; } - const defaults = values.fields - ? [...values.fields].filter((v) => { - return !['id', 'createdBy', 'updatedAt', 'createdAt', 'updatedBy'].includes(v.name); - }) - : []; - if (presetFields.find((v) => v.name === 'id')) { - defaults.push({ - name: 'id', - type: 'bigInt', - autoIncrement: true, - primaryKey: true, - allowNull: false, - uiSchema: { type: 'number', title: '{{t("ID")}}', 'x-component': 'InputNumber', 'x-read-pretty': true }, - interface: 'integer', - }); - } - if (presetFields.find((v) => v.name === 'createdAt')) { - defaults.push({ - name: 'createdAt', - interface: 'createdAt', - type: 'date', - field: 'createdAt', - uiSchema: { - type: 'datetime', - title: '{{t("Created at")}}', - 'x-component': 'DatePicker', - 'x-component-props': {}, - 'x-read-pretty': true, - }, - }); - } - if (presetFields.find((v) => v.name === 'createdBy')) { - defaults.push({ - name: 'createdBy', - interface: 'createdBy', - type: 'belongsTo', - target: 'users', - foreignKey: 'createdById', - uiSchema: { - type: 'object', - title: '{{t("Created by")}}', - 'x-component': 'AssociationField', - 'x-component-props': { - fieldNames: { - value: 'id', - label: 'nickname', - }, - }, - 'x-read-pretty': true, - }, - }); - } - if (presetFields.find((v) => v.name === 'updatedAt')) { - defaults.push({ - type: 'date', - field: 'updatedAt', - name: 'updatedAt', - interface: 'updatedAt', - uiSchema: { - type: 'string', - title: '{{t("Last updated at")}}', - 'x-component': 'DatePicker', - 'x-component-props': {}, - 'x-read-pretty': true, - }, - }); - } - if (presetFields.find((v) => v.name === 'updatedBy')) { - defaults.push({ - type: 'belongsTo', - target: 'users', - foreignKey: 'updatedById', - name: 'updatedBy', - interface: 'updatedBy', - uiSchema: { - type: 'object', - title: '{{t("Last updated by")}}', - 'x-component': 'AssociationField', - 'x-component-props': { - fieldNames: { - value: 'id', - label: 'nickname', - }, - }, - 'x-read-pretty': true, - }, - }); - } - // 其他 - return defaults; + const fields = + values.fields?.filter((v) => { + const item = collectionPresetFields.find((i) => i.value.name === v.name); + return !item; + }) || []; + presetFields.map((v) => { + const item = collectionPresetFields.find((i) => i.value.name === v); + item && fields.push(item.value); + }); + return fields; }; + export const PresetFields = observer( (props: any) => { const { getInterface } = useCollectionManager_deprecated(); @@ -116,11 +37,26 @@ export const PresetFields = observer( const compile = useCompile(); const [selectedRowKeys, setSelectedRowKeys] = useState([]); const { t } = useTranslation(); + const app = useApp(); + const mainDataSourcePlugin: any = app.pm.get('data-source-main'); + const collectionPresetFields = mainDataSourcePlugin.getCollectionPresetFields(); + + const presetFieldsDataSource = useMemo(() => { + return collectionPresetFields.map((v) => { + return { + field: v.value.uiSchema.title, + interface: v.value.interface, + description: v.description, + name: v.value.name, + }; + }); + }, []); const column = [ { title: t('Field'), dataIndex: 'field', key: 'field', + render: (value) => compile(value), }, { title: t('Interface'), @@ -132,61 +68,19 @@ export const PresetFields = observer( title: t('Description'), dataIndex: 'description', key: 'description', - }, - ]; - const dataSource = [ - { - field: t('ID'), - interface: 'integer', - description: t('Primary key, unique identifier, self growth'), - name: 'id', - }, - { - field: t('Created at'), - interface: 'createdAt', - description: t('Store the creation time of each record'), - name: 'createdAt', - }, - { - field: t('Last updated at'), - interface: 'updatedAt', - description: t('Store the last update time of each record'), - name: 'updatedAt', - }, - { - field: t('Created by'), - interface: 'createdBy', - description: t('Store the creation user of each record'), - name: 'createdBy', - }, - - { - field: t('Last updated by'), - interface: 'updatedBy', - description: t('Store the last update user of each record'), - name: 'updatedBy', + render: (value) => compile(value), }, ]; useEffect(() => { - const config = { - autoGenId: false, - createdAt: true, - createdBy: true, - updatedAt: true, - updatedBy: true, - }; - const initialValue = ['id', 'createdAt', 'createdBy', 'updatedAt', 'updatedBy']; + const initialValue = presetFieldsDataSource.map((v) => v.name); setSelectedRowKeys(initialValue); - form.setValues({ ...form.values, ...config }); - }, []); + form.setValues({ ...form.values, autoGenId: false }); + }, [presetFieldsDataSource]); useEffect(() => { const fields = getDefaultCollectionFields( - selectedRowKeys.map((v) => { - return { - name: v, - }; - }), + selectedRowKeys.map((v) => v), form.values, + collectionPresetFields, ); form.setValuesIn('fields', fields); }, [selectedRowKeys]); @@ -197,7 +91,7 @@ export const PresetFields = observer( rowKey="name" bordered scroll={{ x: 600 }} - dataSource={dataSource} + dataSource={presetFieldsDataSource} columns={column} rowSelection={{ type: 'checkbox', @@ -206,21 +100,10 @@ export const PresetFields = observer( name: record.name, disabled: props?.disabled || props?.presetFieldsDisabledIncludes?.includes?.(record.name), }), - onChange: (_, selectedRows) => { - const fields = getDefaultCollectionFields(selectedRows, form.values); - const config = { - autoGenId: false, - createdAt: !!fields.find((v) => v.name === 'createdAt'), - createdBy: !!fields.find((v) => v.name === 'createdBy'), - updatedAt: !!fields.find((v) => v.name === 'updatedAt'), - updatedBy: !!fields.find((v) => v.name === 'updatedBy'), - }; - setSelectedRowKeys( - fields?.map?.((v) => { - return v.name; - }), - ); - form.setValues({ ...form.values, fields, ...config }); + onChange: (selectedKeys, selectedRows) => { + const fields = getDefaultCollectionFields(selectedKeys, form.values, collectionPresetFields); + setSelectedRowKeys(selectedKeys); + form.setValues({ ...form.values, fields, autoGenId: false }); }, }} /> diff --git a/packages/core/client/src/modules/actions/save-record/customizeSaveRecordActionSettings.tsx b/packages/core/client/src/modules/actions/save-record/customizeSaveRecordActionSettings.tsx index 1659001228..95133b6dcf 100644 --- a/packages/core/client/src/modules/actions/save-record/customizeSaveRecordActionSettings.tsx +++ b/packages/core/client/src/modules/actions/save-record/customizeSaveRecordActionSettings.tsx @@ -19,7 +19,6 @@ import { RemoveButton, SecondConFirm, SkipValidation, - WorkflowConfig, } from '../../../schema-component/antd/action/Action.Designer'; /** @@ -53,10 +52,6 @@ export const customizeSaveRecordActionSettings = new SchemaSettings({ name: 'afterSuccessfulSubmission', Component: AfterSuccess, }, - { - name: 'bindWorkflow', - Component: WorkflowConfig, - }, { name: 'refreshDataBlockRequest', Component: RefreshDataBlockRequest, diff --git a/packages/core/client/src/modules/actions/submit/CreateSubmitActionInitializer.tsx b/packages/core/client/src/modules/actions/submit/CreateSubmitActionInitializer.tsx index 2b4127eb86..844ced7a56 100644 --- a/packages/core/client/src/modules/actions/submit/CreateSubmitActionInitializer.tsx +++ b/packages/core/client/src/modules/actions/submit/CreateSubmitActionInitializer.tsx @@ -22,9 +22,6 @@ export const CreateSubmitActionInitializer = (props) => { type: 'primary', htmlType: 'submit', }, - 'x-action-settings': { - triggerWorkflows: [], - }, }; return ; }; diff --git a/packages/core/client/src/modules/actions/submit/UpdateSubmitActionInitializer.tsx b/packages/core/client/src/modules/actions/submit/UpdateSubmitActionInitializer.tsx index ab3fe17889..65e75f161c 100644 --- a/packages/core/client/src/modules/actions/submit/UpdateSubmitActionInitializer.tsx +++ b/packages/core/client/src/modules/actions/submit/UpdateSubmitActionInitializer.tsx @@ -23,9 +23,6 @@ export const UpdateSubmitActionInitializer = (props) => { type: 'primary', htmlType: 'submit', }, - 'x-action-settings': { - triggerWorkflows: [], - }, }; return ; }; diff --git a/packages/core/client/src/modules/actions/submit/createSubmitActionSettings.tsx b/packages/core/client/src/modules/actions/submit/createSubmitActionSettings.tsx index f45d7d1113..34bcb7cdef 100644 --- a/packages/core/client/src/modules/actions/submit/createSubmitActionSettings.tsx +++ b/packages/core/client/src/modules/actions/submit/createSubmitActionSettings.tsx @@ -24,7 +24,6 @@ import { RemoveButton, SecondConFirm, SkipValidation, - WorkflowConfig, } from '../../../schema-component/antd/action/Action.Designer'; import { useCollectionState } from '../../../schema-settings/DataTemplates/hooks/useCollectionState'; import { SchemaSettingsModalItem } from '../../../schema-settings/SchemaSettings'; @@ -154,14 +153,6 @@ export const createSubmitActionSettings = new SchemaSettings({ name: 'secondConfirmation', Component: SecondConFirm, }, - { - name: 'workflowConfig', - Component: WorkflowConfig, - useVisible() { - const fieldSchema = useFieldSchema(); - return isValid(fieldSchema?.['x-action-settings']?.triggerWorkflows); - }, - }, { name: 'saveMode', Component: SaveMode, diff --git a/packages/core/client/src/modules/actions/submit/updateSubmitActionSettings.tsx b/packages/core/client/src/modules/actions/submit/updateSubmitActionSettings.tsx index 7b62deca6d..560785611e 100644 --- a/packages/core/client/src/modules/actions/submit/updateSubmitActionSettings.tsx +++ b/packages/core/client/src/modules/actions/submit/updateSubmitActionSettings.tsx @@ -19,7 +19,6 @@ import { RemoveButton, SecondConFirm, SkipValidation, - WorkflowConfig, } from '../../../schema-component/antd/action/Action.Designer'; import { SaveMode } from './createSubmitActionSettings'; import { SchemaSettingsLinkageRules } from '../../../schema-settings'; @@ -56,14 +55,6 @@ export const updateSubmitActionSettings = new SchemaSettings({ name: 'secondConfirmation', Component: SecondConFirm, }, - { - name: 'workflowConfig', - Component: WorkflowConfig, - useVisible() { - const fieldSchema = useFieldSchema(); - return isValid(fieldSchema?.['x-action-settings']?.triggerWorkflows); - }, - }, { name: 'assignFieldValues', Component: AssignedFieldValues, @@ -120,14 +111,6 @@ export const submitActionSettings = new SchemaSettings({ name: 'secondConfirmation', Component: SecondConFirm, }, - { - name: 'workflowConfig', - Component: WorkflowConfig, - useVisible() { - const fieldSchema = useFieldSchema(); - return isValid(fieldSchema?.['x-action-settings']?.triggerWorkflows); - }, - }, { name: 'saveMode', Component: SaveMode, diff --git a/packages/core/client/src/modules/actions/update-record/customizeUpdateRecordActionSettings.tsx b/packages/core/client/src/modules/actions/update-record/customizeUpdateRecordActionSettings.tsx index 4464f86824..a832027109 100644 --- a/packages/core/client/src/modules/actions/update-record/customizeUpdateRecordActionSettings.tsx +++ b/packages/core/client/src/modules/actions/update-record/customizeUpdateRecordActionSettings.tsx @@ -18,7 +18,6 @@ import { ButtonEditor, RemoveButton, SecondConFirm, - WorkflowConfig, RefreshDataBlockRequest, } from '../../../schema-component/antd/action/Action.Designer'; import { SchemaSettingsLinkageRules } from '../../../schema-settings'; @@ -58,14 +57,6 @@ export const customizeUpdateRecordActionSettings = new SchemaSettings({ name: 'afterSuccessfulSubmission', Component: AfterSuccess, }, - { - name: 'workflowConfig', - Component: WorkflowConfig, - useVisible() { - const fieldSchema = useFieldSchema(); - return isValid(fieldSchema?.['x-action-settings']?.triggerWorkflows); - }, - }, { name: 'refreshDataBlockRequest', Component: RefreshDataBlockRequest, diff --git a/packages/core/client/src/schema-component/antd/action/Action.Designer.tsx b/packages/core/client/src/schema-component/antd/action/Action.Designer.tsx index 601b635cf3..63e758f4fb 100644 --- a/packages/core/client/src/schema-component/antd/action/Action.Designer.tsx +++ b/packages/core/client/src/schema-component/antd/action/Action.Designer.tsx @@ -7,25 +7,16 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import { ArrayTable } from '@formily/antd-v5'; -import { onFieldValueChange } from '@formily/core'; -import { ISchema, useField, useFieldSchema, useForm, useFormEffects } from '@formily/react'; +import { ISchema, useField, useFieldSchema } from '@formily/react'; import { isValid, uid } from '@formily/shared'; -import { Alert, Flex, ModalProps, Tag } from 'antd'; -import React, { useCallback, useMemo, useState } from 'react'; +import { ModalProps } from 'antd'; +import React, { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -import { RemoteSelect, useCompile, useDesignable } from '../..'; +import { useCompile, useDesignable } from '../..'; import { isInitializersSame, useApp } from '../../../application'; -import { usePlugin } from '../../../application/hooks'; import { SchemaSettingOptions, SchemaSettings } from '../../../application/schema-settings'; import { useSchemaToolbar } from '../../../application/schema-toolbar'; -import { useFormBlockContext } from '../../../block-provider/FormBlockProvider'; -import { - joinCollectionName, - useCollectionManager_deprecated, - useCollection_deprecated, -} from '../../../collection-manager'; -import { DataSourceProvider, useDataSourceKey } from '../../../data-source'; +import { useCollectionManager_deprecated, useCollection_deprecated } from '../../../collection-manager'; import { FlagProvider } from '../../../flag-provider'; import { SaveMode } from '../../../modules/actions/submit/createSubmitActionSettings'; import { useOpenModeContext } from '../../../modules/popup/OpenModeProvider'; @@ -392,287 +383,6 @@ export function RemoveButton( ); } -function WorkflowSelect({ formAction, buttonAction, actionType, ...props }) { - const { t } = useTranslation(); - const index = ArrayTable.useIndex(); - const { setValuesIn } = useForm(); - const baseCollection = useCollection_deprecated(); - const { getCollection } = useCollectionManager_deprecated(); - const dataSourceKey = useDataSourceKey(); - const [workflowCollection, setWorkflowCollection] = useState(joinCollectionName(dataSourceKey, baseCollection.name)); - const compile = useCompile(); - - const workflowPlugin = usePlugin('workflow') as any; - const triggerOptions = workflowPlugin.useTriggersOptions(); - const workflowTypes = useMemo( - () => - triggerOptions - .filter((item) => { - return typeof item.options.isActionTriggerable === 'function' || item.options.isActionTriggerable === true; - }) - .map((item) => item.value), - [triggerOptions], - ); - - useFormEffects(() => { - onFieldValueChange(`group[${index}].context`, (field) => { - let collection: any = baseCollection; - if (field.value) { - const paths = field.value.split('.'); - for (let i = 0; i < paths.length && collection; i++) { - const path = paths[i]; - const associationField = collection.fields.find((f) => f.name === path); - if (associationField) { - collection = getCollection(associationField.target, dataSourceKey); - } - } - } - setWorkflowCollection(joinCollectionName(dataSourceKey, collection.name)); - setValuesIn(`group[${index}].workflowKey`, null); - }); - }); - - const optionFilter = useCallback( - ({ key, type, config }) => { - if (key === props.value) { - return true; - } - const trigger = workflowPlugin.triggers.get(type); - if (trigger.isActionTriggerable === true) { - return true; - } - if (typeof trigger.isActionTriggerable === 'function') { - return trigger.isActionTriggerable(config, { - action: actionType, - formAction, - buttonAction, - /** - * @deprecated - */ - direct: buttonAction === 'customize:triggerWorkflows', - }); - } - return false; - }, - [props.value, workflowPlugin.triggers, formAction, buttonAction, actionType], - ); - - return ( - - { - const typeOption = triggerOptions.find((item) => item.value === data.type); - return typeOption ? ( - - {label} - {compile(typeOption.label)} - - ) : ( - label - ); - }} - {...props} - /> - - ); -} - -export function WorkflowConfig() { - const { dn } = useDesignable(); - const { t } = useTranslation(); - const fieldSchema = useFieldSchema(); - const collection = useCollection_deprecated(); - // TODO(refactor): should refactor for getting certain action type, better from 'x-action'. - const formBlock = useFormBlockContext(); - /** - * @deprecated - */ - const actionType = formBlock?.type || fieldSchema['x-action']; - const formAction = formBlock?.type; - const buttonAction = fieldSchema['x-action']; - - const description = { - submit: t('Support pre-action event (local mode), post-action event (local mode), and approval event here.', { - ns: 'workflow', - }), - 'customize:save': t( - 'Support pre-action event (local mode), post-action event (local mode), and approval event here.', - { - ns: 'workflow', - }, - ), - 'customize:update': t( - 'Support pre-action event (local mode), post-action event (local mode), and approval event here.', - { ns: 'workflow' }, - ), - 'customize:triggerWorkflows': t( - 'Workflow will be triggered directly once the button clicked, without data saving. Only supports to be bound with "Custom action event".', - { ns: '@nocobase/plugin-workflow-custom-action-trigger' }, - ), - 'customize:triggerWorkflows_deprecated': t( - '"Submit to workflow" to "Post-action event" is deprecated, please use "Custom action event" instead.', - { ns: 'workflow' }, - ), - destroy: t('Workflow will be triggered before deleting succeeded (only supports pre-action event in local mode).', { - ns: 'workflow', - }), - }[fieldSchema?.['x-action']]; - - return ( - { - fieldSchema['x-action-settings']['triggerWorkflows'] = group; - dn.emit('patch', { - schema: { - ['x-uid']: fieldSchema['x-uid'], - 'x-action-settings': fieldSchema['x-action-settings'], - }, - }); - }} - /> - ); -} - export const actionSettingsItems: SchemaSettingOptions['items'] = [ { name: 'Customize', @@ -774,14 +484,6 @@ export const actionSettingsItems: SchemaSettingOptions['items'] = [ return isValid(fieldSchema?.['x-action-settings']?.onSuccess); }, }, - { - name: 'workflowConfig', - Component: WorkflowConfig, - useVisible() { - const fieldSchema = useFieldSchema(); - return isValid(fieldSchema?.['x-action-settings']?.triggerWorkflows); - }, - }, { name: 'saveMode', Component: SaveMode, diff --git a/packages/core/client/src/schema-component/antd/action/Action.tsx b/packages/core/client/src/schema-component/antd/action/Action.tsx index 4d37f22a7d..4f89630525 100644 --- a/packages/core/client/src/schema-component/antd/action/Action.tsx +++ b/packages/core/client/src/schema-component/antd/action/Action.tsx @@ -47,6 +47,7 @@ import { ActionContextProvider } from './context'; import { useGetAriaLabelOfAction } from './hooks/useGetAriaLabelOfAction'; import { ActionContextProps, ActionProps, ComposedAction } from './types'; import { linkageAction, setInitialActionState } from './utils'; +import { useApp } from '../../../application'; const useA = () => { return { @@ -95,7 +96,7 @@ export const Action: ComposedAction = withDynamicSchemaProps( const { setSubmitted } = useActionContext(); const { getAriaLabel } = useGetAriaLabelOfAction(title); const parentRecordData = useCollectionParentRecordData(); - + const app = useApp(); useEffect(() => { if (field.stateOfLinkageRules) { setInitialActionState(field); @@ -105,13 +106,16 @@ export const Action: ComposedAction = withDynamicSchemaProps( .filter((k) => !k.disabled) .forEach((v) => { v.actions?.forEach((h) => { - linkageAction({ - operator: h.operator, - field, - condition: v.condition, - variables, - localVariables, - }); + linkageAction( + { + operator: h.operator, + field, + condition: v.condition, + variables, + localVariables, + }, + app.jsonLogic, + ); }); }); }, [field, linkageRules, localVariables, variables]); diff --git a/packages/core/client/src/schema-component/antd/action/utils.ts b/packages/core/client/src/schema-component/antd/action/utils.ts index 5e7677ffec..5254021916 100644 --- a/packages/core/client/src/schema-component/antd/action/utils.ts +++ b/packages/core/client/src/schema-component/antd/action/utils.ts @@ -80,25 +80,28 @@ export const requestSettingsSchema: ISchema = { }, }; -export const linkageAction = async ({ - operator, - field, - condition, - variables, - localVariables, -}: { - operator; - field; - condition; - variables: VariablesContextType; - localVariables: VariableOption[]; -}) => { +export const linkageAction = async ( + { + operator, + field, + condition, + variables, + localVariables, + }: { + operator; + field; + condition; + variables: VariablesContextType; + localVariables: VariableOption[]; + }, + jsonLogic: any, +) => { const disableResult = field?.stateOfLinkageRules?.disabled || [false]; const displayResult = field?.stateOfLinkageRules?.display || ['visible']; switch (operator) { case ActionType.Visible: - if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables })) { + if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables }, jsonLogic)) { displayResult.push(operator); field.data = field.data || {}; field.data.hidden = false; @@ -110,7 +113,7 @@ export const linkageAction = async ({ field.display = last(displayResult); break; case ActionType.Hidden: - if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables })) { + if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables }, jsonLogic)) { field.data = field.data || {}; field.data.hidden = true; } else { @@ -119,7 +122,7 @@ export const linkageAction = async ({ } break; case ActionType.Disabled: - if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables })) { + if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables }, jsonLogic)) { disableResult.push(true); } field.stateOfLinkageRules = { @@ -130,7 +133,7 @@ export const linkageAction = async ({ field.componentProps['disabled'] = last(disableResult); break; case ActionType.Active: - if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables })) { + if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables }, jsonLogic)) { disableResult.push(false); } else { disableResult.push(!!field.componentProps?.['disabled']); diff --git a/packages/core/client/src/schema-component/antd/form-item/hooks/useLinkageRulesForSubTableOrSubForm.ts b/packages/core/client/src/schema-component/antd/form-item/hooks/useLinkageRulesForSubTableOrSubForm.ts index f59ba764b7..2fc7a4b2a8 100644 --- a/packages/core/client/src/schema-component/antd/form-item/hooks/useLinkageRulesForSubTableOrSubForm.ts +++ b/packages/core/client/src/schema-component/antd/form-item/hooks/useLinkageRulesForSubTableOrSubForm.ts @@ -16,6 +16,7 @@ import { forEachLinkageRule } from '../../../../schema-settings/LinkageRules/for import useLocalVariables from '../../../../variables/hooks/useLocalVariables'; import useVariables from '../../../../variables/hooks/useVariables'; import { useSubFormValue } from '../../association-field/hooks'; +import { useApp } from '../../../../application'; import { isSubMode } from '../../association-field/util'; const isSubFormOrSubTableField = (fieldSchema: Schema) => { @@ -45,6 +46,7 @@ export const useLinkageRulesForSubTableOrSubForm = () => { const variables = useVariables(); const linkageRules = getLinkageRules(schemaOfSubTableOrSubForm); + const app = useApp(); useEffect(() => { if (!isSubFormOrSubTableField(fieldSchema)) { @@ -77,16 +79,19 @@ export const useLinkageRulesForSubTableOrSubForm = () => { forEachLinkageRule(linkageRules, (action, rule) => { if (action.targetFields?.includes(fieldSchema.name)) { disposes.push( - bindLinkageRulesToFiled({ - field, - linkageRules, - formValues: formValue, - localVariables, - action, - rule, - variables, - variableNameOfLeftCondition: '$iteration', - }), + bindLinkageRulesToFiled( + { + field, + linkageRules, + formValues: formValue, + localVariables, + action, + rule, + variables, + variableNameOfLeftCondition: '$iteration', + }, + app.jsonLogic, + ), ); } }); diff --git a/packages/core/client/src/schema-component/antd/form-v2/Form.tsx b/packages/core/client/src/schema-component/antd/form-v2/Form.tsx index d304ba463b..f099766a90 100644 --- a/packages/core/client/src/schema-component/antd/form-v2/Form.tsx +++ b/packages/core/client/src/schema-component/antd/form-v2/Form.tsx @@ -27,6 +27,7 @@ import { useToken } from '../../../style'; import { useLocalVariables, useVariables } from '../../../variables'; import { useProps } from '../../hooks/useProps'; import { useFormBlockHeight } from './hook'; +import { useApp } from '../../../application'; export interface FormProps extends IFormLayoutProps { form?: FormilyForm; @@ -136,6 +137,7 @@ const WithForm = (props: WithFormProps) => { const localVariables = useLocalVariables({ currentForm: form }); const { templateFinished } = useTemplateBlockContext(); const { loading } = useDataBlockRequest() || {}; + const app = useApp(); const linkageRules: any[] = (getLinkageRules(fieldSchema) || fieldSchema.parent?.['x-linkage-rules'])?.filter((k) => !k.disabled) || []; @@ -175,15 +177,18 @@ const WithForm = (props: WithFormProps) => { // 之前使用的 `onFieldReact` 有问题,没有办法被取消监听,所以这里用 `onFieldInit` 和 `reaction` 代替 onFieldInit(`*(${fields})`, (field: any, form) => { disposes.push( - bindLinkageRulesToFiled({ - field, - linkageRules, - formValues: form.values, - localVariables, - action, - rule, - variables, - }), + bindLinkageRulesToFiled( + { + field, + linkageRules, + formValues: form.values, + localVariables, + action, + rule, + variables, + }, + app.jsonLogic, + ), ); }); } diff --git a/packages/core/client/src/schema-component/antd/record-picker/util.ts b/packages/core/client/src/schema-component/antd/record-picker/util.tsx similarity index 91% rename from packages/core/client/src/schema-component/antd/record-picker/util.ts rename to packages/core/client/src/schema-component/antd/record-picker/util.tsx index d289eb1c0d..b9af341709 100644 --- a/packages/core/client/src/schema-component/antd/record-picker/util.ts +++ b/packages/core/client/src/schema-component/antd/record-picker/util.tsx @@ -11,7 +11,7 @@ import { ISchema } from '@formily/react'; import { isArr } from '@formily/shared'; import { dayjs, getDefaultFormat, str2moment } from '@nocobase/utils/client'; import { Tag } from 'antd'; -import React from 'react'; +import React, { Component } from 'react'; import { CollectionFieldOptions_deprecated, useCollectionManager_deprecated } from '../../../collection-manager'; export const useLabelUiSchema = (collectionField: CollectionFieldOptions_deprecated, label: string): ISchema => { @@ -30,7 +30,10 @@ export const getDatePickerLabels = (props): string => { return isArr(labels) ? labels.join('~') : labels; }; -export const getLabelFormatValue = (labelUiSchema: ISchema, value: any, isTag = false): any => { +export const getLabelFormatValue = (labelUiSchema: ISchema, value: any, isTag = false, TitleRenderer?: any): any => { + if (TitleRenderer) { + return ; + } if (Array.isArray(labelUiSchema?.enum) && value) { const opt: any = labelUiSchema.enum.find((option: any) => option.value === value); if (isTag) { diff --git a/packages/core/client/src/schema-component/common/utils/uitls.tsx b/packages/core/client/src/schema-component/common/utils/uitls.tsx index 836abdbcc2..198f98cfa5 100644 --- a/packages/core/client/src/schema-component/common/utils/uitls.tsx +++ b/packages/core/client/src/schema-component/common/utils/uitls.tsx @@ -14,7 +14,7 @@ import { VariableOption, VariablesContextType } from '../../../variables/types'; import { isVariable } from '../../../variables/utils/isVariable'; import { transformVariableValue } from '../../../variables/utils/transformVariableValue'; import { inferPickerType } from '../../antd/date-picker/util'; -import { getJsonLogic } from '../../common/utils/logic'; + type VariablesCtx = { /** 当前登录的用户 */ $user?: Record; @@ -76,31 +76,34 @@ function getAllKeys(obj) { return keys; } -export const conditionAnalyses = async ({ - ruleGroup, - variables, - localVariables, - variableNameOfLeftCondition, -}: { - ruleGroup; - variables: VariablesContextType; - localVariables: VariableOption[]; - /** - * used to parse the variable name of the left condition value - * @default '$nForm' - */ - variableNameOfLeftCondition?: string; -}) => { +export const conditionAnalyses = async ( + { + ruleGroup, + variables, + localVariables, + variableNameOfLeftCondition, + }: { + ruleGroup; + variables: VariablesContextType; + localVariables: VariableOption[]; + /** + * used to parse the variable name of the left condition value + * @default '$nForm' + */ + variableNameOfLeftCondition?: string; + }, + jsonLogic: any, +) => { const type = Object.keys(ruleGroup)[0] || '$and'; const conditions = ruleGroup[type]; let results = conditions.map(async (condition) => { if ('$and' in condition || '$or' in condition) { - return await conditionAnalyses({ ruleGroup: condition, variables, localVariables }); + return await conditionAnalyses({ ruleGroup: condition, variables, localVariables }, jsonLogic); } - const jsonlogic = getInnermostKeyAndValue(condition); - const operator = jsonlogic?.key; + const logicCalculation = getInnermostKeyAndValue(condition); + const operator = logicCalculation?.key; if (!operator) { return true; @@ -113,12 +116,11 @@ export const conditionAnalyses = async ({ }) .then(({ value }) => value); - const parsingResult = isVariable(jsonlogic?.value) - ? [variables.parseVariable(jsonlogic?.value, localVariables).then(({ value }) => value), targetValue] - : [jsonlogic?.value, targetValue]; + const parsingResult = isVariable(logicCalculation?.value) + ? [variables.parseVariable(logicCalculation?.value, localVariables).then(({ value }) => value), targetValue] + : [logicCalculation?.value, targetValue]; try { - const jsonLogic = getJsonLogic(); const [value, targetValue] = await Promise.all(parsingResult); const targetCollectionField = await variables.getCollectionField(targetVariableName, localVariables); let currentInputValue = transformVariableValue(targetValue, { targetCollectionField }); diff --git a/packages/core/client/src/schema-initializer/components/CreateRecordAction.tsx b/packages/core/client/src/schema-initializer/components/CreateRecordAction.tsx index 0fc08b9b60..a940d82085 100644 --- a/packages/core/client/src/schema-initializer/components/CreateRecordAction.tsx +++ b/packages/core/client/src/schema-initializer/components/CreateRecordAction.tsx @@ -22,6 +22,7 @@ import { linkageAction } from '../../schema-component/antd/action/utils'; import { usePopupUtils } from '../../schema-component/antd/page/pagePopupUtils'; import { parseVariables } from '../../schema-component/common/utils/uitls'; import { useLocalVariables, useVariables } from '../../variables'; +import { useApp } from '../../application'; export function useAclCheck(actionPath) { const aclCheck = useAclCheckFn(); @@ -73,6 +74,7 @@ const InternalCreateRecordAction = (props: any, ref) => { const { openPopup } = usePopupUtils(); const treeRecordData = useTreeParentRecord(); const cm = useCollectionManager(); + const app = useApp(); useEffect(() => { field.stateOfLinkageRules = {}; @@ -80,13 +82,16 @@ const InternalCreateRecordAction = (props: any, ref) => { .filter((k) => !k.disabled) .forEach((v) => { v.actions?.forEach((h) => { - linkageAction({ - operator: h.operator, - field, - condition: v.condition, - variables, - localVariables, - }); + linkageAction( + { + operator: h.operator, + field, + condition: v.condition, + variables, + localVariables, + }, + app.jsonLogic, + ); }); }); }, [field, linkageRules, localVariables, variables]); @@ -143,7 +148,6 @@ export const CreateAction = observer( 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']; @@ -176,6 +180,7 @@ export const CreateAction = observer( const compile = useCompile(); const { designable } = useDesignable(); const icon = props.icon || null; + const app = useApp(); const menuItems = useMemo(() => { return inheritsCollections.map((option) => ({ key: option.name, @@ -196,13 +201,16 @@ export const CreateAction = observer( .filter((k) => !k.disabled) .forEach((v) => { v.actions?.forEach((h) => { - linkageAction({ - operator: h.operator, - field, - condition: v.condition, - variables, - localVariables, - }); + linkageAction( + { + operator: h.operator, + field, + condition: v.condition, + variables, + localVariables, + }, + app.jsonLogic, + ); }); }); }, [field, linkageRules, localVariables, variables]); diff --git a/packages/plugins/@nocobase/plugin-block-workbench/src/client/ModalActionSchemaInitializerItem.tsx b/packages/core/client/src/schema-initializer/items/ModalActionSchemaInitializerItem.tsx similarity index 94% rename from packages/plugins/@nocobase/plugin-block-workbench/src/client/ModalActionSchemaInitializerItem.tsx rename to packages/core/client/src/schema-initializer/items/ModalActionSchemaInitializerItem.tsx index 4cb4d0cf55..370ad2b5db 100644 --- a/packages/plugins/@nocobase/plugin-block-workbench/src/client/ModalActionSchemaInitializerItem.tsx +++ b/packages/core/client/src/schema-initializer/items/ModalActionSchemaInitializerItem.tsx @@ -20,7 +20,7 @@ import { uid } from '@nocobase/utils/client'; import React, { useMemo, useState } from 'react'; export function ModalActionSchemaInitializerItem(props) { - const { modalSchema = {}, ...otherProps } = props; + const { modalSchema = {}, components = {}, ...otherProps } = props; const { properties, ...others } = modalSchema; const [visible, setVisible] = useState(false); const { setVisible: setSchemaInitializerVisible } = useSchemaInitializer(); @@ -92,7 +92,7 @@ export function ModalActionSchemaInitializerItem(props) { }} /> - + ); diff --git a/packages/core/client/src/schema-initializer/items/index.tsx b/packages/core/client/src/schema-initializer/items/index.tsx index 242c6a7063..4397589d1a 100644 --- a/packages/core/client/src/schema-initializer/items/index.tsx +++ b/packages/core/client/src/schema-initializer/items/index.tsx @@ -30,3 +30,4 @@ export * from './RecordReadPrettyAssociationFormBlockInitializer'; export * from './SelectActionInitializer'; export * from './SubmitActionInitializer'; export * from './TableActionColumnInitializer'; +export * from './ModalActionSchemaInitializerItem'; diff --git a/packages/core/client/src/schema-settings/LinkageRules/bindLinkageRulesToFiled.ts b/packages/core/client/src/schema-settings/LinkageRules/bindLinkageRulesToFiled.ts index 091287b69b..be17782a4e 100644 --- a/packages/core/client/src/schema-settings/LinkageRules/bindLinkageRulesToFiled.ts +++ b/packages/core/client/src/schema-settings/LinkageRules/bindLinkageRulesToFiled.ts @@ -39,29 +39,32 @@ interface Props { variableNameOfLeftCondition?: string; } -export function bindLinkageRulesToFiled({ - field, - linkageRules, - formValues, - localVariables, - action, - rule, - variables, - variableNameOfLeftCondition, -}: { - field: any; - linkageRules: any[]; - formValues: any; - localVariables: VariableOption[]; - action: any; - rule: any; - variables: VariablesContextType; - /** - * used to parse the variable name of the left condition value - * @default '$nForm' - */ - variableNameOfLeftCondition?: string; -}) { +export function bindLinkageRulesToFiled( + { + field, + linkageRules, + formValues, + localVariables, + action, + rule, + variables, + variableNameOfLeftCondition, + }: { + field: any; + linkageRules: any[]; + formValues: any; + localVariables: VariableOption[]; + action: any; + rule: any; + variables: VariablesContextType; + /** + * used to parse the variable name of the left condition value + * @default '$nForm' + */ + variableNameOfLeftCondition?: string; + }, + jsonLogic: any, +) { field['initStateOfLinkageRules'] = { display: field.initStateOfLinkageRules?.display || getTempFieldState(true, field.display), required: field.initStateOfLinkageRules?.required || getTempFieldState(true, field.required || false), @@ -89,7 +92,7 @@ export function bindLinkageRulesToFiled({ .join(','); return result; }, - getSubscriber({ action, field, rule, variables, localVariables, variableNameOfLeftCondition }), + getSubscriber({ action, field, rule, variables, localVariables, variableNameOfLeftCondition }, jsonLogic), { fireImmediately: true, equals: _.isEqual }, ); } @@ -176,36 +179,42 @@ function getVariableValue(variableString: string, localVariables: VariableOption return getValuesByPath(ctx, getPath(variableString)); } -function getSubscriber({ - action, - field, - rule, - variables, - localVariables, - variableNameOfLeftCondition, -}: { - action: any; - field: any; - rule: any; - variables: VariablesContextType; - localVariables: VariableOption[]; - /** - * used to parse the variable name of the left condition value - * @default '$nForm' - */ - variableNameOfLeftCondition?: string; -}): (value: string, oldValue: string) => void { +function getSubscriber( + { + action, + field, + rule, + variables, + localVariables, + variableNameOfLeftCondition, + }: { + action: any; + field: any; + rule: any; + variables: VariablesContextType; + localVariables: VariableOption[]; + /** + * used to parse the variable name of the left condition value + * @default '$nForm' + */ + variableNameOfLeftCondition?: string; + }, + jsonLogic, +): (value: string, oldValue: string) => void { return () => { // 当条件改变触发 reaction 时,会同步收集字段状态,并保存到 field.stateOfLinkageRules 中 - collectFieldStateOfLinkageRules({ - operator: action.operator, - value: action.value, - field, - condition: rule.condition, - variables, - localVariables, - variableNameOfLeftCondition, - }); + collectFieldStateOfLinkageRules( + { + operator: action.operator, + value: action.value, + field, + condition: rule.condition, + variables, + localVariables, + variableNameOfLeftCondition, + }, + jsonLogic, + ); // 当条件改变时,有可能会触发多个 reaction,所以这里需要延迟一下,确保所有的 reaction 都执行完毕后, // 再从 field.stateOfLinkageRules 中取值,因为此时 field.stateOfLinkageRules 中的值才是全的。 @@ -286,15 +295,10 @@ function getFieldNameByOperator(operator: ActionType) { } } -export const collectFieldStateOfLinkageRules = ({ - operator, - value, - field, - condition, - variables, - localVariables, - variableNameOfLeftCondition, -}: Props) => { +export const collectFieldStateOfLinkageRules = ( + { operator, value, field, condition, variables, localVariables, variableNameOfLeftCondition }: Props, + jsonLogic: any, +) => { const requiredResult = field?.stateOfLinkageRules?.required || [field?.initStateOfLinkageRules?.required]; const displayResult = field?.stateOfLinkageRules?.display || [field?.initStateOfLinkageRules?.display]; const patternResult = field?.stateOfLinkageRules?.pattern || [field?.initStateOfLinkageRules?.pattern]; @@ -304,14 +308,14 @@ export const collectFieldStateOfLinkageRules = ({ switch (operator) { case ActionType.Required: - requiredResult.push(getTempFieldState(conditionAnalyses(paramsToGetConditionResult), true)); + requiredResult.push(getTempFieldState(conditionAnalyses(paramsToGetConditionResult, jsonLogic), true)); field.stateOfLinkageRules = { ...field.stateOfLinkageRules, required: requiredResult, }; break; case ActionType.InRequired: - requiredResult.push(getTempFieldState(conditionAnalyses(paramsToGetConditionResult), false)); + requiredResult.push(getTempFieldState(conditionAnalyses(paramsToGetConditionResult, jsonLogic), false)); field.stateOfLinkageRules = { ...field.stateOfLinkageRules, required: requiredResult, @@ -320,7 +324,7 @@ export const collectFieldStateOfLinkageRules = ({ case ActionType.Visible: case ActionType.None: case ActionType.Hidden: - displayResult.push(getTempFieldState(conditionAnalyses(paramsToGetConditionResult), operator)); + displayResult.push(getTempFieldState(conditionAnalyses(paramsToGetConditionResult, jsonLogic), operator)); field.stateOfLinkageRules = { ...field.stateOfLinkageRules, display: displayResult, @@ -329,7 +333,7 @@ export const collectFieldStateOfLinkageRules = ({ case ActionType.Editable: case ActionType.ReadOnly: case ActionType.ReadPretty: - patternResult.push(getTempFieldState(conditionAnalyses(paramsToGetConditionResult), operator)); + patternResult.push(getTempFieldState(conditionAnalyses(paramsToGetConditionResult, jsonLogic), operator)); field.stateOfLinkageRules = { ...field.stateOfLinkageRules, pattern: patternResult, @@ -364,7 +368,7 @@ export const collectFieldStateOfLinkageRules = ({ if (isConditionEmpty(condition)) { valueResult.push(getTempFieldState(true, getValue())); } else { - valueResult.push(getTempFieldState(conditionAnalyses(paramsToGetConditionResult), getValue())); + valueResult.push(getTempFieldState(conditionAnalyses(paramsToGetConditionResult, jsonLogic), getValue())); } field.stateOfLinkageRules = { ...field.stateOfLinkageRules, diff --git a/packages/core/client/src/schema-settings/LinkageRules/compute-rules.ts b/packages/core/client/src/schema-settings/LinkageRules/compute-rules.ts index 2e56cc8a1c..2c8daf81ca 100644 --- a/packages/core/client/src/schema-settings/LinkageRules/compute-rules.ts +++ b/packages/core/client/src/schema-settings/LinkageRules/compute-rules.ts @@ -25,13 +25,13 @@ const getActionValue = (operator, value) => { } }; -const getSatisfiedActions = async ({ rules, variables, localVariables }) => { +const getSatisfiedActions = async ({ rules, variables, localVariables }, jsonLogic) => { const satisfiedRules = ( await Promise.all( rules .filter((k) => !k.disabled) .map(async (rule) => { - if (await conditionAnalyses({ ruleGroup: rule.condition, variables, localVariables })) { + if (await conditionAnalyses({ ruleGroup: rule.condition, variables, localVariables }, jsonLogic)) { return rule; } else return null; }), @@ -40,15 +40,15 @@ const getSatisfiedActions = async ({ rules, variables, localVariables }) => { return satisfiedRules.map((rule) => rule.actions).flat(); }; -const getSatisfiedValues = async ({ rules, variables, localVariables }) => { - return (await getSatisfiedActions({ rules, variables, localVariables })).map((action) => ({ +const getSatisfiedValues = async ({ rules, variables, localVariables }, jsonLogic) => { + return (await getSatisfiedActions({ rules, variables, localVariables }, jsonLogic)).map((action) => ({ ...action, value: getActionValue(action.operator, action.value), })); }; -export const getSatisfiedValueMap = async ({ rules, variables, localVariables }) => { - const values = await getSatisfiedValues({ rules, variables, localVariables }); +export const getSatisfiedValueMap = async ({ rules, variables, localVariables }, jsonLogic) => { + const values = await getSatisfiedValues({ rules, variables, localVariables }, jsonLogic); const valueMap = values.reduce((a, v) => ({ ...a, [v.operator]: v.value }), {}); return valueMap; }; diff --git a/packages/core/client/src/schema-settings/LinkageRules/useActionValues.ts b/packages/core/client/src/schema-settings/LinkageRules/useActionValues.ts index f54ee21e0d..eb857fc6cf 100644 --- a/packages/core/client/src/schema-settings/LinkageRules/useActionValues.ts +++ b/packages/core/client/src/schema-settings/LinkageRules/useActionValues.ts @@ -15,7 +15,7 @@ import React, { useCallback, useEffect, useState } from 'react'; import { useLocalVariables, useVariables } from '../../variables'; import { getSatisfiedValueMap } from './compute-rules'; import { LinkageRuleCategory, LinkageRuleDataKeyMap } from './type'; - +import { useApp } from '../../application'; export function useSatisfiedActionValues({ formValues, category = 'default', @@ -35,10 +35,11 @@ export function useSatisfiedActionValues({ const localVariables = useLocalVariables({ currentForm: { values: formValues } as any }); const localSchema = schema ?? fieldSchema; const styleRules = rules ?? localSchema[LinkageRuleDataKeyMap[category]]; + const app = useApp(); const compute = useCallback(() => { if (styleRules && formValues) { - getSatisfiedValueMap({ rules: styleRules, variables, localVariables }) + getSatisfiedValueMap({ rules: styleRules, variables, localVariables }, app.jsonLogic) .then((valueMap) => { if (!isEmpty(valueMap)) { setValueMap(valueMap); diff --git a/packages/plugins/@nocobase/plugin-action-bulk-edit/src/client/BulkEditAction.Settings.tsx b/packages/plugins/@nocobase/plugin-action-bulk-edit/src/client/BulkEditAction.Settings.tsx index cbedb56281..713d5f0c1b 100644 --- a/packages/plugins/@nocobase/plugin-action-bulk-edit/src/client/BulkEditAction.Settings.tsx +++ b/packages/plugins/@nocobase/plugin-action-bulk-edit/src/client/BulkEditAction.Settings.tsx @@ -19,7 +19,6 @@ import { useOpenModeContext, useSchemaToolbar, SecondConFirm, - WorkflowConfig, AfterSuccess, RefreshDataBlockRequest, } from '@nocobase/client'; @@ -188,14 +187,6 @@ export const bulkEditFormSubmitActionSettings = new SchemaSettings({ name: 'secondConfirmation', Component: SecondConFirm, }, - { - name: 'workflowConfig', - Component: WorkflowConfig, - useVisible() { - const fieldSchema = useFieldSchema(); - return isValid(fieldSchema?.['x-action-settings']?.triggerWorkflows); - }, - }, { name: 'afterSuccessfulSubmission', Component: AfterSuccess, diff --git a/packages/plugins/@nocobase/plugin-action-print/src/client/__e2e__/schemaInitailizer.test.ts b/packages/plugins/@nocobase/plugin-action-print/src/client/__e2e__/schemaInitailizer.test.ts index 0db69c31d2..6969ac2974 100644 --- a/packages/plugins/@nocobase/plugin-action-print/src/client/__e2e__/schemaInitailizer.test.ts +++ b/packages/plugins/@nocobase/plugin-action-print/src/client/__e2e__/schemaInitailizer.test.ts @@ -25,7 +25,7 @@ test.describe('ReadPrettyFormActionInitializers & CalendarFormActionInitializers const nocoPage = await mockPage(oneCalenderWithViewAction).waitForInit(); await mockRecord('general', { singleLineText: 'test' }); await nocoPage.goto(); - await page.getByTitle('test').click(); + await page.getByLabel('block-item-CardItem-general-').getByLabel('event-title').click(); await page.getByLabel('schema-initializer-ActionBar-details:configureActions-general').hover(); await page.getByRole('menuitem', { name: 'Print' }).click(); await page.getByLabel('action-Action-Print-print-general-form').click(); diff --git a/packages/plugins/@nocobase/plugin-block-workbench/src/client/WorkbenchAction.tsx b/packages/plugins/@nocobase/plugin-block-workbench/src/client/WorkbenchAction.tsx index 889dda2bc0..236b8d0ed1 100644 --- a/packages/plugins/@nocobase/plugin-block-workbench/src/client/WorkbenchAction.tsx +++ b/packages/plugins/@nocobase/plugin-block-workbench/src/client/WorkbenchAction.tsx @@ -8,7 +8,7 @@ */ import { useFieldSchema } from '@formily/react'; -import { Action, Icon, useComponent, withDynamicSchemaProps } from '@nocobase/client'; +import { Action, Icon, useCompile, useComponent, withDynamicSchemaProps } from '@nocobase/client'; import { Avatar } from 'antd'; import { createStyles } from 'antd-style'; import React, { useContext } from 'react'; @@ -36,13 +36,15 @@ function Button() { const backgroundColor = fieldSchema['x-component-props']?.['iconColor']; const { layout } = useContext(WorkbenchBlockContext); const { styles, cx } = useStyles(); + const compile = useCompile(); + const title = compile(fieldSchema.title); return layout === WorkbenchLayout.Grid ? ( -
+
} /> -
{fieldSchema.title}
+
{title}
) : ( - {fieldSchema.title} + {title} ); } diff --git a/packages/plugins/@nocobase/plugin-block-workbench/src/client/WorkbenchCustomRequestActionSchemaInitializerItem.tsx b/packages/plugins/@nocobase/plugin-block-workbench/src/client/WorkbenchCustomRequestActionSchemaInitializerItem.tsx index 3c3ba21adb..9b1da436a0 100644 --- a/packages/plugins/@nocobase/plugin-block-workbench/src/client/WorkbenchCustomRequestActionSchemaInitializerItem.tsx +++ b/packages/plugins/@nocobase/plugin-block-workbench/src/client/WorkbenchCustomRequestActionSchemaInitializerItem.tsx @@ -7,10 +7,15 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import { ButtonEditor, SchemaSettings, SchemaSettingsActionLinkItem, useSchemaInitializer } from '@nocobase/client'; +import { + ButtonEditor, + SchemaSettings, + SchemaSettingsActionLinkItem, + useSchemaInitializer, + ModalActionSchemaInitializerItem, +} from '@nocobase/client'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import { ModalActionSchemaInitializerItem } from './ModalActionSchemaInitializerItem'; export const workbenchActionSettingsCustomRequest = new SchemaSettings({ name: 'workbench:actionSettings:customRequest', items: [ diff --git a/packages/plugins/@nocobase/plugin-block-workbench/src/client/WorkbenchLinkActionSchemaInitializerItem.tsx b/packages/plugins/@nocobase/plugin-block-workbench/src/client/WorkbenchLinkActionSchemaInitializerItem.tsx index d8fded62e8..b7aa1e2a78 100644 --- a/packages/plugins/@nocobase/plugin-block-workbench/src/client/WorkbenchLinkActionSchemaInitializerItem.tsx +++ b/packages/plugins/@nocobase/plugin-block-workbench/src/client/WorkbenchLinkActionSchemaInitializerItem.tsx @@ -13,10 +13,10 @@ import { SchemaSettingsActionLinkItem, useSchemaInitializer, useSchemaInitializerItem, + ModalActionSchemaInitializerItem, } from '@nocobase/client'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import { ModalActionSchemaInitializerItem } from './ModalActionSchemaInitializerItem'; export const workbenchActionSettingsLink = new SchemaSettings({ name: 'workbench:actionSettings:link', diff --git a/packages/plugins/@nocobase/plugin-block-workbench/src/client/WorkbenchPopupActionSchemaInitializerItem.tsx b/packages/plugins/@nocobase/plugin-block-workbench/src/client/WorkbenchPopupActionSchemaInitializerItem.tsx index 0894db145a..f49beda7d3 100644 --- a/packages/plugins/@nocobase/plugin-block-workbench/src/client/WorkbenchPopupActionSchemaInitializerItem.tsx +++ b/packages/plugins/@nocobase/plugin-block-workbench/src/client/WorkbenchPopupActionSchemaInitializerItem.tsx @@ -13,10 +13,10 @@ import { SchemaSettings, useSchemaInitializer, useOpenModeContext, + ModalActionSchemaInitializerItem, } from '@nocobase/client'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import { ModalActionSchemaInitializerItem } from './ModalActionSchemaInitializerItem'; export const workbenchActionSettingsPopup = new SchemaSettings({ name: 'workbench:actionSettings:popup', diff --git a/packages/plugins/@nocobase/plugin-block-workbench/src/client/WorkbenchScanActionSchemaInitializerItem.tsx b/packages/plugins/@nocobase/plugin-block-workbench/src/client/WorkbenchScanActionSchemaInitializerItem.tsx index c6ea7464a3..de32050e93 100644 --- a/packages/plugins/@nocobase/plugin-block-workbench/src/client/WorkbenchScanActionSchemaInitializerItem.tsx +++ b/packages/plugins/@nocobase/plugin-block-workbench/src/client/WorkbenchScanActionSchemaInitializerItem.tsx @@ -13,10 +13,10 @@ import { SchemaSettings, useSchemaInitializer, useSchemaInitializerItem, + ModalActionSchemaInitializerItem, } from '@nocobase/client'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import { ModalActionSchemaInitializerItem } from './ModalActionSchemaInitializerItem'; export const workbenchActionSettingsScanQrCode = new SchemaSettings({ name: 'workbench:actionSettings:scanQrCode', diff --git a/packages/plugins/@nocobase/plugin-calendar/src/client/__e2e__/eventsBackgroundColor.test.ts b/packages/plugins/@nocobase/plugin-calendar/src/client/__e2e__/eventsBackgroundColor.test.ts index f1f6546ab7..ce682acb2e 100644 --- a/packages/plugins/@nocobase/plugin-calendar/src/client/__e2e__/eventsBackgroundColor.test.ts +++ b/packages/plugins/@nocobase/plugin-calendar/src/client/__e2e__/eventsBackgroundColor.test.ts @@ -10,7 +10,7 @@ import { expect, test } from '@nocobase/test/e2e'; import { backgroundColorFieldBasic } from './templates'; -test.describe('Background color field', () => { +test.describe('Color field', () => { test('basic', async ({ mockPage, mockRecords, page }) => { const nocoPage = await mockPage(backgroundColorFieldBasic).waitForInit(); await mockRecords('calendar', 3); @@ -19,18 +19,22 @@ test.describe('Background color field', () => { // 1. The default option is Not selected await page.getByLabel('block-item-CardItem-calendar-').hover(); await page.getByLabel('designer-schema-settings-CardItem-blockSettings:calendar-calendar').hover(); - await page.getByRole('menuitem', { name: 'Background color field Not selected' }).click(); + await page.getByRole('menuitem', { name: 'Color field Not selected' }).click(); // 2. Switch to the single select option await page.getByRole('option', { name: 'Single select' }).click(); - await expect(page.getByRole('menuitem', { name: 'Background color field Single select' })).toBeVisible(); + await page.getByLabel('block-item-CardItem-calendar-').hover(); + await page.getByLabel('designer-schema-settings-CardItem-blockSettings:calendar-calendar').hover(); + await expect(page.getByRole('menuitem', { name: 'Color field Single select' })).toBeVisible(); await page.mouse.move(-300, 0); // 3. Switch to the radio group option await page.getByLabel('block-item-CardItem-calendar-').hover(); await page.getByLabel('designer-schema-settings-CardItem-blockSettings:calendar-calendar').hover(); - await page.getByRole('menuitem', { name: 'Background color field Single select' }).click(); + await page.getByRole('menuitem', { name: 'Color field Single select' }).click(); await page.getByRole('option', { name: 'Radio group' }).click(); - await expect(page.getByRole('menuitem', { name: 'Background color field Radio group' })).toBeVisible(); + await page.getByLabel('block-item-CardItem-calendar-').hover(); + await page.getByLabel('designer-schema-settings-CardItem-blockSettings:calendar-calendar').hover(); + await expect(page.getByRole('menuitem', { name: 'Color field Radio group' })).toBeVisible(); }); }); diff --git a/packages/plugins/@nocobase/plugin-calendar/src/client/calendar/Calendar.tsx b/packages/plugins/@nocobase/plugin-calendar/src/client/calendar/Calendar.tsx index d911ee9b19..5dd7592368 100644 --- a/packages/plugins/@nocobase/plugin-calendar/src/client/calendar/Calendar.tsx +++ b/packages/plugins/@nocobase/plugin-calendar/src/client/calendar/Calendar.tsx @@ -30,10 +30,11 @@ import { useToken, withDynamicSchemaProps, withSkeletonComponent, + useApp, } from '@nocobase/client'; import type { Dayjs } from 'dayjs'; import dayjs from 'dayjs'; -import { cloneDeep, get } from 'lodash'; +import { cloneDeep, get, omit } from 'lodash'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { View } from 'react-big-calendar'; import { i18nt, useTranslation } from '../../locale'; @@ -116,6 +117,8 @@ const useEvents = ( ); const { t } = useTranslation(); const { fields } = useCollection(); + const app = useApp(); + const plugin = app.pm.get('calendar') as any; const labelUiSchema = fields.find((v) => v.name === fieldNames?.title)?.uiSchema; const enumUiSchema = fields.find((v) => v.name === fieldNames?.colorFieldName); return useMemo(() => { @@ -164,7 +167,10 @@ const useEvents = ( }); if (res) return out; - const title = getLabelFormatValue(labelUiSchema, get(item, fieldNames.title), true); + const targetTitleCollectionField = fields.find((v) => v.name === fieldNames.title); + const targetTitle = plugin.getTitleFieldInterface(targetTitleCollectionField.interface); + const title = getLabelFormatValue(labelUiSchema, get(item, fieldNames.title), true, targetTitle?.TitleRenderer); + const event: Event = { id: get(item, fieldNames.id || 'id'), colorFieldValue: item[fieldNames.colorFieldName], @@ -275,7 +281,7 @@ export const Calendar: any = withDynamicSchemaProps( }, [reactBigCalendar]); // 新版 UISchema(1.0 之后)中已经废弃了 useProps,这里之所以继续保留是为了兼容旧版的 UISchema - const { dataSource, fieldNames, showLunar, defaultView } = useProps(props); + const { dataSource, fieldNames, showLunar, defaultView, getFontColor, getBackgroundColor } = useProps(props); const height = useCalenderHeight(); const [date, setDate] = useState(new Date()); const [view, setView] = useState(props.defaultView || 'month'); @@ -285,7 +291,6 @@ export const Calendar: any = withDynamicSchemaProps( const parentRecordData = useCollectionParentRecordData(); const fieldSchema = useFieldSchema(); const field = useField(); - const { token } = useToken(); //nint deal with slot select to show create popup const { parseAction } = useACLRoleContext(); const collection = useCollection(); @@ -296,6 +301,8 @@ export const Calendar: any = withDynamicSchemaProps( const ctx = useActionContext(); const [visibleAddNewer, setVisibleAddNewer] = useState(false); const [currentSelectDate, setCurrentSelectDate] = useState(undefined); + const colorCollectionField = collection.getField(fieldNames.colorFieldName); + useEffect(() => { setView(props.defaultView); }, [props.defaultView]); @@ -339,10 +346,17 @@ export const Calendar: any = withDynamicSchemaProps( const eventPropGetter = (event: Event) => { if (event.colorFieldValue) { - const fontColor = token[`${getColorString(event.colorFieldValue, enumList)}7`]; - const backgroundColor = token[`${getColorString(event.colorFieldValue, enumList)}1`]; + const fontColor = getFontColor?.(event.colorFieldValue); + const backgroundColor = getBackgroundColor?.(event.colorFieldValue); + const style = {}; + if (fontColor) { + style['color'] = fontColor; + } + if (backgroundColor) { + style['backgroundColor'] = backgroundColor; + } return { - style: { color: fontColor, backgroundColor, border: 'none' }, + style, }; } }; @@ -435,7 +449,7 @@ export const Calendar: any = withDynamicSchemaProps( return; } record.__event = { - ...event, + ...omit(event, 'title'), start: formatDate(dayjs(event.start)), end: formatDate(dayjs(event.end)), }; diff --git a/packages/plugins/@nocobase/plugin-calendar/src/client/calendar/Calender.Settings.tsx b/packages/plugins/@nocobase/plugin-calendar/src/client/calendar/Calender.Settings.tsx index 1abf582986..2f08b3c11a 100644 --- a/packages/plugins/@nocobase/plugin-calendar/src/client/calendar/Calender.Settings.tsx +++ b/packages/plugins/@nocobase/plugin-calendar/src/client/calendar/Calender.Settings.tsx @@ -24,6 +24,7 @@ import { useDesignable, useFormBlockContext, usePopupSettings, + useApp, } from '@nocobase/client'; import React, { useMemo } from 'react'; import { useTranslation } from '../../locale'; @@ -73,14 +74,17 @@ export const calendarBlockSettings = new SchemaSettings({ const fieldNames = fieldSchema?.['x-decorator-props']?.['fieldNames'] || {}; const { service } = useCalendarBlockContext(); const { getCollectionFieldsOptions } = useCollectionManager_deprecated(); - const { name, title } = useCollection(); + const { name } = useCollection(); + const app = useApp(); + const plugin = app.pm.get('calendar') as any; + const { titleFieldInterfaces } = plugin; const field = useField(); const { dn } = useDesignable(); return { title: t('Title field'), value: fieldNames.title, - options: getCollectionFieldsOptions(name, 'string'), + options: getCollectionFieldsOptions(name, null, Object.keys(titleFieldInterfaces)), onChange: (title) => { const fieldNames = field.decoratorProps.fieldNames || {}; fieldNames['title'] = title; @@ -112,13 +116,14 @@ export const calendarBlockSettings = new SchemaSettings({ const { name } = useCollection(); const field = useField(); const { dn } = useDesignable(); - const fliedList = getCollectionFieldsOptions(name, 'string'); - const filteredItems = [ - { label: t('Not selected'), value: '' }, - ...fliedList.filter((item) => item.interface === 'radioGroup' || item.interface === 'select'), - ]; + const app = useApp(); + const plugin = app.pm.get('calendar') as any; + const { colorFieldInterfaces } = plugin; + const fliedList = getCollectionFieldsOptions(name, null, Object.keys(colorFieldInterfaces)); + const filteredItems = [{ label: t('Not selected'), value: '' }, ...fliedList]; + return { - title: t('Background color field'), + title: t('Color field'), value: fieldNames.colorFieldName || '', options: filteredItems, onChange: (colorFieldName: string) => { @@ -230,10 +235,13 @@ export const calendarBlockSettings = new SchemaSettings({ const { dn } = useDesignable(); const { service } = useCalendarBlockContext(); const { name } = useCollection(); + const app = useApp(); + const plugin = app.pm.get('calendar') as any; + const { dateTimeFields } = plugin; return { title: t('Start date field'), value: fieldNames.start, - options: getCollectionFieldsOptions(name, ['date', 'datetime', 'dateOnly', 'datetimeNoTz', 'unixTimestamp'], { + options: getCollectionFieldsOptions(name, null, dateTimeFields, { association: ['o2o', 'obo', 'oho', 'm2o'], }), onChange: (start) => { @@ -265,10 +273,13 @@ export const calendarBlockSettings = new SchemaSettings({ const { dn } = useDesignable(); const { name } = useCollection(); const fieldNames = fieldSchema?.['x-decorator-props']?.['fieldNames'] || {}; + const app = useApp(); + const plugin = app.pm.get('calendar') as any; + const { dateTimeFields } = plugin; return { title: t('End date field'), value: fieldNames.end, - options: getCollectionFieldsOptions(name, ['date', 'datetime', 'dateOnly', 'datetimeNoTz', 'unixTimestamp'], { + options: getCollectionFieldsOptions(name, null, dateTimeFields, { association: ['o2o', 'obo', 'oho', 'm2o'], }), onChange: (end) => { diff --git a/packages/plugins/@nocobase/plugin-calendar/src/client/index.tsx b/packages/plugins/@nocobase/plugin-calendar/src/client/index.tsx index 2d53d47ace..f6ed96abb9 100644 --- a/packages/plugins/@nocobase/plugin-calendar/src/client/index.tsx +++ b/packages/plugins/@nocobase/plugin-calendar/src/client/index.tsx @@ -6,8 +6,8 @@ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. * For more information, please refer to: https://www.nocobase.com/agreement. */ - -import { Plugin } from '@nocobase/client'; +import React from 'react'; +import { Plugin, useToken } from '@nocobase/client'; import { generateNTemplate } from '../locale'; import { CalendarV2 } from './calendar'; import { calendarBlockSettings } from './calendar/Calender.Settings'; @@ -27,7 +27,79 @@ import { useCreateCalendarBlock, } from './schema-initializer/items'; +const TitleRenderer = ({ value }) => { + return {value || 'N/A'}; +}; +interface ColorFunctions { + loading: boolean; + getFontColor: (value: any) => string; // 返回字体颜色 + getBackgroundColor: (value: any) => string; // 返回背景颜色 +} + +const useGetColor = (field) => { + const { token } = useToken(); + return { + loading: false, + getFontColor(value) { + const option = field.uiSchema.enum.find((item) => item.value === value); + if (option) { + return token[`${option.color}7`]; + } + return null; + }, + getBackgroundColor(value) { + const option = field.uiSchema.enum.find((item) => item.value === value); + if (option) { + return token[`${option.color}1`]; + } + return null; + }, + }; +}; + +type TitleRendererProps = { value: any }; + export class PluginCalendarClient extends Plugin { + titleFieldInterfaces: { [T: string]: { TitleRenderer: React.FC } } = { + input: { TitleRenderer }, + select: { TitleRenderer }, + phone: { TitleRenderer }, + email: { TitleRenderer }, + radioGroup: { TitleRenderer }, + }; + colorFieldInterfaces: { + [T: string]: { useGetColor: (field: any) => ColorFunctions }; + } = { + select: { useGetColor }, + radioGroup: { useGetColor }, + }; + + dateTimeFieldInterfaces = ['date', 'datetime', 'dateOnly', 'datetimeNoTz', 'unixTimestamp', 'createdAt', 'updatedAt']; + + registerTitleFieldInterface(key: string, options: { TitleRenderer: React.FC }) { + this.titleFieldInterfaces[key] = options; + } + getTitleFieldInterface(key: string) { + if (key) { + return this.titleFieldInterfaces[key]; + } else { + return this.titleFieldInterfaces; + } + } + registerDateTimeFieldInterface(data: string | string[]) { + if (Array.isArray(data)) { + const result = this.dateTimeFieldInterfaces.concat(data); + this.dateTimeFieldInterfaces = result; + } else { + this.dateTimeFieldInterfaces.push(data); + } + } + registerColorFieldInterface(type, option: { useGetColor: (field: any) => ColorFunctions }) { + this.colorFieldInterfaces[type] = option; + } + getColorFieldInterface(type: string) { + return this.colorFieldInterfaces[type]; + } async load() { this.app.dataSourceManager.addCollectionTemplates([CalendarCollectionTemplate]); this.app.schemaInitializerManager.addItem('page:addBlock', 'dataBlocks.calendar', { diff --git a/packages/plugins/@nocobase/plugin-calendar/src/client/schema-initializer/CalendarBlockProvider.tsx b/packages/plugins/@nocobase/plugin-calendar/src/client/schema-initializer/CalendarBlockProvider.tsx index 96eda1e565..d185dc152c 100644 --- a/packages/plugins/@nocobase/plugin-calendar/src/client/schema-initializer/CalendarBlockProvider.tsx +++ b/packages/plugins/@nocobase/plugin-calendar/src/client/schema-initializer/CalendarBlockProvider.tsx @@ -9,8 +9,8 @@ import { ArrayField } from '@formily/core'; import { useField, useFieldSchema } from '@formily/react'; -import { BlockProvider, useBlockRequestContext, withDynamicSchemaProps } from '@nocobase/client'; -import React, { createContext, useContext, useEffect } from 'react'; +import { BlockProvider, useBlockRequestContext, withDynamicSchemaProps, useApp, useCollection } from '@nocobase/client'; +import React, { createContext, useContext, useEffect, useState, useMemo, useRef } from 'react'; import { useCalendarBlockParams } from '../hooks/useCalendarBlockParams'; export const CalendarBlockContext = createContext({}); @@ -57,11 +57,12 @@ export const CalendarBlockProvider = withDynamicSchemaProps( if (parseVariableLoading) { return null; } - return ( - - - +
+ + + +
); }, { displayName: 'CalendarBlockProvider' }, @@ -71,18 +72,40 @@ export const useCalendarBlockContext = () => { return useContext(CalendarBlockContext); }; +const useDefaultGetColor = () => { + return { + getFontColor(value) { + return null; + }, + getBackgroundColor(value) { + return null; + }, + }; +}; + export const useCalendarBlockProps = () => { const ctx = useCalendarBlockContext(); const field = useField(); + const app = useApp(); + const plugin = app.pm.get('calendar') as any; + const collection = useCollection(); + const colorCollectionField = collection.getField(ctx.fieldNames.colorFieldName); + const pluginColorField = plugin.getColorFieldInterface(colorCollectionField?.interface) || {}; + const useGetColor = pluginColorField.useGetColor || useDefaultGetColor; + const { getFontColor, getBackgroundColor } = useGetColor(colorCollectionField) || {}; + useEffect(() => { if (!ctx?.service?.loading) { field.componentProps.dataSource = ctx?.service?.data?.data; } }, [ctx?.service?.loading]); + return { fieldNames: ctx.fieldNames, showLunar: ctx.showLunar, defaultView: ctx.defaultView, fixedBlock: ctx.fixedBlock, + getFontColor, + getBackgroundColor, }; }; diff --git a/packages/plugins/@nocobase/plugin-calendar/src/client/schema-initializer/items/CalendarBlockInitializer.tsx b/packages/plugins/@nocobase/plugin-calendar/src/client/schema-initializer/items/CalendarBlockInitializer.tsx index 21ebffba5f..16f6538e61 100644 --- a/packages/plugins/@nocobase/plugin-calendar/src/client/schema-initializer/items/CalendarBlockInitializer.tsx +++ b/packages/plugins/@nocobase/plugin-calendar/src/client/schema-initializer/items/CalendarBlockInitializer.tsx @@ -21,6 +21,8 @@ import { useGlobalTheme, useSchemaInitializer, useSchemaInitializerItem, + useApp, + useCompile, } from '@nocobase/client'; import React, { useContext } from 'react'; import { useTranslation } from '../../../locale'; @@ -67,17 +69,23 @@ export const useCreateCalendarBlock = () => { const { getCollectionField, getCollectionFieldsOptions } = useCollectionManager_deprecated(); const options = useContext(SchemaOptionsContext); const { theme } = useGlobalTheme(); + const app = useApp(); + const plugin = app.pm.get('calendar') as any; + const { titleFieldInterfaces, dateTimeFieldInterfaces } = plugin; const createCalendarBlock = async ({ item }) => { - const stringFieldsOptions = getCollectionFieldsOptions(item.name, 'string', { dataSource: item.dataSource }); - const dateFieldsOptions = getCollectionFieldsOptions( + const titleFieldsOptions = getCollectionFieldsOptions( item.name, - ['date', 'datetime', 'dateOnly', 'datetimeNoTz', 'unixTimestamp'], + null, + Object.keys(titleFieldInterfaces).map((v) => v || v), { - association: ['o2o', 'obo', 'oho', 'm2o'], dataSource: item.dataSource, }, ); + const dateFieldsOptions = getCollectionFieldsOptions(item.name, null, dateTimeFieldInterfaces, { + association: ['o2o', 'obo', 'oho', 'm2o'], + dataSource: item.dataSource, + }); const values = await FormDialog( t('Create calendar block'), @@ -90,7 +98,7 @@ export const useCreateCalendarBlock = () => { properties: { title: { title: t('Title field'), - enum: stringFieldsOptions, + enum: titleFieldsOptions, required: true, 'x-component': 'Select', 'x-decorator': 'FormItem', diff --git a/packages/plugins/@nocobase/plugin-calendar/src/locale/en-US.json b/packages/plugins/@nocobase/plugin-calendar/src/locale/en-US.json index 154a119272..14b1e49d98 100644 --- a/packages/plugins/@nocobase/plugin-calendar/src/locale/en-US.json +++ b/packages/plugins/@nocobase/plugin-calendar/src/locale/en-US.json @@ -1,22 +1,4 @@ -/** - * This file is part of the NocoBase (R) project. - * Copyright (c) 2020-2024 NocoBase Co., Ltd. - * Authors: NocoBase Team. - * - * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. - * For more information, please refer to: https://www.nocobase.com/agreement. - */ - -/** - * This file is part of the NocoBase (R) project. - * Copyright (c) 2020-2024 NocoBase Co., Ltd. - * Authors: NocoBase Team. - * - * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. - * For more information, please refer to: https://www.nocobase.com/agreement. - */ - -export default { +{ "Configure calendar": "Configure calendar", "Title field": "Title field", "Custom title": "Custom title", @@ -60,6 +42,6 @@ export default { "Monthly": "Monthly", "Yearly": "Yearly", "Repeats": "Repeats", - "Background color field": "Background color field", - "Not selected": "Not selected", -}; + "Color field": "Color field", + "Not selected": "Not selected" +} \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-calendar/src/locale/es-ES.json b/packages/plugins/@nocobase/plugin-calendar/src/locale/es-ES.json index 8d0b1a1a48..263bd98537 100644 --- a/packages/plugins/@nocobase/plugin-calendar/src/locale/es-ES.json +++ b/packages/plugins/@nocobase/plugin-calendar/src/locale/es-ES.json @@ -1,22 +1,4 @@ -/** - * This file is part of the NocoBase (R) project. - * Copyright (c) 2020-2024 NocoBase Co., Ltd. - * Authors: NocoBase Team. - * - * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. - * For more information, please refer to: https://www.nocobase.com/agreement. - */ - -/** - * This file is part of the NocoBase (R) project. - * Copyright (c) 2020-2024 NocoBase Co., Ltd. - * Authors: NocoBase Team. - * - * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. - * For more information, please refer to: https://www.nocobase.com/agreement. - */ - -export default { +{ "Configure calendar": "Configurar calendario", "Title field": "Campo de título", "Custom title": "Título personalizado", @@ -60,6 +42,6 @@ export default { "Monthly": "Mensual", "Yearly": "Anual", "Repeats": "se repite", - "Background color field": "Campo de color de fondo", - "Not selected": "No seleccionado", -}; + "Color field": "Campo de color", + "Not selected": "No seleccionado" +} \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-calendar/src/locale/fr-FR.json b/packages/plugins/@nocobase/plugin-calendar/src/locale/fr-FR.json index 3327363f00..136942fe8f 100644 --- a/packages/plugins/@nocobase/plugin-calendar/src/locale/fr-FR.json +++ b/packages/plugins/@nocobase/plugin-calendar/src/locale/fr-FR.json @@ -1,13 +1,4 @@ -/** - * This file is part of the NocoBase (R) project. - * Copyright (c) 2020-2024 NocoBase Co., Ltd. - * Authors: NocoBase Team. - * - * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. - * For more information, please refer to: https://www.nocobase.com/agreement. - */ - -export default { +{ "Configure calendar": "Configurer le calendrier", "Title field": "Champ de titre", "Custom title": "Titre personnalisé", @@ -51,4 +42,4 @@ export default { "Monthly": "Mensuel", "Yearly": "Annuel", "Repeats": "Répétitions" -}; +} \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-calendar/src/locale/ja-JP.json b/packages/plugins/@nocobase/plugin-calendar/src/locale/ja-JP.json index 32123a6b28..aa40624f25 100644 --- a/packages/plugins/@nocobase/plugin-calendar/src/locale/ja-JP.json +++ b/packages/plugins/@nocobase/plugin-calendar/src/locale/ja-JP.json @@ -1,22 +1,4 @@ -/** - * This file is part of the NocoBase (R) project. - * Copyright (c) 2020-2024 NocoBase Co., Ltd. - * Authors: NocoBase Team. - * - * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. - * For more information, please refer to: https://www.nocobase.com/agreement. - */ - -/** - * This file is part of the NocoBase (R) project. - * Copyright (c) 2020-2024 NocoBase Co., Ltd. - * Authors: NocoBase Team. - * - * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. - * For more information, please refer to: https://www.nocobase.com/agreement. - */ - -export default { +{ "Configure calendar": "カレンダーの設定", "Title field": "タイトルフィールド", "Start date field": "開始日フィールド", @@ -61,5 +43,5 @@ export default { "Monthly": "毎月", "Yearly": "毎年", "Repeats": "繰り返し", - "Update record": "レコードを更新する", -}; + "Update record": "レコードを更新する" +} \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-calendar/src/locale/ko-KR.json b/packages/plugins/@nocobase/plugin-calendar/src/locale/ko-KR.json index 0f1206a996..107d8d8a90 100644 --- a/packages/plugins/@nocobase/plugin-calendar/src/locale/ko-KR.json +++ b/packages/plugins/@nocobase/plugin-calendar/src/locale/ko-KR.json @@ -1,13 +1,4 @@ -/** - * This file is part of the NocoBase (R) project. - * Copyright (c) 2020-2024 NocoBase Co., Ltd. - * Authors: NocoBase Team. - * - * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. - * For more information, please refer to: https://www.nocobase.com/agreement. - */ - -export default { +{ "Configure calendar": "캘린더 구성", "Title field": "제목 필드", "Custom title": "사용자 정의 제목", @@ -52,4 +43,4 @@ export default { "Monthly": "매월", "Yearly": "매년", "Repeats": "반복" -}; +} \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-calendar/src/locale/pt-BR.json b/packages/plugins/@nocobase/plugin-calendar/src/locale/pt-BR.json index a7486b5151..96285365f4 100644 --- a/packages/plugins/@nocobase/plugin-calendar/src/locale/pt-BR.json +++ b/packages/plugins/@nocobase/plugin-calendar/src/locale/pt-BR.json @@ -1,13 +1,4 @@ -/** - * This file is part of the NocoBase (R) project. - * Copyright (c) 2020-2024 NocoBase Co., Ltd. - * Authors: NocoBase Team. - * - * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. - * For more information, please refer to: https://www.nocobase.com/agreement. - */ - -export default { +{ "Configure calendar": "Configurar calendário", "Title field": "Campo de título", "Custom title": "Título personalizado", @@ -48,4 +39,4 @@ export default { "Monthly": "Mensal", "Yearly": "Anual", "Repeats": "Repete" -} +} \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-calendar/src/locale/ru-RU.json b/packages/plugins/@nocobase/plugin-calendar/src/locale/ru-RU.json index 627a70d49b..390ed5405c 100644 --- a/packages/plugins/@nocobase/plugin-calendar/src/locale/ru-RU.json +++ b/packages/plugins/@nocobase/plugin-calendar/src/locale/ru-RU.json @@ -1,13 +1,4 @@ -/** - * This file is part of the NocoBase (R) project. - * Copyright (c) 2020-2024 NocoBase Co., Ltd. - * Authors: NocoBase Team. - * - * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. - * For more information, please refer to: https://www.nocobase.com/agreement. - */ - -export default { +{ "Configure calendar": "Настроить календарь", "Title field": "Поле заголовка", "Start date field": "Поле даты начала", @@ -37,4 +28,4 @@ export default { "Edit": "Изменить", "Delete": "Удалить", "Print": "Печать" -} +} \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-calendar/src/locale/tr-TR.json b/packages/plugins/@nocobase/plugin-calendar/src/locale/tr-TR.json index 2fc3d31e86..22a0d83416 100644 --- a/packages/plugins/@nocobase/plugin-calendar/src/locale/tr-TR.json +++ b/packages/plugins/@nocobase/plugin-calendar/src/locale/tr-TR.json @@ -1,13 +1,4 @@ -/** - * This file is part of the NocoBase (R) project. - * Copyright (c) 2020-2024 NocoBase Co., Ltd. - * Authors: NocoBase Team. - * - * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. - * For more information, please refer to: https://www.nocobase.com/agreement. - */ - -export default { +{ "Configure calendar": "Takvimi yapılandır", "Title field": "Başlık alanı", "Start date field": "Başlangıç tarihi alanı", @@ -37,4 +28,4 @@ export default { "Edit": "Düzenle", "Delete": "Sil", "Print": "Yazdır" -} +} \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-calendar/src/locale/uk-UA.json b/packages/plugins/@nocobase/plugin-calendar/src/locale/uk-UA.json index 31652957b6..1280278da2 100644 --- a/packages/plugins/@nocobase/plugin-calendar/src/locale/uk-UA.json +++ b/packages/plugins/@nocobase/plugin-calendar/src/locale/uk-UA.json @@ -1,13 +1,4 @@ -/** - * This file is part of the NocoBase (R) project. - * Copyright (c) 2020-2024 NocoBase Co., Ltd. - * Authors: NocoBase Team. - * - * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. - * For more information, please refer to: https://www.nocobase.com/agreement. - */ - -export default { +{ "Configure calendar": "Налаштувати календар", "Title field": "Поле заголовка", "Custom title": "Власний заголовок", @@ -51,4 +42,4 @@ export default { "Monthly": "Щомісяця", "Yearly": "Щороку", "Repeats": "Повторюється" -}; +} \ No newline at end of file diff --git a/packages/plugins/@nocobase/plugin-calendar/src/locale/zh-CN.json b/packages/plugins/@nocobase/plugin-calendar/src/locale/zh-CN.json index 692bbc1421..d94ebda242 100644 --- a/packages/plugins/@nocobase/plugin-calendar/src/locale/zh-CN.json +++ b/packages/plugins/@nocobase/plugin-calendar/src/locale/zh-CN.json @@ -47,7 +47,7 @@ "Month": "月", "Week": "周", "{{count}} more items": "{{count}} 更多事项", - "Background color field": "背景颜色字段", + "Color field": "颜色字段", "Not selected": "未选择", "Default view": "默认视图", "Event open mode": "事项打开方式" diff --git a/packages/plugins/@nocobase/plugin-data-source-main/src/client/__e2e__/collection-template/general2.test.ts b/packages/plugins/@nocobase/plugin-data-source-main/src/client/__e2e__/collection-template/general2.test.ts index 29612d242f..e1a6b9b234 100644 --- a/packages/plugins/@nocobase/plugin-data-source-main/src/client/__e2e__/collection-template/general2.test.ts +++ b/packages/plugins/@nocobase/plugin-data-source-main/src/client/__e2e__/collection-template/general2.test.ts @@ -27,10 +27,6 @@ test.describe('create collection with preset fields', () => { //默认提交的数据符合预期 expect(postData).toMatchObject({ autoGenId: false, - createdAt: true, - createdBy: true, - updatedAt: true, - updatedBy: true, fields: expect.arrayContaining([ expect.objectContaining({ name: 'id', @@ -79,10 +75,6 @@ test.describe('create collection with preset fields', () => { //提交的数据符合预期 expect(postData).toMatchObject({ autoGenId: false, - createdAt: false, - createdBy: false, - updatedAt: false, - updatedBy: false, fields: expect.arrayContaining([ expect.objectContaining({ name: 'id', @@ -110,10 +102,6 @@ test.describe('create collection with preset fields', () => { //提交的数据符合预期 expect(postData).toMatchObject({ autoGenId: false, - createdAt: true, - createdBy: false, - updatedAt: false, - updatedBy: false, fields: expect.arrayContaining([ expect.objectContaining({ name: 'createdAt', @@ -141,10 +129,6 @@ test.describe('create collection with preset fields', () => { //提交的数据符合预期 expect(postData).toMatchObject({ autoGenId: false, - createdAt: false, - createdBy: true, - updatedAt: false, - updatedBy: false, fields: expect.arrayContaining([ expect.objectContaining({ name: 'createdBy', @@ -172,10 +156,6 @@ test.describe('create collection with preset fields', () => { //提交的数据符合预期 expect(postData).toMatchObject({ autoGenId: false, - createdAt: false, - createdBy: false, - updatedAt: false, - updatedBy: true, fields: expect.arrayContaining([ expect.objectContaining({ name: 'updatedBy', @@ -203,10 +183,6 @@ test.describe('create collection with preset fields', () => { //提交的数据符合预期 expect(postData).toMatchObject({ autoGenId: false, - createdAt: false, - createdBy: false, - updatedAt: true, - updatedBy: false, fields: expect.arrayContaining([ expect.objectContaining({ name: 'updatedAt', @@ -233,10 +209,6 @@ test.describe('create collection with preset fields', () => { //提交的数据符合预期 expect(postData).toMatchObject({ autoGenId: false, - createdAt: false, - createdBy: false, - updatedAt: false, - updatedBy: false, fields: [], }); }); diff --git a/packages/plugins/@nocobase/plugin-data-source-main/src/client/index.ts b/packages/plugins/@nocobase/plugin-data-source-main/src/client/index.ts index f333386c37..e89af1a873 100644 --- a/packages/plugins/@nocobase/plugin-data-source-main/src/client/index.ts +++ b/packages/plugins/@nocobase/plugin-data-source-main/src/client/index.ts @@ -8,9 +8,131 @@ */ import { Plugin } from '@nocobase/client'; +import { orderBy, reject } from 'lodash'; +type PresetFieldConfig = { + order: number; // 定义字段的顺序。 + description: string; // 字段描述 + value: { + name: string; + interface: string; + type: string; + uiSchema: Record; + field?: string; + [T: string]: any; + }; +}; class PluginDataSourceMainClient extends Plugin { - async load() {} + collectionPresetFields: { order: number; value: any }[] = []; + addCollectionPresetField(config: PresetFieldConfig) { + this.collectionPresetFields.push(config); + } + removeCollectionPresetField(fieldName: string) { + this.collectionPresetFields = reject(this.collectionPresetFields, (v) => v.value.name === fieldName); + } + getCollectionPresetFields() { + return orderBy(this.collectionPresetFields, ['order'], ['asc']); + } + async load() { + this.addCollectionPresetField({ + order: 100, + description: '{{t("Primary key, unique identifier, self growth") }}', + value: { + name: 'id', + type: 'bigInt', + autoIncrement: true, + primaryKey: true, + allowNull: false, + uiSchema: { + type: 'number', + title: '{{t("ID")}}', + 'x-component': 'InputNumber', + 'x-read-pretty': true, + }, + interface: 'integer', + }, + }); + this.addCollectionPresetField({ + order: 200, + description: '{{t("Store the creation time of each record")}}', + value: { + name: 'createdAt', + interface: 'createdAt', + type: 'date', + field: 'createdAt', + uiSchema: { + type: 'datetime', + title: '{{t("Created at")}}', + 'x-component': 'DatePicker', + 'x-component-props': {}, + 'x-read-pretty': true, + }, + }, + }); + this.addCollectionPresetField({ + order: 300, + description: '{{t("Store the creation user of each record") }}', + value: { + name: 'createdBy', + interface: 'createdBy', + type: 'belongsTo', + target: 'users', + foreignKey: 'createdById', + uiSchema: { + type: 'object', + title: '{{t("Created by")}}', + 'x-component': 'AssociationField', + 'x-component-props': { + fieldNames: { + value: 'id', + label: 'nickname', + }, + }, + 'x-read-pretty': true, + }, + }, + }); + this.addCollectionPresetField({ + order: 400, + description: '{{t("Store the last update time of each record")}}', + value: { + type: 'date', + field: 'updatedAt', + name: 'updatedAt', + interface: 'updatedAt', + uiSchema: { + type: 'datetime', + title: '{{t("Last updated at")}}', + 'x-component': 'DatePicker', + 'x-component-props': {}, + 'x-read-pretty': true, + }, + }, + }); + this.addCollectionPresetField({ + order: 500, + description: '{{t("Store the last update user of each record")}}', + value: { + type: 'belongsTo', + target: 'users', + foreignKey: 'updatedById', + name: 'updatedBy', + interface: 'updatedBy', + uiSchema: { + type: 'object', + title: '{{t("Last updated by")}}', + 'x-component': 'AssociationField', + 'x-component-props': { + fieldNames: { + value: 'id', + label: 'nickname', + }, + }, + 'x-read-pretty': true, + }, + }, + }); + } } export default PluginDataSourceMainClient; diff --git a/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/http-api/collections.test.ts b/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/http-api/collections.test.ts index 4af695a2e0..e689a9a154 100644 --- a/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/http-api/collections.test.ts +++ b/packages/plugins/@nocobase/plugin-data-source-main/src/server/__tests__/http-api/collections.test.ts @@ -20,6 +20,7 @@ describe('collections repository', () => { await agent.resource('collections').create({ values: { name: 'tags', + createdAt: true, fields: [ { name: 'title', @@ -31,6 +32,7 @@ describe('collections repository', () => { await agent.resource('collections').create({ values: { name: 'foos', + createdAt: true, fields: [ { name: 'title', @@ -49,6 +51,7 @@ describe('collections repository', () => { await agent.resource('collections').create({ values: { name: 'comments', + createdAt: true, fields: [ { name: 'title', @@ -60,6 +63,7 @@ describe('collections repository', () => { await agent.resource('collections').create({ values: { name: 'posts', + createdAt: true, fields: [ { name: 'title', @@ -329,16 +333,17 @@ describe('collections repository', () => { }, }); const postId = response.body.data.id; - const response1 = await agent.resource('posts.tags', postId).list({ - appends: ['foos'], - page: 1, - pageSize: 20, - sort: ['-createdAt', '-id'], - }); + // const response1 = await agent.resource('posts.tags', postId).list({ + // appends: ['foos'], + // page: 1, + // pageSize: 20, + // sort: ['-createdAt', '-id'], + // }); - console.log(JSON.stringify(response1.body.data)); + console.log('postId', response.body); - expect(response1.body.data[0]['id']).toEqual(3); + // expect(res + // ponse1.body.data[0]['id']).toEqual(3); }); it('case 11', async () => { @@ -486,6 +491,7 @@ describe('collections repository', () => { .create({ values: { name: 'test', + createdAt: true, }, }); @@ -518,6 +524,7 @@ describe('collections repository', () => { .create({ values: { name: 'test', + createdAt: true, fields: [ { name: 'testField', diff --git a/packages/plugins/@nocobase/plugin-data-source-main/src/server/server.ts b/packages/plugins/@nocobase/plugin-data-source-main/src/server/server.ts index 9ef05a47eb..e94bb7c180 100644 --- a/packages/plugins/@nocobase/plugin-data-source-main/src/server/server.ts +++ b/packages/plugins/@nocobase/plugin-data-source-main/src/server/server.ts @@ -377,7 +377,24 @@ export class PluginDataSourceMainServer extends Plugin { } await next(); }); - + this.app.resourceManager.use(async function pushUISchemaWhenUpdateCollectionField(ctx, next) { + const { resourceName, actionName } = ctx.action; + if (resourceName === 'collections' && actionName === 'create') { + const { values } = ctx.action.params; + const keys = Object.keys(values); + const presetKeys = ['createdAt', 'createdBy', 'updatedAt', 'updatedBy']; + for (const presetKey of presetKeys) { + if (keys.includes(presetKey)) { + continue; + } + values[presetKey] = !!values.fields?.find((v) => v.name === presetKey); + } + ctx.action.mergeParams({ + values, + }); + } + await next(); + }); this.app.acl.allow('collections', 'list', 'loggedIn'); this.app.acl.allow('collections', 'listMeta', 'loggedIn'); this.app.acl.allow('collectionCategories', 'list', 'loggedIn'); diff --git a/packages/plugins/@nocobase/plugin-field-formula/src/client/components/Formula/Expression.tsx b/packages/plugins/@nocobase/plugin-field-formula/src/client/components/Formula/Expression.tsx index 591c5bcfd0..942c659ff2 100644 --- a/packages/plugins/@nocobase/plugin-field-formula/src/client/components/Formula/Expression.tsx +++ b/packages/plugins/@nocobase/plugin-field-formula/src/client/components/Formula/Expression.tsx @@ -8,14 +8,17 @@ */ import React from 'react'; -import { useCollectionManager_deprecated, useCompile, Variable } from '@nocobase/client'; +import { useCollectionManager_deprecated, useCompile, Variable, useApp } from '@nocobase/client'; export const Expression = (props) => { - const { value = '', supports = [], useCurrentFields, onChange } = props; + const { value = '', useCurrentFields, onChange } = props; + const app = useApp(); + const plugin = app.pm.get('field-formula') as any; + const { expressionFields } = plugin; const compile = useCompile(); const { interfaces } = useCollectionManager_deprecated(); - const fields = (useCurrentFields?.() ?? []).filter((field) => supports.includes(field.interface)); + const fields = (useCurrentFields?.() ?? []).filter((field) => expressionFields.includes(field.interface)); const options = fields.map((field) => ({ label: compile(field.uiSchema.title), diff --git a/packages/plugins/@nocobase/plugin-field-formula/src/client/index.tsx b/packages/plugins/@nocobase/plugin-field-formula/src/client/index.tsx index 1fcb59f752..5819afe8fd 100644 --- a/packages/plugins/@nocobase/plugin-field-formula/src/client/index.tsx +++ b/packages/plugins/@nocobase/plugin-field-formula/src/client/index.tsx @@ -9,11 +9,38 @@ import { Plugin } from '@nocobase/client'; import { Formula } from './components'; -import { renderExpressionDescription } from './scopes'; -import { FormulaFieldInterface } from './interfaces/formula'; import { FormulaComponentFieldSettings } from './FormulaComponentFieldSettings'; +import { FormulaFieldInterface } from './interfaces/formula'; +import { renderExpressionDescription } from './scopes'; export class PluginFieldFormulaClient extends Plugin { + expressionFields = [ + 'checkbox', + 'number', + 'percent', + 'integer', + 'number', + 'percent', + 'input', + 'textarea', + 'email', + 'phone', + 'datetime', + 'createdAt', + 'updatedAt', + 'radioGroup', + 'checkboxGroup', + 'select', + 'multipleSelect', + ]; + registerExpressionFieldInterface(data: string | string[]) { + if (Array.isArray(data)) { + const result = this.expressionFields.concat(data); + this.expressionFields = result; + } else { + this.expressionFields.push(data); + } + } async load() { this.app.addComponents({ Formula, diff --git a/packages/plugins/@nocobase/plugin-field-formula/src/client/interfaces/formula.tsx b/packages/plugins/@nocobase/plugin-field-formula/src/client/interfaces/formula.tsx index a3a74fbe7e..a6a8ea539c 100644 --- a/packages/plugins/@nocobase/plugin-field-formula/src/client/interfaces/formula.tsx +++ b/packages/plugins/@nocobase/plugin-field-formula/src/client/interfaces/formula.tsx @@ -150,31 +150,6 @@ export class FormulaFieldInterface extends CollectionFieldInterface { 'x-component': 'Formula.Expression', 'x-decorator': 'FormItem', 'x-component-props': { - supports: [ - 'checkbox', - - 'number', - 'percent', - 'integer', - 'number', - 'percent', - - 'input', - 'textarea', - 'email', - 'phone', - - 'datetime', - 'createdAt', - 'updatedAt', - - 'radioGroup', - 'checkboxGroup', - 'select', - 'multipleSelect', - - // 'json' - ], useCurrentFields: '{{ useCurrentFields }}', // evaluate(exp: string) { // const { values } = useForm(); diff --git a/packages/plugins/@nocobase/plugin-field-sequence/src/client/sequence.tsx b/packages/plugins/@nocobase/plugin-field-sequence/src/client/sequence.tsx index 501016b9a1..0d6ccc1826 100644 --- a/packages/plugins/@nocobase/plugin-field-sequence/src/client/sequence.tsx +++ b/packages/plugins/@nocobase/plugin-field-sequence/src/client/sequence.tsx @@ -121,9 +121,11 @@ const RuleTypes = { number: t('Number', { ns: NAMESPACE }), lowercase: t('Lowercase letters', { ns: NAMESPACE }), uppercase: t('Uppercase letters', { ns: NAMESPACE }), - symbol: t('Symbols', { ns: NAMESPACE }) + symbol: t('Symbols', { ns: NAMESPACE }), }; - return {value?.map(charset => charsetLabels[charset]).join(', ') || t('Number', { ns: NAMESPACE })}; + return ( + {value?.map((charset) => charsetLabels[charset]).join(', ') || t('Number', { ns: NAMESPACE })} + ); }, }, fieldset: { @@ -154,14 +156,14 @@ const RuleTypes = { { value: 'number', label: `{{t("Number", { ns: "${NAMESPACE}" })}}` }, { value: 'lowercase', label: `{{t("Lowercase letters", { ns: "${NAMESPACE}" })}}` }, { value: 'uppercase', label: `{{t("Uppercase letters", { ns: "${NAMESPACE}" })}}` }, - { value: 'symbol', label: `{{t("Symbols", { ns: "${NAMESPACE}" })}}` } + { value: 'symbol', label: `{{t("Symbols", { ns: "${NAMESPACE}" })}}` }, ], required: true, default: ['number'], 'x-validator': { minItems: 1, - message: `{{t("At least one character set should be selected", { ns: "${NAMESPACE}" })}}` - } + message: `{{t("At least one character set should be selected", { ns: "${NAMESPACE}" })}}`, + }, }, }, defaults: { diff --git a/packages/plugins/@nocobase/plugin-field-sequence/src/server/fields/sequence-field.ts b/packages/plugins/@nocobase/plugin-field-sequence/src/server/fields/sequence-field.ts index 388feef336..1478a11615 100644 --- a/packages/plugins/@nocobase/plugin-field-sequence/src/server/fields/sequence-field.ts +++ b/packages/plugins/@nocobase/plugin-field-sequence/src/server/fields/sequence-field.ts @@ -301,7 +301,7 @@ const CHAR_SETS = { lowercase: 'abcdefghijklmnopqrstuvwxyz', uppercase: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', // 符号只保留常用且安全的符号,有需要的可以自己加比如[]{}|;:,.<>放在链接或者文件名里容易出问题的字符 - symbol: '!@#$%^&*_-+' + symbol: '!@#$%^&*_-+', } as const; interface RandomCharOptions { @@ -317,21 +317,16 @@ sequencePatterns.register('randomChar', { if (!options?.charsets || options.charsets.length === 0) { return 'At least one character set should be selected'; } - if (options.charsets.some(charset => !CHAR_SETS[charset])) { + if (options.charsets.some((charset) => !CHAR_SETS[charset])) { return 'Invalid charset selected'; } return null; }, - - generate(instance: any, options: RandomCharOptions) { - const { - length = 6, - charsets = ['number'] - } = options; - const chars = [...new Set( - charsets.reduce((acc, charset) => acc + CHAR_SETS[charset], '') - )]; + generate(instance: any, options: RandomCharOptions) { + const { length = 6, charsets = ['number'] } = options; + + const chars = [...new Set(charsets.reduce((acc, charset) => acc + CHAR_SETS[charset], ''))]; const getRandomChar = () => { const randomIndex = Math.floor(Math.random() * chars.length); @@ -352,20 +347,27 @@ sequencePatterns.register('randomChar', { }, getMatcher(options: RandomCharOptions) { - const pattern = [...new Set( - (options.charsets || ['number']).reduce((acc, charset) => { - switch (charset) { - case 'number': return acc + '0-9'; - case 'lowercase': return acc + 'a-z'; - case 'uppercase': return acc + 'A-Z'; - case 'symbol': return acc + CHAR_SETS.symbol.replace('-', '').replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&') + '-'; - default: return acc; - } - }, '') - )].join(''); + const pattern = [ + ...new Set( + (options.charsets || ['number']).reduce((acc, charset) => { + switch (charset) { + case 'number': + return acc + '0-9'; + case 'lowercase': + return acc + 'a-z'; + case 'uppercase': + return acc + 'A-Z'; + case 'symbol': + return acc + CHAR_SETS.symbol.replace('-', '').replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&') + '-'; + default: + return acc; + } + }, ''), + ), + ].join(''); return `[${pattern}]{${options.length || 6}}`; - } + }, }); interface PatternConfig { diff --git a/packages/plugins/@nocobase/plugin-kanban/src/client/KanbanBlockInitializer.tsx b/packages/plugins/@nocobase/plugin-kanban/src/client/KanbanBlockInitializer.tsx index 147ec16328..2c55912ee4 100644 --- a/packages/plugins/@nocobase/plugin-kanban/src/client/KanbanBlockInitializer.tsx +++ b/packages/plugins/@nocobase/plugin-kanban/src/client/KanbanBlockInitializer.tsx @@ -22,6 +22,7 @@ import { SchemaComponent, SchemaComponentOptions, useAPIClient, + useApp, useCollectionManager_deprecated, useGlobalTheme, useSchemaInitializer, @@ -153,10 +154,13 @@ export const useCreateKanbanBlock = () => { const options = useContext(SchemaOptionsContext); const { theme } = useGlobalTheme(); const api = useAPIClient(); + const app = useApp(); + const plugin = app.pm.get('kanban') as any; + const groupFieldInterfaces = plugin.getGroupFieldInterface() || []; const createKanbanBlock = async ({ item }) => { const collectionFields = getCollectionFields(item.name, item.dataSource); const fields = collectionFields - ?.filter((field) => ['select', 'radioGroup'].includes(field.interface)) + ?.filter((field) => Object.keys(groupFieldInterfaces).find((v) => v === field.interface)) ?.map((field) => { return { label: field?.uiSchema?.title, @@ -218,13 +222,16 @@ export function useCreateAssociationKanbanBlock() { const { theme } = useGlobalTheme(); const { getCollectionFields } = useCollectionManager_deprecated(); const api = useAPIClient(); + const app = useApp(); const createAssociationKanbanBlock = async ({ item }) => { - console.log(item); const field = item.associationField; const collectionFields = getCollectionFields(item.name, item.dataSource); + const plugin = app.pm.get('kanban') as any; + const groupFieldInterfaces = plugin.getGroupFieldInterface() || []; + const fields = collectionFields - ?.filter((field) => ['select', 'radioGroup'].includes(field.interface)) + ?.filter((field) => Object.keys(groupFieldInterfaces).find((v) => v === field.interface)) ?.map((field) => { return { label: field?.uiSchema?.title, diff --git a/packages/plugins/@nocobase/plugin-kanban/src/client/KanbanBlockProvider.tsx b/packages/plugins/@nocobase/plugin-kanban/src/client/KanbanBlockProvider.tsx index 19580c1ea9..355de3fb95 100644 --- a/packages/plugins/@nocobase/plugin-kanban/src/client/KanbanBlockProvider.tsx +++ b/packages/plugins/@nocobase/plugin-kanban/src/client/KanbanBlockProvider.tsx @@ -12,14 +12,15 @@ import { useField, useFieldSchema } from '@formily/react'; import { BlockProvider, useACLRoleContext, + useAPIClient, useBlockRequestContext, useCollection, useCollection_deprecated, + useApp, } from '@nocobase/client'; import { Spin } from 'antd'; import { isEqual } from 'lodash'; import React, { createContext, useCallback, useContext, useEffect, useState } from 'react'; -import { toColumns } from './Kanban'; export const KanbanBlockContext = createContext({}); KanbanBlockContext.displayName = 'KanbanBlockContext'; @@ -93,20 +94,54 @@ const useDisableCardDrag = () => { return !result; }; +export const toColumns = (groupCollectionField: any, dataSource: Array = [], primaryKey, options) => { + const columns = { + __unknown__: { + id: '__unknown__', + title: 'Unknown', + color: 'default', + cards: [], + }, + }; + options?.forEach((item) => { + columns[item.value] = { + id: item.value, + title: item.label, + color: item.color, + cards: [], + }; + }); + dataSource.forEach((ds) => { + const value = ds[groupCollectionField.name]; + if (value && columns[value]) { + columns[value].cards.push({ ...ds, id: ds[primaryKey] }); + } else { + columns.__unknown__.cards.push(ds); + } + }); + if (columns.__unknown__.cards.length === 0) { + delete columns.__unknown__; + } + return Object.values(columns); +}; + export const useKanbanBlockProps = () => { const field = useField(); const ctx = useKanbanBlockContext(); const [dataSource, setDataSource] = useState([]); const primaryKey = useCollection()?.getPrimaryKey(); - + const app = useApp(); + const plugin = app.pm.get('kanban') as any; + const targetGroupField = plugin.getGroupFieldInterface(ctx.groupField.interface); + const { options } = targetGroupField?.useGetGroupOptions(ctx.groupField) || { options: [] }; useEffect(() => { - const data = toColumns(ctx.groupField, ctx?.service?.data?.data, primaryKey); + const data = toColumns(ctx.groupField, ctx?.service?.data?.data, primaryKey, options); if (isEqual(field.value, data) && dataSource === field.value) { return; } field.value = data; setDataSource(field.value); - }, [ctx?.service?.loading]); + }, [ctx?.service?.loading, options]); const disableCardDrag = useDisableCardDrag(); diff --git a/packages/plugins/@nocobase/plugin-kanban/src/client/board/hook.ts b/packages/plugins/@nocobase/plugin-kanban/src/client/board/hook.ts index 3fe3f6645e..b8152e58ca 100644 --- a/packages/plugins/@nocobase/plugin-kanban/src/client/board/hook.ts +++ b/packages/plugins/@nocobase/plugin-kanban/src/client/board/hook.ts @@ -28,6 +28,6 @@ export const useKanbanBlockHeight = () => { const blockTitleHeaderHeight = title ? token.fontSizeLG * token.lineHeightLG + token.padding * 2 - 1 : 0; - const footerheight = token.controlPaddingHorizontal + token.margin + token.paddingLG - token.marginXS; - return height - actionBarHeight - kanbanHeaderHeight - footerheight - blockTitleHeaderHeight; + const footerHeight = token.controlPaddingHorizontal + token.margin + token.paddingLG - token.marginXS; + return height - actionBarHeight - kanbanHeaderHeight - footerHeight - blockTitleHeaderHeight; }; diff --git a/packages/plugins/@nocobase/plugin-kanban/src/client/index.tsx b/packages/plugins/@nocobase/plugin-kanban/src/client/index.tsx index 295724264d..3c5d43d517 100644 --- a/packages/plugins/@nocobase/plugin-kanban/src/client/index.tsx +++ b/packages/plugins/@nocobase/plugin-kanban/src/client/index.tsx @@ -43,7 +43,45 @@ const KanbanPluginProvider = React.memo((props) => { }); KanbanPluginProvider.displayName = 'KanbanPluginProvider'; +type GroupOption = { + value: string | number; + label: string; + color: string; +}; + +type CollectionField = { + name: string; + type: string; + interface: string; + [key: string]: any; // 扩展字段 +}; + +type Option = { color?: string; label: string; value: string }; +type GroupOptions = { options: Option[]; loading?: boolean }; +type GetGroupOptions = (collectionField: string) => GroupOptions; + +type UseGetGroupOptions = (collectionField: CollectionField) => { options: GroupOption[] }; + +const useDefaultGroupFieldsOptions = (collectionField) => { + return { options: collectionField.uiSchema.enum }; +}; class PluginKanbanClient extends Plugin { + groupFields: { [T: string]: { useGetGroupOptions: GetGroupOptions } } = { + select: { useGetGroupOptions: useDefaultGroupFieldsOptions }, + radioGroup: { useGetGroupOptions: useDefaultGroupFieldsOptions }, + }; + + registerGroupFieldInterface(interfaceName: string, options: { useGetGroupOptions: GetGroupOptions }) { + this.groupFields[interfaceName] = options; + } + + getGroupFieldInterface(key) { + if (key) { + return this.groupFields[key]; + } + return this.groupFields; + } + async load() { this.app.use(KanbanPluginProvider); this.app.schemaInitializerManager.add(kanbanCardInitializers_deprecated); diff --git a/packages/plugins/@nocobase/plugin-map/src/client/block/MapBlock.Settings.tsx b/packages/plugins/@nocobase/plugin-map/src/client/block/MapBlock.Settings.tsx index e6cce51f3a..1500e2514f 100644 --- a/packages/plugins/@nocobase/plugin-map/src/client/block/MapBlock.Settings.tsx +++ b/packages/plugins/@nocobase/plugin-map/src/client/block/MapBlock.Settings.tsx @@ -101,7 +101,7 @@ export const mapBlockSettings = new SchemaSettings({ const { dn } = useDesignable(); const { service } = useMapBlockContext(); const { name } = useCollection(); - const mapFieldOptions = getCollectionFieldsOptions(name, ['point', 'lineString', 'polygon'], { + const mapFieldOptions = getCollectionFieldsOptions(name, ['point', 'lineString', 'polygon'], null, { association: ['o2o', 'obo', 'oho', 'o2m', 'm2o', 'm2m'], }); return { @@ -164,7 +164,7 @@ export const mapBlockSettings = new SchemaSettings({ const { getCollectionFieldsOptions } = useCollectionManager_deprecated(); const { name } = useCollection(); const fieldNames = fieldSchema?.['x-decorator-props']?.['fieldNames'] || {}; - const mapFieldOptions = getCollectionFieldsOptions(name, ['point', 'lineString', 'polygon'], { + const mapFieldOptions = getCollectionFieldsOptions(name, ['point', 'lineString', 'polygon'], null, { association: ['o2o', 'obo', 'oho', 'o2m', 'm2o', 'm2m'], }); const isPointField = findNestedOption(fieldNames.field, mapFieldOptions)?.type === 'point'; diff --git a/packages/plugins/@nocobase/plugin-map/src/client/block/MapBlockInitializer.tsx b/packages/plugins/@nocobase/plugin-map/src/client/block/MapBlockInitializer.tsx index 793c7ceb4f..1012413911 100644 --- a/packages/plugins/@nocobase/plugin-map/src/client/block/MapBlockInitializer.tsx +++ b/packages/plugins/@nocobase/plugin-map/src/client/block/MapBlockInitializer.tsx @@ -38,11 +38,11 @@ export const MapBlockInitializer = () => { componentType={`Map`} icon={} onCreateBlockSchema={async ({ item }) => { - const mapFieldOptions = getCollectionFieldsOptions(item.name, ['point', 'lineString', 'polygon'], { + const mapFieldOptions = getCollectionFieldsOptions(item.name, ['point', 'lineString', 'polygon'], null, { association: ['o2o', 'obo', 'oho', 'o2m', 'm2o', 'm2m'], dataSource: item.dataSource, }); - const markerFieldOptions = getCollectionFieldsOptions(item.name, 'string', { + const markerFieldOptions = getCollectionFieldsOptions(item.name, 'string', null, { dataSource: item.dataSource, }); const values = await FormDialog( diff --git a/packages/plugins/@nocobase/plugin-workflow-action-trigger/src/server/ActionTrigger.ts b/packages/plugins/@nocobase/plugin-workflow-action-trigger/src/server/ActionTrigger.ts index f880ac32d7..b77d3b5bea 100644 --- a/packages/plugins/@nocobase/plugin-workflow-action-trigger/src/server/ActionTrigger.ts +++ b/packages/plugins/@nocobase/plugin-workflow-action-trigger/src/server/ActionTrigger.ts @@ -9,7 +9,7 @@ import { get, pick } from 'lodash'; import { BelongsTo, HasOne } from 'sequelize'; -import { Model, modelAssociationByKey } from '@nocobase/database'; +import { Collection, Model, modelAssociationByKey } from '@nocobase/database'; import Application, { DefaultContext } from '@nocobase/server'; import { Context as ActionContext, Next } from '@nocobase/actions'; @@ -27,14 +27,10 @@ export default class extends Trigger { const self = this; async function triggerWorkflowActionMiddleware(context: Context, next: Next) { - const { resourceName, actionName } = context.action; - - if (resourceName === 'workflows' && actionName === 'trigger') { - return self.workflowTriggerAction(context, next); - } - await next(); + const { actionName } = context.action; + if (!['create', 'update'].includes(actionName)) { return; } @@ -45,20 +41,17 @@ export default class extends Trigger { workflow.app.dataSourceManager.use(triggerWorkflowActionMiddleware); } - /** - * @deprecated - */ - async workflowTriggerAction(context: Context, next: Next) { - const { triggerWorkflows } = context.action.params; - - if (!triggerWorkflows) { - return context.throw(400); + getTargetCollection(collection: Collection, association: string) { + if (!association) { + return collection; } - context.status = 202; - await next(); + let targetCollection = collection; + for (const key of association.split('.')) { + targetCollection = collection.db.getCollection(targetCollection.getField(key).target); + } - return this.collectionTriggerAction(context); + return targetCollection; } private async collectionTriggerAction(context: Context) { @@ -76,7 +69,6 @@ export default class extends Trigger { return; } - const fullCollectionName = joinCollectionName(dataSourceHeader, collection.name); const { currentUser, currentRole } = context.state; const { model: UserModel } = this.workflow.db.getCollection('users'); const userInfo = { @@ -92,9 +84,8 @@ export default class extends Trigger { const globalWorkflows = new Map(); const localWorkflows = new Map(); workflows.forEach((item) => { - if (resourceName === 'workflows' && actionName === 'trigger') { - localWorkflows.set(item.key, item); - } else if (item.config.collection === fullCollectionName) { + const targetCollection = this.getTargetCollection(collection, triggersKeysMap.get(item.key)); + if (item.config.collection === joinCollectionName(dataSourceHeader, targetCollection.name)) { if (item.config.global) { if (item.config.actions?.includes(actionName)) { globalWorkflows.set(item.key, item); diff --git a/packages/plugins/@nocobase/plugin-workflow-action-trigger/src/server/__tests__/trigger.test.ts b/packages/plugins/@nocobase/plugin-workflow-action-trigger/src/server/__tests__/trigger.test.ts index 2573e03e6e..e3244e22b4 100644 --- a/packages/plugins/@nocobase/plugin-workflow-action-trigger/src/server/__tests__/trigger.test.ts +++ b/packages/plugins/@nocobase/plugin-workflow-action-trigger/src/server/__tests__/trigger.test.ts @@ -207,6 +207,7 @@ describe('workflow > action-trigger', () => { type: 'action', config: { collection: 'posts', + appends: ['createdBy'], }, }); @@ -300,155 +301,21 @@ describe('workflow > action-trigger', () => { }); }); - /** - * @deprecated - */ - describe('directly trigger', () => { - it('no collection configured should not be triggered', async () => { - const workflow = await WorkflowModel.create({ - enabled: true, - type: 'action', - }); - - const res1 = await userAgents[0].resource('workflows').trigger({ - values: { title: 't1' }, - triggerWorkflows: `${workflow.key}`, - }); - expect(res1.status).toBe(202); - - await sleep(500); - - const e1s = await workflow.getExecutions(); - expect(e1s.length).toBe(0); - }); - - it('trigger on form data', async () => { - const workflow = await WorkflowModel.create({ - enabled: true, - type: 'action', - config: { - collection: 'posts', - appends: ['createdBy'], - }, - }); - - const res1 = await userAgents[0].resource('workflows').trigger({ - values: { title: 't1' }, - triggerWorkflows: `${workflow.key}`, - }); - expect(res1.status).toBe(202); - - await sleep(500); - - const [e1] = await workflow.getExecutions(); - expect(e1.status).toBe(EXECUTION_STATUS.RESOLVED); - expect(e1.context.data).toMatchObject({ title: 't1' }); - expect(e1.context.data.createdBy).toBeUndefined(); - }); - - it('trigger on record data', async () => { - const workflow = await WorkflowModel.create({ - enabled: true, - type: 'action', - config: { - collection: 'posts', - appends: ['createdBy'], - }, - }); - - const post = await PostRepo.create({ - values: { title: 't1', createdBy: users[0].id }, - }); - - const res1 = await userAgents[0].resource('workflows').trigger({ - values: post.toJSON(), - triggerWorkflows: `${workflow.key}`, - }); - expect(res1.status).toBe(202); - - await sleep(500); - - const [e1] = await workflow.getExecutions(); - expect(e1.status).toBe(EXECUTION_STATUS.RESOLVED); - expect(e1.context.data).toMatchObject({ title: 't1' }); - expect(e1.context.data).toHaveProperty('createdBy'); - expect(e1.context.data.createdBy.id).toBe(users[0].id); - }); - - it('multi trigger', async () => { - const w1 = await WorkflowModel.create({ - enabled: true, - type: 'action', - config: { - collection: 'posts', - }, - }); - - const w2 = await WorkflowModel.create({ - enabled: true, - type: 'action', - config: { - collection: 'posts', - }, - }); - - const res1 = await userAgents[0].resource('workflows').trigger({ - values: { title: 't1' }, - triggerWorkflows: `${w1.key},${w2.key}`, - }); - expect(res1.status).toBe(202); - - await sleep(500); - - const [e1] = await w1.getExecutions(); - expect(e1.status).toBe(EXECUTION_STATUS.RESOLVED); - expect(e1.context.data).toMatchObject({ title: 't1' }); - - const [e2] = await w2.getExecutions(); - expect(e2.status).toBe(EXECUTION_STATUS.RESOLVED); - expect(e2.context.data).toMatchObject({ title: 't1' }); - }); - - it('user submitted form', async () => { - const workflow = await WorkflowModel.create({ - enabled: true, - type: 'action', - config: { - collection: 'posts', - appends: ['createdBy'], - }, - }); - - const res1 = await userAgents[0].resource('posts').create({ - values: { title: 't1' }, - triggerWorkflows: `${workflow.key}`, - }); - expect(res1.status).toBe(200); - - await sleep(500); - - const [e1] = await workflow.getExecutions(); - expect(e1.status).toBe(EXECUTION_STATUS.RESOLVED); - expect(e1.context.user).toBeDefined(); - expect(e1.context.user.id).toBe(users[0].id); - }); - }); - describe('context data path', () => { it('level: 1', async () => { const workflow = await WorkflowModel.create({ enabled: true, type: 'action', config: { - collection: 'posts', + collection: 'categories', }, }); - const res1 = await userAgents[0].resource('workflows').trigger({ + const res1 = await userAgents[0].resource('posts').create({ values: { title: 't1', category: { title: 'c1' } }, triggerWorkflows: `${workflow.key}!category`, }); - expect(res1.status).toBe(202); + expect(res1.status).toBe(200); await sleep(500); @@ -462,15 +329,15 @@ describe('workflow > action-trigger', () => { enabled: true, type: 'action', config: { - collection: 'posts', + collection: 'categories', }, }); - const res1 = await userAgents[0].resource('workflows').trigger({ + const res1 = await userAgents[0].resource('comments').create({ values: { content: 'comment1', post: { category: { title: 'c1' } } }, triggerWorkflows: `${workflow.key}!post.category`, }); - expect(res1.status).toBe(202); + expect(res1.status).toBe(200); await sleep(500); @@ -563,11 +430,11 @@ describe('workflow > action-trigger', () => { }, }); - const res1 = await userAgents[0].resource('workflows').trigger({ + const res1 = await userAgents[0].resource('posts').create({ values: { title: 't1' }, triggerWorkflows: `${w1.key}`, }); - expect(res1.status).toBe(202); + expect(res1.status).toBe(200); await sleep(500); @@ -586,11 +453,11 @@ describe('workflow > action-trigger', () => { enabled: true, }); - const res3 = await userAgents[0].resource('workflows').trigger({ + const res3 = await userAgents[0].resource('posts').create({ values: { title: 't2' }, triggerWorkflows: `${w1.key}`, }); - expect(res3.status).toBe(202); + expect(res3.status).toBe(200); await sleep(500); @@ -769,11 +636,11 @@ describe('workflow > action-trigger', () => { }, }); - const res1 = await userAgents[0].resource('workflows').trigger({ + const res1 = await userAgents[0].resource('posts').create({ values: { title: 't1' }, triggerWorkflows: `${workflow.key}`, }); - expect(res1.status).toBe(202); + expect(res1.status).toBe(200); await sleep(500); diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/WorkflowCanvas.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/WorkflowCanvas.tsx index e07ba290e1..508bbf98e8 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/client/WorkflowCanvas.tsx +++ b/packages/plugins/@nocobase/plugin-workflow/src/client/WorkflowCanvas.tsx @@ -157,6 +157,7 @@ function ExecuteActionButton() { scope={{ useCancelAction, useExecuteConfirmAction, + ...trigger.scope, }} schema={{ name: `trigger-modal-${workflow.type}-${workflow.id}`, diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/index.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/index.tsx index af8896ee02..b51e3afab7 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/client/index.tsx +++ b/packages/plugins/@nocobase/plugin-workflow/src/client/index.tsx @@ -7,11 +7,7 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import React from 'react'; -import { useFieldSchema } from '@formily/react'; -import { isValid } from '@formily/shared'; - -import { PagePopups, Plugin, useCompile, WorkflowConfig } from '@nocobase/client'; +import { PagePopups, Plugin, useCompile } from '@nocobase/client'; import { Registry } from '@nocobase/utils/client'; // import { ExecutionPage } from './ExecutionPage'; @@ -35,9 +31,13 @@ import UpdateInstruction from './nodes/update'; import DestroyInstruction from './nodes/destroy'; import { getWorkflowDetailPath, getWorkflowExecutionsPath } from './utils'; import { lang, NAMESPACE } from './locale'; -import { customizeSubmitToWorkflowActionSettings } from './settings/customizeSubmitToWorkflowActionSettings'; import { VariableOption } from './variable'; import { WorkflowTasks, TasksProvider, TaskTypeOptions } from './WorkflowTasks'; +import { BindWorkflowConfig } from './settings/BindWorkflowConfig'; + +const workflowConfigSettings = { + Component: BindWorkflowConfig, +}; type InstructionGroup = { key?: string; @@ -138,15 +138,13 @@ export default class PluginWorkflowClient extends Plugin { this.app.use(TasksProvider); - this.app.schemaSettingsManager.add(customizeSubmitToWorkflowActionSettings); - - this.app.schemaSettingsManager.addItem('actionSettings:delete', 'workflowConfig', { - Component: WorkflowConfig, - useVisible() { - const fieldSchema = useFieldSchema(); - return isValid(fieldSchema?.['x-action-settings']?.triggerWorkflows); - }, - }); + this.app.schemaSettingsManager.addItem('actionSettings:submit', 'workflowConfig', workflowConfigSettings); + this.app.schemaSettingsManager.addItem('actionSettings:createSubmit', 'workflowConfig', workflowConfigSettings); + this.app.schemaSettingsManager.addItem('actionSettings:updateSubmit', 'workflowConfig', workflowConfigSettings); + this.app.schemaSettingsManager.addItem('actionSettings:saveRecord', 'workflowConfig', workflowConfigSettings); + this.app.schemaSettingsManager.addItem('actionSettings:updateRecord', 'workflowConfig', workflowConfigSettings); + this.app.schemaSettingsManager.addItem('actionSettings:delete', 'workflowConfig', workflowConfigSettings); + this.app.schemaSettingsManager.addItem('actionSettings:bulkEditSubmit', 'workflowConfig', workflowConfigSettings); this.registerInstructionGroup('control', { key: 'control', label: `{{t("Control", { ns: "${NAMESPACE}" })}}` }); this.registerInstructionGroup('calculation', { @@ -195,3 +193,4 @@ export * from './hooks'; export { default as useStyles } from './style'; export * from './variable'; export * from './ExecutionContextProvider'; +export * from './settings/BindWorkflowConfig'; diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/settings/BindWorkflowConfig.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/settings/BindWorkflowConfig.tsx new file mode 100644 index 0000000000..c08d163b0f --- /dev/null +++ b/packages/plugins/@nocobase/plugin-workflow/src/client/settings/BindWorkflowConfig.tsx @@ -0,0 +1,309 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import React, { useCallback, useMemo, useState } from 'react'; +import { ArrayTable } from '@formily/antd-v5'; +import { onFieldValueChange } from '@formily/core'; +import { ISchema, useFieldSchema, useForm, useFormEffects } from '@formily/react'; +import { + DataSourceProvider, + joinCollectionName, + RemoteSelect, + SchemaSettingsActionModalItem, + useCollection_deprecated, + useCollectionManager_deprecated, + useCompile, + useDataSourceKey, + useDesignable, + useFormBlockContext, +} from '@nocobase/client'; +import { useTranslation } from 'react-i18next'; +import { usePlugin } from '@nocobase/client'; +import { Alert, Flex, Tag } from 'antd'; + +function WorkflowSelect({ formAction, buttonAction, actionType, ...props }) { + const { t } = useTranslation(); + const index = ArrayTable.useIndex(); + const { setValuesIn } = useForm(); + const baseCollection = useCollection_deprecated(); + const { getCollection } = useCollectionManager_deprecated(); + const dataSourceKey = useDataSourceKey(); + const [workflowCollection, setWorkflowCollection] = useState(joinCollectionName(dataSourceKey, baseCollection.name)); + const compile = useCompile(); + + const workflowPlugin = usePlugin('workflow') as any; + const triggerOptions = workflowPlugin.useTriggersOptions(); + const workflowTypes = useMemo( + () => + triggerOptions + .filter((item) => { + return typeof item.options.isActionTriggerable === 'function' || item.options.isActionTriggerable === true; + }) + .map((item) => item.value), + [triggerOptions], + ); + + useFormEffects(() => { + onFieldValueChange(`group[${index}].context`, (field) => { + let collection: any = baseCollection; + if (field.value) { + const paths = field.value.split('.'); + for (let i = 0; i < paths.length && collection; i++) { + const path = paths[i]; + const associationField = collection.fields.find((f) => f.name === path); + if (associationField) { + collection = getCollection(associationField.target, dataSourceKey); + } + } + } + setWorkflowCollection(joinCollectionName(dataSourceKey, collection.name)); + setValuesIn(`group[${index}].workflowKey`, null); + }); + }); + + const optionFilter = useCallback( + ({ key, type, config }) => { + if (key === props.value) { + return true; + } + const trigger = workflowPlugin.triggers.get(type); + if (trigger.isActionTriggerable === true) { + return true; + } + if (typeof trigger.isActionTriggerable === 'function') { + return trigger.isActionTriggerable(config, { + action: actionType, + formAction, + buttonAction, + /** + * @deprecated + */ + direct: buttonAction === 'customize:triggerWorkflows', + }); + } + return false; + }, + [props.value, workflowPlugin.triggers, formAction, buttonAction, actionType], + ); + + return ( + + { + const typeOption = triggerOptions.find((item) => item.value === data.type); + return typeOption ? ( + + {label} + {compile(typeOption.label)} + + ) : ( + label + ); + }} + {...props} + /> + + ); +} + +export function BindWorkflowConfig() { + const { dn } = useDesignable(); + const { t } = useTranslation(); + const fieldSchema = useFieldSchema(); + const collection = useCollection_deprecated(); + // TODO(refactor): should refactor for getting certain action type, better from 'x-action'. + const formBlock = useFormBlockContext(); + /** + * @deprecated + */ + const actionType = formBlock?.type || fieldSchema['x-action']; + const formAction = formBlock?.type; + const buttonAction = fieldSchema['x-action']; + + const description = { + submit: t('Support pre-action event (local mode), post-action event (local mode), and approval event here.', { + ns: 'workflow', + }), + 'customize:save': t( + 'Support pre-action event (local mode), post-action event (local mode), and approval event here.', + { + ns: 'workflow', + }, + ), + 'customize:update': t( + 'Support pre-action event (local mode), post-action event (local mode), and approval event here.', + { ns: 'workflow' }, + ), + 'customize:triggerWorkflows': t( + 'Workflow will be triggered directly once the button clicked, without data saving. Only supports to be bound with "Custom action event".', + { ns: '@nocobase/plugin-workflow-custom-action-trigger' }, + ), + 'customize:triggerWorkflows_deprecated': t( + '"Submit to workflow" to "Post-action event" is deprecated, please use "Custom action event" instead.', + { ns: 'workflow' }, + ), + destroy: t('Workflow will be triggered before deleting succeeded (only supports pre-action event in local mode).', { + ns: 'workflow', + }), + }[fieldSchema?.['x-action']]; + + return ( + { + fieldSchema['x-action-settings']['triggerWorkflows'] = group; + dn.emit('patch', { + schema: { + ['x-uid']: fieldSchema['x-uid'], + 'x-action-settings': fieldSchema['x-action-settings'], + }, + }); + }} + /> + ); +} diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/settings/customizeSubmitToWorkflowActionSettings.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/settings/customizeSubmitToWorkflowActionSettings.tsx deleted file mode 100644 index 4729e2bdef..0000000000 --- a/packages/plugins/@nocobase/plugin-workflow/src/client/settings/customizeSubmitToWorkflowActionSettings.tsx +++ /dev/null @@ -1,65 +0,0 @@ -/** - * This file is part of the NocoBase (R) project. - * Copyright (c) 2020-2024 NocoBase Co., Ltd. - * Authors: NocoBase Team. - * - * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. - * For more information, please refer to: https://www.nocobase.com/agreement. - */ - -import { useFieldSchema } from '@formily/react'; -import { isValid } from '@formily/shared'; -import { - AfterSuccess, - AssignedFieldValues, - ButtonEditor, - RemoveButton, - SchemaSettings, - SecondConFirm, - SkipValidation, - WorkflowConfig, - useSchemaToolbar, -} from '@nocobase/client'; - -export const customizeSubmitToWorkflowActionSettings = new SchemaSettings({ - name: 'actionSettings:submitToWorkflow', - items: [ - { - name: 'editButton', - Component: ButtonEditor, - useComponentProps() { - const { buttonEditorProps } = useSchemaToolbar(); - return buttonEditorProps; - }, - }, - { - name: 'secondConfirmation', - Component: SecondConFirm, - }, - { - name: 'assignFieldValues', - Component: AssignedFieldValues, - }, - { - name: 'skipRequiredValidation', - Component: SkipValidation, - }, - { - name: 'afterSuccessfulSubmission', - Component: AfterSuccess, - }, - { - name: 'bindWorkflow', - Component: WorkflowConfig, - }, - { - name: 'delete', - sort: 100, - Component: RemoveButton as any, - useComponentProps() { - const { removeButtonProps } = useSchemaToolbar(); - return removeButtonProps; - }, - }, - ], -}); diff --git a/packages/plugins/@nocobase/plugin-workflow/src/server/actions/workflows.ts b/packages/plugins/@nocobase/plugin-workflow/src/server/actions/workflows.ts index 37d212e6c2..ffc6331947 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/server/actions/workflows.ts +++ b/packages/plugins/@nocobase/plugin-workflow/src/server/actions/workflows.ts @@ -102,9 +102,9 @@ export async function sync(context: Context, next) { * @deprecated * Keep for action trigger compatibility */ -export async function trigger(context: Context, next) { - return next(); -} +// export async function trigger(context: Context, next) { +// return next(); +// } export async function execute(context: Context, next) { const plugin = context.app.pm.get(Plugin) as Plugin;