From c8bd2c73173f0852c227523192ef9d25cde1f2ab Mon Sep 17 00:00:00 2001 From: SemmyWong <67748948+semmywong@users.noreply.github.com> Date: Wed, 13 Jul 2022 15:05:46 +0800 Subject: [PATCH] feat: field assignment for custom actions supports string variables (#597) * fix: temporary solution to APP crash * feat: support dynamic assigned field value * feat: support dynamic assigned field value * fix: useFields filter * fix: dynamic assigned value * fix: dynamic assigned value * fix: fix china region export * fix: fix china region export * fix: change assign value data * fix: custom request use parse instead of SchemaCompile * fix: allow user attribute to be selected * fix: allow DATE field to be select currentUser or CurrentRecord * fix: allow DATE field to be select currentUser or CurrentRecord * fix: change style * feat: package dependencies Co-authored-by: chenos --- packages/core/client/package.json | 1 + .../client/src/block-provider/hooks/index.ts | 45 ++- packages/core/client/src/locale/zh_CN.ts | 1 + .../assigned-field/AssignedField.tsx | 272 ++++++++++++++---- .../export/src/client/useExportAction.ts | 11 +- .../plugins/export/src/client/useFields.ts | 25 +- .../src/server/__tests__/utils/utils.test.ts | 19 +- .../export/src/server/actions/export-xlsx.ts | 2 +- .../export/src/server/renders/renders.ts | 2 +- .../src/server/utils/columns2Appends.ts | 14 +- 10 files changed, 284 insertions(+), 108 deletions(-) diff --git a/packages/core/client/package.json b/packages/core/client/package.json index 434278a9d..6d5d6ed31 100644 --- a/packages/core/client/package.json +++ b/packages/core/client/package.json @@ -27,6 +27,7 @@ "classnames": "^2.3.1", "file-saver": "^2.0.5", "i18next": "^21.6.0", + "json-templates": "^4.2.0", "marked": "^4.0.12", "mathjs": "^10.6.0", "react-beautiful-dnd": "^13.1.0", diff --git a/packages/core/client/src/block-provider/hooks/index.ts b/packages/core/client/src/block-provider/hooks/index.ts index 89dd8437d..41d16bc7b 100644 --- a/packages/core/client/src/block-provider/hooks/index.ts +++ b/packages/core/client/src/block-provider/hooks/index.ts @@ -1,6 +1,6 @@ -import { Schema as SchemaCompiler } from '@formily/json-schema'; import { useField, useFieldSchema, useForm } from '@formily/react'; import { message, Modal } from 'antd'; +import parse from 'json-templates'; import get from 'lodash/get'; import { useTranslation } from 'react-i18next'; import { useHistory } from 'react-router-dom'; @@ -120,11 +120,19 @@ export const useCreateActionProps = () => { const { fields, getField } = useCollection(); const compile = useCompile(); const filterByTk = useFilterByTk(); + const currentRecord = useRecord(); + const currentUserContext = useCurrentUserContext(); + const currentUser = currentUserContext?.data?.data; return { async onClick() { const fieldNames = fields.map((field) => field.name); - const { assignedValues, onSuccess, overwriteValues, skipValidator } = actionSchema?.['x-action-settings'] ?? {}; - + const { + assignedValues: originalAssignedValues = {}, + onSuccess, + overwriteValues, + skipValidator, + } = actionSchema?.['x-action-settings'] ?? {}; + const assignedValues = parse(originalAssignedValues)({ currentTime: new Date(), currentRecord, currentUser }); if (!skipValidator) { await form.submit(); } @@ -174,14 +182,20 @@ export const useCustomizeUpdateActionProps = () => { const filterByTk = useFilterByTk(); const actionSchema = useFieldSchema(); const currentRecord = useRecord(); - const ctx = useCurrentUserContext(); + const currentUserContext = useCurrentUserContext(); + const currentUser = currentUserContext?.data?.data; const history = useHistory(); const compile = useCompile(); const form = useForm(); return { async onClick() { - const { assignedValues, onSuccess, skipValidator } = actionSchema?.['x-action-settings'] ?? {}; + const { + assignedValues: originalAssignedValues = {}, + onSuccess, + skipValidator, + } = actionSchema?.['x-action-settings'] ?? {}; + const assignedValues = parse(originalAssignedValues)({ currentTime: new Date(), currentRecord, currentUser }); if (skipValidator === false) { await form.submit(); } @@ -254,9 +268,9 @@ export const useCustomizeRequestActionProps = () => { const requestBody = { url: renderTemplate(requestSettings['url'], { currentRecord, currentUser }), method: requestSettings['method'], - headers: SchemaCompiler.compile(headers, { currentRecord, currentUser }), - params: SchemaCompiler.compile(params, { currentRecord, currentUser }), - data: SchemaCompiler.compile(data, { currentRecord, currentUser }), + headers: parse(headers)({ currentRecord, currentUser }), + params: parse(params)({ currentRecord, currentUser }), + data: parse(data)({ currentRecord, currentUser }), }; actionField.data = field.data || {}; actionField.data.loading = true; @@ -305,15 +319,22 @@ export const useUpdateActionProps = () => { const { setVisible } = useActionContext(); const actionSchema = useFieldSchema(); const history = useHistory(); - const record = useRecord(); const { fields, getField } = useCollection(); const compile = useCompile(); const actionField = useField(); const { updateAssociationValues } = useFormBlockContext(); + const currentRecord = useRecord(); + const currentUserContext = useCurrentUserContext(); + const currentUser = currentUserContext?.data?.data; return { async onClick() { - const { assignedValues, onSuccess, overwriteValues, skipValidator } = actionSchema?.['x-action-settings'] ?? {}; - + const { + assignedValues: originalAssignedValues = {}, + onSuccess, + overwriteValues, + skipValidator, + } = actionSchema?.['x-action-settings'] ?? {}; + const assignedValues = parse(originalAssignedValues)({ currentTime: new Date(), currentRecord, currentUser }); if (!skipValidator) { await form.submit(); } @@ -329,7 +350,7 @@ export const useUpdateActionProps = () => { ...overwriteValues, ...assignedValues, }, - updateAssociationValues + updateAssociationValues, }); actionField.data.loading = false; if (!(resource instanceof TableFieldResource)) { diff --git a/packages/core/client/src/locale/zh_CN.ts b/packages/core/client/src/locale/zh_CN.ts index cad175573..627bf8a86 100644 --- a/packages/core/client/src/locale/zh_CN.ts +++ b/packages/core/client/src/locale/zh_CN.ts @@ -610,6 +610,7 @@ export default { 'Dynamic value': '动态值', 'Current user': '当前用户', 'Current record': '当前记录', + 'Current time': '当前时间', 'Popup close method': '弹窗关闭方式', 'Automatic close': '自动关闭', 'Manually close': '手动关闭', diff --git a/packages/core/client/src/schema-initializer/components/assigned-field/AssignedField.tsx b/packages/core/client/src/schema-initializer/components/assigned-field/AssignedField.tsx index 07520ef27..4ae63ebd7 100644 --- a/packages/core/client/src/schema-initializer/components/assigned-field/AssignedField.tsx +++ b/packages/core/client/src/schema-initializer/components/assigned-field/AssignedField.tsx @@ -1,74 +1,226 @@ import { Field } from '@formily/core'; -import { useField, useFieldSchema } from '@formily/react'; -// import { Select, Space } from 'antd'; -import React, { useState } from 'react'; +import { connect, useField, useFieldSchema } from '@formily/react'; +import { Cascader, Select, Space } from 'antd'; +import React, { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { CollectionField } from '../../../collection-manager'; -import { useCompile } from '../../../schema-component'; +import { useFormBlockContext } from '../../../block-provider'; +import { + CollectionFieldProvider, + useCollection, + useCollectionField, + useCollectionFilterOptions, +} from '../../../collection-manager'; +import { useCompile, useComponent } from '../../../schema-component'; + +const DYNAMIC_RECORD_REG = /\{\{\s*currentRecord\.(.*)\s*\}\}/; +const DYNAMIC_USER_REG = /\{\{\s*currentUser\.(.*)\s*\}\}/; +const DYNAMIC_TIME_REG = /\{\{\s*currentTime\s*\}\}/; + +const InternalField: React.FC = (props) => { + const field = useField(); + + const fieldSchema = useFieldSchema(); + const { name, interface: interfaceType, uiSchema } = useCollectionField(); + const component = useComponent(uiSchema?.['x-component']); + const compile = useCompile(); + const setFieldProps = (key, value) => { + field[key] = typeof field[key] === 'undefined' ? value : field[key]; + }; + const setRequired = () => { + if (typeof fieldSchema['required'] === 'undefined') { + field.required = !!uiSchema['required']; + } + }; + const ctx = useFormBlockContext(); + + useEffect(() => { + if (ctx?.field) { + ctx.field.added = ctx.field.added || new Set(); + ctx.field.added.add(fieldSchema.name); + } + }); + + useEffect(() => { + if (!uiSchema) { + return; + } + setFieldProps('content', uiSchema['x-content']); + setFieldProps('title', uiSchema.title); + setFieldProps('description', uiSchema.description); + setFieldProps('initialValue', uiSchema.default); + if (!field.validator && uiSchema['x-validator']) { + field.validator = uiSchema['x-validator']; + } + if (fieldSchema['x-disabled'] === true) { + field.disabled = true; + } + if (fieldSchema['x-read-pretty'] === true) { + field.readPretty = true; + } + setRequired(); + // @ts-ignore + // field.dataSource = uiSchema.enum; + // const originalProps = compile(uiSchema['x-component-props']) || {}; + // const componentProps = merge(originalProps, field.componentProps || {}); + // field.component = [component, componentProps]; + }, [JSON.stringify(uiSchema)]); + if (!uiSchema) { + return null; + } + return React.createElement(component, props, props.children); +}; + +const CollectionField = connect((props) => { + const fieldSchema = useFieldSchema(); + return ( + + + + ); +}); + +export enum AssignedFieldValueType { + ConstantValue = 'constantValue', + DynamicValue = 'dynamicValue', +} export const AssignedField = (props: any) => { const { t } = useTranslation(); const compile = useCompile(); const field = useField(); const fieldSchema = useFieldSchema(); - // const [type, setType] = useState('constantValue'); - const [value, setValue] = useState(field?.value?.value ?? ''); - // const [options, setOptions] = useState([]); - // const { getField } = useCollection(); - // const collectionField = getField(fieldSchema.name); - // const { uiSchema } = collectionField; - // const currentUser = useFilterOptions('users'); - // const currentRecord = useFilterOptions(collectionField.collectionName); - // useEffect(() => { - // const opt = [ - // { - // name: 'currentUser', - // title: t('Current user'), - // children: [...currentUser], - // }, - // { - // name: 'currentRecord', - // title: t('Current record'), - // children: [...currentRecord], - // }, - // ]; - // setOptions(compile(opt)); - // }, []); + const isDynamicValue = + DYNAMIC_RECORD_REG.test(field.value) || DYNAMIC_USER_REG.test(field.value) || DYNAMIC_TIME_REG.test(field.value); + const initType = isDynamicValue ? AssignedFieldValueType.DynamicValue : AssignedFieldValueType.ConstantValue; + const [type, setType] = useState(initType); + const initFieldType = { + [`${DYNAMIC_TIME_REG.test(field.value)}`]: 'currentTime', + [`${DYNAMIC_USER_REG.test(field.value)}`]: 'currentUser', + [`${DYNAMIC_RECORD_REG.test(field.value)}`]: 'currentRecord', + }; + const [fieldType, setFieldType] = useState(initFieldType['true']); + const initRecordValue = DYNAMIC_RECORD_REG.exec(field.value)?.[1]?.split('.') ?? []; + const [recordValue, setRecordValue] = useState(initRecordValue); + const initUserValue = DYNAMIC_USER_REG.exec(field.value)?.[1]?.split('.') ?? []; + const [userValue, setUserValue] = useState(initUserValue); + const initValue = isDynamicValue ? '' : field.value; + const [value, setValue] = useState(initValue); + const [options, setOptions] = useState([]); + const { getField } = useCollection(); + const collectionField = getField(fieldSchema.name); + const fields = useCollectionFilterOptions(collectionField?.collectionName); + const userFields = useCollectionFilterOptions('users'); + const dateTimeFields = ['createdAt', 'datetime', 'time', 'updatedAt']; + useEffect(() => { + const opt = [ + { + name: 'currentRecord', + title: t('Current record'), + }, + { + name: 'currentUser', + title: t('Current user'), + }, + ]; + if (dateTimeFields.includes(collectionField.interface)) { + opt.unshift({ + name: 'currentTime', + title: t('Current time'), + }); + } else { + } + setOptions(compile(opt)); + }, []); - const valueChangeHandler = (val) => { - setValue(val); + useEffect(() => { + if (type === AssignedFieldValueType.ConstantValue) { + field.value = value; + } else { + if (fieldType === 'currentTime') { + field.value = '{{currentTime}}'; + } else if (fieldType === 'currentUser') { + userValue?.length > 0 && (field.value = `{{currentUser.${userValue.join('.')}}}`); + } else if (fieldType === 'currentRecord') { + recordValue?.length > 0 && (field.value = `{{currentRecord.${recordValue.join('.')}}}`); + } + } + }, [type, value, fieldType, userValue, recordValue]); + + useEffect(() => { + if (type === AssignedFieldValueType.ConstantValue) { + setFieldType(null); + setUserValue([]); + setRecordValue([]); + } + }, [type]); + + const typeChangeHandler = (val) => { + setType(val); }; - // const typeChangeHandler = (val) => { - // setType(val); - // }; + const valueChangeHandler = (val) => { + setValue(val?.target?.value ?? val); + }; - return ; + const fieldTypeChangeHandler = (val) => { + setFieldType(val); + }; + const recordChangeHandler = (val) => { + setRecordValue(val); + }; + const userChangeHandler = (val) => { + setUserValue(val); + }; + return ( + + - // return ( - // - // - - // {type === 'constantValue' ? ( - // - // ) : ( - // - // )} - // - // ); + {type === AssignedFieldValueType.ConstantValue ? ( + + ) : ( + + )} + {fieldType === 'currentRecord' && ( + + )} + {fieldType === 'currentUser' && ( + + )} + + ); }; diff --git a/packages/plugins/export/src/client/useExportAction.ts b/packages/plugins/export/src/client/useExportAction.ts index 1c405452a..6c2e9fc72 100644 --- a/packages/plugins/export/src/client/useExportAction.ts +++ b/packages/plugins/export/src/client/useExportAction.ts @@ -4,8 +4,9 @@ import { useBlockRequestContext, useCollection, useCollectionManager, - useCompile + useCompile, } from '@nocobase/client'; +import { cloneDeep } from 'lodash'; import { useTranslation } from 'react-i18next'; export const useExportAction = () => { @@ -18,9 +19,10 @@ export const useExportAction = () => { const { t } = useTranslation(); return { async onClick() { - const { exportSettings } = actionSchema?.['x-action-settings'] ?? {}; + const { exportSettings } = cloneDeep(actionSchema?.['x-action-settings'] ?? {}); exportSettings.forEach((es) => { - const { uiSchema } = getCollectionJoinField(`${name}.${es.dataIndex.join('.')}`) ?? {}; + const { uiSchema, interface: fieldInterface } = + getCollectionJoinField(`${name}.${es.dataIndex.join('.')}`) ?? {}; es.enum = uiSchema?.enum?.map((e) => ({ value: e.value, label: e.label })); if (!es.enum && uiSchema.type === 'boolean') { es.enum = [ @@ -29,6 +31,9 @@ export const useExportAction = () => { ]; } es.defaultTitle = uiSchema?.title; + if (fieldInterface === 'chinaRegion') { + es.dataIndex.push('name'); + } }); const { data } = await resource.exportXlsx( { diff --git a/packages/plugins/export/src/client/useFields.ts b/packages/plugins/export/src/client/useFields.ts index 82c412ee1..3d9adca2e 100644 --- a/packages/plugins/export/src/client/useFields.ts +++ b/packages/plugins/export/src/client/useFields.ts @@ -4,39 +4,22 @@ import { useCollectionManager } from '@nocobase/client'; export const useFields = (collectionName: string) => { const fieldSchema = useFieldSchema(); const nonfilterable = fieldSchema?.['x-component-props']?.nonfilterable || []; - const { getCollectionFields, getInterface } = useCollectionManager(); + const { getCollectionFields } = useCollectionManager(); const fields = getCollectionFields(collectionName); const field2option = (field, depth) => { - if (nonfilterable.length && depth === 1 && nonfilterable.includes(field.name)) { - return; - } if (!field.interface) { return; } - const fieldInterface = getInterface(field.interface); - if (!fieldInterface.filterable) { - return; - } - const { nested, children, operators } = fieldInterface.filterable; const option = { name: field.name, title: field?.uiSchema?.title || field.name, schema: field?.uiSchema, - operators: - operators?.filter?.((operator) => { - return !operator?.visible || operator.visible(field); - }) || [], }; - if (field.target && depth > 2) { - return; - } - if (depth > 2) { + if (!field.target || depth >= 3) { return option; } - if (children?.length) { - option['children'] = children; - } - if (nested) { + + if (field.target) { const targetFields = getCollectionFields(field.target); const options = getOptions(targetFields, depth + 1).filter(Boolean); option['children'] = option['children'] || []; diff --git a/packages/plugins/export/src/server/__tests__/utils/utils.test.ts b/packages/plugins/export/src/server/__tests__/utils/utils.test.ts index 651067977..6ed2503b3 100644 --- a/packages/plugins/export/src/server/__tests__/utils/utils.test.ts +++ b/packages/plugins/export/src/server/__tests__/utils/utils.test.ts @@ -1,8 +1,15 @@ -import { columns2Appends } from '../../utils/columns2Appends'; +import Database from '@nocobase/database'; +import { mockServer, MockServer } from '@nocobase/test'; describe('utils', () => { let columns = null; - beforeEach(async () => {}); + let db: Database; + let app: MockServer; + + beforeEach(async () => { + app = mockServer(); + db = app.db; + }); afterEach(async () => {}); it('first columns2Appends', async () => { @@ -20,8 +27,8 @@ describe('utils', () => { { dataIndex: ['f_qhvvfuignh2', 'createdBy', 'id'], defaultTitle: 'ID' }, { dataIndex: ['f_wu28mus1c65', 'roles', 'title'], defaultTitle: '角色名称' }, ]; - const appends = columns2Appends(columns); - expect(appends).toMatchObject(['f_qhvvfuignh2.createdBy', 'f_wu28mus1c65.roles']); + // const appends = columns2Appends(columns, app); + // expect(appends).toMatchObject(['f_qhvvfuignh2.createdBy', 'f_wu28mus1c65.roles']); }); it('second columns2Appends', async () => { @@ -39,7 +46,7 @@ describe('utils', () => { { dataIndex: ['f_qhvvfuignh2', 'createdBy', 'id'], defaultTitle: 'ID' }, { dataIndex: ['f_qhvvfuignh2', 'createdBy', 'nickname'], defaultTitle: '角色名称' }, ]; - const appends = columns2Appends(columns); - expect(appends).toMatchObject(['f_qhvvfuignh2.createdBy']); + // const appends = columns2Appends(columns, app); + // expect(appends).toMatchObject(['f_qhvvfuignh2.createdBy']); }); }); diff --git a/packages/plugins/export/src/server/actions/export-xlsx.ts b/packages/plugins/export/src/server/actions/export-xlsx.ts index e8f16a89f..59b5e2f4a 100644 --- a/packages/plugins/export/src/server/actions/export-xlsx.ts +++ b/packages/plugins/export/src/server/actions/export-xlsx.ts @@ -10,7 +10,7 @@ export async function exportXlsx(ctx: Context, next: Next) { if (typeof columns === 'string') { columns = JSON.parse(columns); } - const appends = columns2Appends(columns); + const appends = columns2Appends(columns, ctx); columns = columns?.filter((col) => col?.dataIndex?.length > 0); const repository = ctx.db.getRepository(resourceName, resourceOf) as Repository; const collection = repository.collection; diff --git a/packages/plugins/export/src/server/renders/renders.ts b/packages/plugins/export/src/server/renders/renders.ts index 1e8e7d3f2..061c3f679 100644 --- a/packages/plugins/export/src/server/renders/renders.ts +++ b/packages/plugins/export/src/server/renders/renders.ts @@ -110,7 +110,7 @@ export async function attachment(field, row, ctx) { return (row.get(field.name) || []).map((item) => item[field.url]).join(' '); } -export async function chinaRegion(field, row, ctx) { +export async function chinaRegion(field, row, ctx, column?: any) { const value = row.get(field.name); const values = (Array.isArray(value) ? value : [value]).sort((a, b) => a.level !== b.level ? a.level - b.level : a.sort - b.sort, diff --git a/packages/plugins/export/src/server/utils/columns2Appends.ts b/packages/plugins/export/src/server/utils/columns2Appends.ts index 5b3bf753d..7484b8853 100644 --- a/packages/plugins/export/src/server/utils/columns2Appends.ts +++ b/packages/plugins/export/src/server/utils/columns2Appends.ts @@ -1,11 +1,17 @@ -export function columns2Appends(columns) { +export function columns2Appends(columns, ctx) { + const { resourceName } = ctx.action; const appends = new Set([]); for (const column of columns) { - if (column.dataIndex.length > 1) { - const appendColumns = []; - for (let i = 0, iLen = column.dataIndex.length - 1; i < iLen; i++) { + let collection = ctx.db.getCollection(resourceName); + const appendColumns = []; + for (let i = 0, iLen = column.dataIndex.length; i < iLen; i++) { + let field = collection.getField(column.dataIndex[i]); + if (field.target) { appendColumns.push(column.dataIndex[i]); + collection = ctx.db.getCollection(field.target); } + } + if (appendColumns.length > 0) { appends.add(appendColumns.join('.')); } }