feat: release 202502 (#6259)

* chore(versions): 😊 publish v1.6.0-alpha.24

* chore(versions): 😊 publish v1.6.0-alpha.25

* feat: support extending frontend filter operators (#6085)

* feat: operator extension

* fix: bug

* refactor: code improve

* fix: jsonLogic

---------

Co-authored-by: chenos <chenlinxh@gmail.com>

* refactor: remove registerOperators (#6224)

* refactor(plugin-workflow): trigger workflow action settings (#6143)

* refactor(plugin-workflow): move bind workflow settings to plugin

* refactor(plugin-block-workbench): move component to core

* refactor(plugin-block-workbench): adjust component api

* fix(plugin-workflow-action-trigger): fix test cases

* fix(plugin-workflow): fix component scope

* fix(plugin-workflow-action-trigger): fix test cases

* chore(versions): 😊 publish v1.6.0-alpha.26

* feat: support the extension of preset fields in collections (#6183)

* feat: support the extension of preset fields in collections

* fix: bug

* fix: bug

* fix: bug

* refactor: create collection

* fix: config

* fix: test case

* refactor: code improve

* refactor: code improve

* fix: bug

* fix: bug

---------

Co-authored-by: chenos <chenlinxh@gmail.com>

* feat: support for the extension of optional fields for Kanban, Calendar, and Formula Field plugins (#6076)

* feat: kanban field extention

* fix: bug

* fix: bug

* fix: bug

* fix: bug

* feat: calender title fields

* feat: background color fields

* fix: bug

* fix: bug

* feat: formula field expression support field

* feat: preset fields

* fix: bug

* fix: bug

* fix: bug

* fix: bug

* fix: bug

* fix: bug

* fix: bug

* fix: bug

* refactor: code improve

* fix: bug

* fix: bug

* fix: bug

* fix: bug

* refactor: code improve

* revert: preset fields

* refactor: code improve

* refactor: code improve

* fix: bug

* fix: bug

* fix: bug

* refactor: code improve

* fix: bug

* refactor: code improve

* refactor: code improve

* fix: bug

* fix: locale

* refactor: code improve

* fix: bug

* refactor: code improve

* refactor: code improve

* refactor: code improve

* refactor: locale

* fix: test

* fix: bug

* fix: test

* fix: test

---------

Co-authored-by: chenos <chenlinxh@gmail.com>

* chore(versions): 😊 publish v1.6.0-alpha.27

* fix(data-source-main): update order

* fix: improve code

* fix: getFontColor (#6241)

* chore(versions): 😊 publish v1.6.0-alpha.28

* fix: print action e2e test (#6256)

* fix: print action e2e test

* fix: test

* fix: version

---------

Co-authored-by: katherinehhh <katherine_15995@163.com>
Co-authored-by: nocobase[bot] <179432756+nocobase[bot]@users.noreply.github.com>
Co-authored-by: Junyi <mytharcher@users.noreply.github.com>
This commit is contained in:
chenos 2025-02-21 13:25:17 +08:00 committed by GitHub
parent 459a721e98
commit 5fbc7697c6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
74 changed files with 1177 additions and 1257 deletions

View File

@ -2,9 +2,7 @@
"version": "1.6.0-beta.8", "version": "1.6.0-beta.8",
"npmClient": "yarn", "npmClient": "yarn",
"useWorkspaces": true, "useWorkspaces": true,
"npmClientArgs": [ "npmClientArgs": ["--ignore-engines"],
"--ignore-engines"
],
"command": { "command": {
"version": { "version": {
"forcePublish": true, "forcePublish": true,

View File

@ -44,8 +44,14 @@ import type { CollectionFieldInterfaceFactory } from '../data-source';
import { OpenModeProvider } from '../modules/popup/OpenModeProvider'; import { OpenModeProvider } from '../modules/popup/OpenModeProvider';
import { AppSchemaComponentProvider } from './AppSchemaComponentProvider'; import { AppSchemaComponentProvider } from './AppSchemaComponentProvider';
import type { Plugin } from './Plugin'; import type { Plugin } from './Plugin';
import { getOperators } from './globalOperators';
import type { RequireJS } from './utils/requirejs'; import type { RequireJS } from './utils/requirejs';
type JsonLogic = {
addOperation: (name: string, fn?: any) => void;
rmOperation: (name: string) => void;
};
declare global { declare global {
interface Window { interface Window {
define: RequireJS['define']; define: RequireJS['define'];
@ -100,7 +106,7 @@ export class Application {
public dataSourceManager: DataSourceManager; public dataSourceManager: DataSourceManager;
public name: string; public name: string;
public globalVars: Record<string, any> = {}; public globalVars: Record<string, any> = {};
public jsonLogic: JsonLogic;
loading = true; loading = true;
maintained = false; maintained = false;
maintaining = false; maintaining = false;
@ -155,6 +161,7 @@ export class Application {
this.apiClient.auth.locale = lng; this.apiClient.auth.locale = lng;
}); });
this.initListeners(); this.initListeners();
this.jsonLogic = getOperators();
} }
private initListeners() { private initListeners() {

View File

@ -9,13 +9,11 @@
/* globals define,module */ /* globals define,module */
import dayjs from 'dayjs';
/* /*
Using a Universal Module Loader that should be browser, require, and AMD friendly Using a Universal Module Loader that should be browser, require, and AMD friendly
http://ricostacruz.com/cheatsheets/umdjs.html http://ricostacruz.com/cheatsheets/umdjs.html
*/ */
export function getJsonLogic() { export function getOperators() {
'use strict'; 'use strict';
/* globals console:false */ /* globals console:false */
@ -359,12 +357,12 @@ export function getJsonLogic() {
return !!value; return !!value;
}; };
jsonLogic.get_operator = function (logic) { jsonLogic.getOperator = function (logic) {
return Object.keys(logic)[0]; return Object.keys(logic)[0];
}; };
jsonLogic.get_values = function (logic) { jsonLogic.getValues = function (logic) {
return logic[jsonLogic.get_operator(logic)]; return logic[jsonLogic.getOperator(logic)];
}; };
jsonLogic.apply = function (logic, data) { jsonLogic.apply = function (logic, data) {
@ -379,7 +377,7 @@ export function getJsonLogic() {
return logic; return logic;
} }
var op = jsonLogic.get_operator(logic); var op = jsonLogic.getOperator(logic);
var values = logic[op]; var values = logic[op];
var i; var i;
var current; var current;
@ -543,7 +541,7 @@ export function getJsonLogic() {
var collection = []; var collection = [];
if (jsonLogic.is_logic(logic)) { if (jsonLogic.is_logic(logic)) {
var op = jsonLogic.get_operator(logic); var op = jsonLogic.getOperator(logic);
var values = logic[op]; var values = logic[op];
if (!Array.isArray(values)) { if (!Array.isArray(values)) {
@ -564,11 +562,11 @@ export function getJsonLogic() {
return arrayUnique(collection); return arrayUnique(collection);
}; };
jsonLogic.add_operation = function (name, code) { jsonLogic.addOperation = function (name, code) {
operations[name] = code; operations[name] = code;
}; };
jsonLogic.rm_operation = function (name) { jsonLogic.rmOperation = function (name) {
delete operations[name]; delete operations[name];
}; };
@ -593,8 +591,8 @@ export function getJsonLogic() {
if (jsonLogic.is_logic(pattern)) { if (jsonLogic.is_logic(pattern)) {
if (jsonLogic.is_logic(rule)) { if (jsonLogic.is_logic(rule)) {
var pattern_op = jsonLogic.get_operator(pattern); var pattern_op = jsonLogic.getOperator(pattern);
var rule_op = jsonLogic.get_operator(rule); var rule_op = jsonLogic.getOperator(rule);
if (pattern_op === '@' || pattern_op === rule_op) { if (pattern_op === '@' || pattern_op === rule_op) {
// echo "\nOperators match, go deeper\n"; // echo "\nOperators match, go deeper\n";

View File

@ -133,7 +133,8 @@ export const useCollectionManager_deprecated = (dataSourceName?: string) => {
const getCollectionFieldsOptions = useCallback( const getCollectionFieldsOptions = useCallback(
( (
collectionName: string, collectionName: string,
type: string | string[] = 'string', type?: string | string[],
interfaces?: string | string[],
opts?: { opts?: {
dataSource?: string; dataSource?: string;
cached?: Record<string, any>; cached?: Record<string, any>;
@ -183,9 +184,12 @@ export const useCollectionManager_deprecated = (dataSourceName?: string) => {
return _.cloneDeep(cached[collectionName]); return _.cloneDeep(cached[collectionName]);
} }
if (typeof type === 'string') { if (type && typeof type === 'string') {
type = [type]; type = [type];
} }
if (interfaces && typeof interfaces === 'string') {
interfaces = [interfaces];
}
const fields = getCollectionFields(collectionName, customDataSourceNameValue); const fields = getCollectionFields(collectionName, customDataSourceNameValue);
const options = fields const options = fields
?.filter( ?.filter(
@ -193,7 +197,8 @@ export const useCollectionManager_deprecated = (dataSourceName?: string) => {
field.interface && field.interface &&
!exceptInterfaces.includes(field.interface) && !exceptInterfaces.includes(field.interface) &&
(allowAllTypes || (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 && field.target && field.target !== collectionName && Array.isArray(association)
? association.includes(field.interface) ? association.includes(field.interface)
: false)), : false)),
@ -207,7 +212,7 @@ export const useCollectionManager_deprecated = (dataSourceName?: string) => {
if (association && field.target) { if (association && field.target) {
result.children = collectionNames.includes(field.target) result.children = collectionNames.includes(field.target)
? [] ? []
: getCollectionFieldsOptions(field.target, type, { : getCollectionFieldsOptions(field.target, type, interfaces, {
...opts, ...opts,
cached, cached,
dataSource: customDataSourceNameValue, dataSource: customDataSourceNameValue,

View File

@ -28,6 +28,7 @@ export class CreatedAtFieldInterface extends CollectionFieldInterface {
'x-read-pretty': true, 'x-read-pretty': true,
}, },
}; };
description = '{{t("Store the creation time of each record")}}';
availableTypes = []; availableTypes = [];
properties = { properties = {
...defaultProps, ...defaultProps,

View File

@ -76,4 +76,5 @@ export class CreatedByFieldInterface extends CollectionFieldInterface {
schema['x-component-props']['ellipsis'] = true; schema['x-component-props']['ellipsis'] = true;
} }
} }
description = '{{t("Store the creation user of each record")}}';
} }

View File

@ -55,5 +55,7 @@ export class IdFieldInterface extends CollectionFieldInterface {
filterable = { filterable = {
operators: operators.id, operators: operators.id,
}; };
description = '{{t("Primary key, unique identifier, self growth") }}';
titleUsable = true; titleUsable = true;
} }

View File

@ -21,13 +21,14 @@ export class UpdatedAtFieldInterface extends CollectionFieldInterface {
type: 'date', type: 'date',
field: 'updatedAt', field: 'updatedAt',
uiSchema: { uiSchema: {
type: 'string', type: 'datetime',
title: '{{t("Last updated at")}}', title: '{{t("Last updated at")}}',
'x-component': 'DatePicker', 'x-component': 'DatePicker',
'x-component-props': {}, 'x-component-props': {},
'x-read-pretty': true, 'x-read-pretty': true,
}, },
}; };
description = '{{t("Store the last update time of each record")}}';
availableTypes = []; availableTypes = [];
properties = { properties = {
...defaultProps, ...defaultProps,

View File

@ -75,4 +75,5 @@ export class UpdatedByFieldInterface extends CollectionFieldInterface {
schema['x-component-props']['ellipsis'] = true; schema['x-component-props']['ellipsis'] = true;
} }
} }
description = '{{t("Store the last update user of each record")}}';
} }

View File

@ -9,106 +9,27 @@
import { observer, useForm } from '@formily/react'; import { observer, useForm } from '@formily/react';
import { Table, Tag } from 'antd'; import { Table, Tag } from 'antd';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useCollectionManager_deprecated } from '../../'; 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') { if (values?.template === 'view' || values?.template === 'sql') {
return values.fields; return values.fields;
} }
const defaults = values.fields const fields =
? [...values.fields].filter((v) => { values.fields?.filter((v) => {
return !['id', 'createdBy', 'updatedAt', 'createdAt', 'updatedBy'].includes(v.name); const item = collectionPresetFields.find((i) => i.value.name === v.name);
}) return !item;
: []; }) || [];
if (presetFields.find((v) => v.name === 'id')) { presetFields.map((v) => {
defaults.push({ const item = collectionPresetFields.find((i) => i.value.name === v);
name: 'id', item && fields.push(item.value);
type: 'bigInt',
autoIncrement: true,
primaryKey: true,
allowNull: false,
uiSchema: { type: 'number', title: '{{t("ID")}}', 'x-component': 'InputNumber', 'x-read-pretty': true },
interface: 'integer',
}); });
} return fields;
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;
}; };
export const PresetFields = observer( export const PresetFields = observer(
(props: any) => { (props: any) => {
const { getInterface } = useCollectionManager_deprecated(); const { getInterface } = useCollectionManager_deprecated();
@ -116,11 +37,26 @@ export const PresetFields = observer(
const compile = useCompile(); const compile = useCompile();
const [selectedRowKeys, setSelectedRowKeys] = useState([]); const [selectedRowKeys, setSelectedRowKeys] = useState([]);
const { t } = useTranslation(); 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 = [ const column = [
{ {
title: t('Field'), title: t('Field'),
dataIndex: 'field', dataIndex: 'field',
key: 'field', key: 'field',
render: (value) => compile(value),
}, },
{ {
title: t('Interface'), title: t('Interface'),
@ -132,61 +68,19 @@ export const PresetFields = observer(
title: t('Description'), title: t('Description'),
dataIndex: 'description', dataIndex: 'description',
key: 'description', key: 'description',
}, render: (value) => compile(value),
];
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',
}, },
]; ];
useEffect(() => { useEffect(() => {
const config = { const initialValue = presetFieldsDataSource.map((v) => v.name);
autoGenId: false,
createdAt: true,
createdBy: true,
updatedAt: true,
updatedBy: true,
};
const initialValue = ['id', 'createdAt', 'createdBy', 'updatedAt', 'updatedBy'];
setSelectedRowKeys(initialValue); setSelectedRowKeys(initialValue);
form.setValues({ ...form.values, ...config }); form.setValues({ ...form.values, autoGenId: false });
}, []); }, [presetFieldsDataSource]);
useEffect(() => { useEffect(() => {
const fields = getDefaultCollectionFields( const fields = getDefaultCollectionFields(
selectedRowKeys.map((v) => { selectedRowKeys.map((v) => v),
return {
name: v,
};
}),
form.values, form.values,
collectionPresetFields,
); );
form.setValuesIn('fields', fields); form.setValuesIn('fields', fields);
}, [selectedRowKeys]); }, [selectedRowKeys]);
@ -197,7 +91,7 @@ export const PresetFields = observer(
rowKey="name" rowKey="name"
bordered bordered
scroll={{ x: 600 }} scroll={{ x: 600 }}
dataSource={dataSource} dataSource={presetFieldsDataSource}
columns={column} columns={column}
rowSelection={{ rowSelection={{
type: 'checkbox', type: 'checkbox',
@ -206,21 +100,10 @@ export const PresetFields = observer(
name: record.name, name: record.name,
disabled: props?.disabled || props?.presetFieldsDisabledIncludes?.includes?.(record.name), disabled: props?.disabled || props?.presetFieldsDisabledIncludes?.includes?.(record.name),
}), }),
onChange: (_, selectedRows) => { onChange: (selectedKeys, selectedRows) => {
const fields = getDefaultCollectionFields(selectedRows, form.values); const fields = getDefaultCollectionFields(selectedKeys, form.values, collectionPresetFields);
const config = { setSelectedRowKeys(selectedKeys);
autoGenId: false, form.setValues({ ...form.values, fields, 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 });
}, },
}} }}
/> />

View File

@ -19,7 +19,6 @@ import {
RemoveButton, RemoveButton,
SecondConFirm, SecondConFirm,
SkipValidation, SkipValidation,
WorkflowConfig,
} from '../../../schema-component/antd/action/Action.Designer'; } from '../../../schema-component/antd/action/Action.Designer';
/** /**
@ -53,10 +52,6 @@ export const customizeSaveRecordActionSettings = new SchemaSettings({
name: 'afterSuccessfulSubmission', name: 'afterSuccessfulSubmission',
Component: AfterSuccess, Component: AfterSuccess,
}, },
{
name: 'bindWorkflow',
Component: WorkflowConfig,
},
{ {
name: 'refreshDataBlockRequest', name: 'refreshDataBlockRequest',
Component: RefreshDataBlockRequest, Component: RefreshDataBlockRequest,

View File

@ -22,9 +22,6 @@ export const CreateSubmitActionInitializer = (props) => {
type: 'primary', type: 'primary',
htmlType: 'submit', htmlType: 'submit',
}, },
'x-action-settings': {
triggerWorkflows: [],
},
}; };
return <ActionInitializerItem {...props} schema={schema} />; return <ActionInitializerItem {...props} schema={schema} />;
}; };

View File

@ -23,9 +23,6 @@ export const UpdateSubmitActionInitializer = (props) => {
type: 'primary', type: 'primary',
htmlType: 'submit', htmlType: 'submit',
}, },
'x-action-settings': {
triggerWorkflows: [],
},
}; };
return <ActionInitializerItem {...props} schema={schema} />; return <ActionInitializerItem {...props} schema={schema} />;
}; };

View File

@ -24,7 +24,6 @@ import {
RemoveButton, RemoveButton,
SecondConFirm, SecondConFirm,
SkipValidation, SkipValidation,
WorkflowConfig,
} from '../../../schema-component/antd/action/Action.Designer'; } from '../../../schema-component/antd/action/Action.Designer';
import { useCollectionState } from '../../../schema-settings/DataTemplates/hooks/useCollectionState'; import { useCollectionState } from '../../../schema-settings/DataTemplates/hooks/useCollectionState';
import { SchemaSettingsModalItem } from '../../../schema-settings/SchemaSettings'; import { SchemaSettingsModalItem } from '../../../schema-settings/SchemaSettings';
@ -154,14 +153,6 @@ export const createSubmitActionSettings = new SchemaSettings({
name: 'secondConfirmation', name: 'secondConfirmation',
Component: SecondConFirm, Component: SecondConFirm,
}, },
{
name: 'workflowConfig',
Component: WorkflowConfig,
useVisible() {
const fieldSchema = useFieldSchema();
return isValid(fieldSchema?.['x-action-settings']?.triggerWorkflows);
},
},
{ {
name: 'saveMode', name: 'saveMode',
Component: SaveMode, Component: SaveMode,

View File

@ -19,7 +19,6 @@ import {
RemoveButton, RemoveButton,
SecondConFirm, SecondConFirm,
SkipValidation, SkipValidation,
WorkflowConfig,
} from '../../../schema-component/antd/action/Action.Designer'; } from '../../../schema-component/antd/action/Action.Designer';
import { SaveMode } from './createSubmitActionSettings'; import { SaveMode } from './createSubmitActionSettings';
import { SchemaSettingsLinkageRules } from '../../../schema-settings'; import { SchemaSettingsLinkageRules } from '../../../schema-settings';
@ -56,14 +55,6 @@ export const updateSubmitActionSettings = new SchemaSettings({
name: 'secondConfirmation', name: 'secondConfirmation',
Component: SecondConFirm, Component: SecondConFirm,
}, },
{
name: 'workflowConfig',
Component: WorkflowConfig,
useVisible() {
const fieldSchema = useFieldSchema();
return isValid(fieldSchema?.['x-action-settings']?.triggerWorkflows);
},
},
{ {
name: 'assignFieldValues', name: 'assignFieldValues',
Component: AssignedFieldValues, Component: AssignedFieldValues,
@ -120,14 +111,6 @@ export const submitActionSettings = new SchemaSettings({
name: 'secondConfirmation', name: 'secondConfirmation',
Component: SecondConFirm, Component: SecondConFirm,
}, },
{
name: 'workflowConfig',
Component: WorkflowConfig,
useVisible() {
const fieldSchema = useFieldSchema();
return isValid(fieldSchema?.['x-action-settings']?.triggerWorkflows);
},
},
{ {
name: 'saveMode', name: 'saveMode',
Component: SaveMode, Component: SaveMode,

View File

@ -18,7 +18,6 @@ import {
ButtonEditor, ButtonEditor,
RemoveButton, RemoveButton,
SecondConFirm, SecondConFirm,
WorkflowConfig,
RefreshDataBlockRequest, RefreshDataBlockRequest,
} from '../../../schema-component/antd/action/Action.Designer'; } from '../../../schema-component/antd/action/Action.Designer';
import { SchemaSettingsLinkageRules } from '../../../schema-settings'; import { SchemaSettingsLinkageRules } from '../../../schema-settings';
@ -58,14 +57,6 @@ export const customizeUpdateRecordActionSettings = new SchemaSettings({
name: 'afterSuccessfulSubmission', name: 'afterSuccessfulSubmission',
Component: AfterSuccess, Component: AfterSuccess,
}, },
{
name: 'workflowConfig',
Component: WorkflowConfig,
useVisible() {
const fieldSchema = useFieldSchema();
return isValid(fieldSchema?.['x-action-settings']?.triggerWorkflows);
},
},
{ {
name: 'refreshDataBlockRequest', name: 'refreshDataBlockRequest',
Component: RefreshDataBlockRequest, Component: RefreshDataBlockRequest,

View File

@ -7,25 +7,16 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import { ArrayTable } from '@formily/antd-v5'; import { ISchema, useField, useFieldSchema } from '@formily/react';
import { onFieldValueChange } from '@formily/core';
import { ISchema, useField, useFieldSchema, useForm, useFormEffects } from '@formily/react';
import { isValid, uid } from '@formily/shared'; import { isValid, uid } from '@formily/shared';
import { Alert, Flex, ModalProps, Tag } from 'antd'; import { ModalProps } from 'antd';
import React, { useCallback, useMemo, useState } from 'react'; import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { RemoteSelect, useCompile, useDesignable } from '../..'; import { useCompile, useDesignable } from '../..';
import { isInitializersSame, useApp } from '../../../application'; import { isInitializersSame, useApp } from '../../../application';
import { usePlugin } from '../../../application/hooks';
import { SchemaSettingOptions, SchemaSettings } from '../../../application/schema-settings'; import { SchemaSettingOptions, SchemaSettings } from '../../../application/schema-settings';
import { useSchemaToolbar } from '../../../application/schema-toolbar'; import { useSchemaToolbar } from '../../../application/schema-toolbar';
import { useFormBlockContext } from '../../../block-provider/FormBlockProvider'; import { useCollectionManager_deprecated, useCollection_deprecated } from '../../../collection-manager';
import {
joinCollectionName,
useCollectionManager_deprecated,
useCollection_deprecated,
} from '../../../collection-manager';
import { DataSourceProvider, useDataSourceKey } from '../../../data-source';
import { FlagProvider } from '../../../flag-provider'; import { FlagProvider } from '../../../flag-provider';
import { SaveMode } from '../../../modules/actions/submit/createSubmitActionSettings'; import { SaveMode } from '../../../modules/actions/submit/createSubmitActionSettings';
import { useOpenModeContext } from '../../../modules/popup/OpenModeProvider'; 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 (
<DataSourceProvider dataSource="main">
<RemoteSelect
manual={false}
placeholder={t('Select workflow', { ns: 'workflow' })}
fieldNames={{
label: 'title',
value: 'key',
}}
service={{
resource: 'workflows',
action: 'list',
params: {
filter: {
type: workflowTypes,
enabled: true,
'config.collection': workflowCollection,
},
},
}}
optionFilter={optionFilter}
optionRender={({ label, data }) => {
const typeOption = triggerOptions.find((item) => item.value === data.type);
return typeOption ? (
<Flex justify="space-between">
<span>{label}</span>
<Tag color={typeOption.color}>{compile(typeOption.label)}</Tag>
</Flex>
) : (
label
);
}}
{...props}
/>
</DataSourceProvider>
);
}
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 (
<SchemaSettingsActionModalItem
title={t('Bind workflows', { ns: 'workflow' })}
scope={{
fieldFilter(field) {
return ['belongsTo', 'hasOne'].includes(field.type);
},
}}
components={{
Alert,
ArrayTable,
WorkflowSelect,
}}
schema={
{
type: 'void',
title: t('Bind workflows', { ns: 'workflow' }),
properties: {
description: description && {
type: 'void',
'x-component': 'Alert',
'x-component-props': {
message: description,
style: {
marginBottom: '1em',
},
},
},
group: {
type: 'array',
'x-component': 'ArrayTable',
'x-decorator': 'FormItem',
items: {
type: 'object',
properties: {
sort: {
type: 'void',
'x-component': 'ArrayTable.Column',
'x-component-props': { width: 50, title: '', align: 'center' },
properties: {
sort: {
type: 'void',
'x-component': 'ArrayTable.SortHandle',
},
},
},
context: {
type: 'void',
'x-component': 'ArrayTable.Column',
'x-component-props': {
title: t('Trigger data context', { ns: 'workflow' }),
width: 200,
},
properties: {
context: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'AppendsTreeSelect',
'x-component-props': {
placeholder: t('Select context', { ns: 'workflow' }),
popupMatchSelectWidth: false,
collection: `${
collection.dataSource && collection.dataSource !== 'main' ? `${collection.dataSource}:` : ''
}${collection.name}`,
filter: '{{ fieldFilter }}',
rootOption: {
label: t('Full form data', { ns: 'workflow' }),
value: '',
},
allowClear: false,
loadData: buttonAction === 'destroy' ? null : undefined,
},
default: '',
},
},
},
workflowKey: {
type: 'void',
'x-component': 'ArrayTable.Column',
'x-component-props': {
title: t('Workflow', { ns: 'workflow' }),
},
properties: {
workflowKey: {
type: 'number',
'x-decorator': 'FormItem',
'x-component': 'WorkflowSelect',
'x-component-props': {
placeholder: t('Select workflow', { ns: 'workflow' }),
actionType,
formAction,
buttonAction,
},
required: true,
},
},
},
operations: {
type: 'void',
'x-component': 'ArrayTable.Column',
'x-component-props': {
width: 32,
},
properties: {
remove: {
type: 'void',
'x-component': 'ArrayTable.Remove',
},
},
},
},
},
properties: {
add: {
type: 'void',
title: t('Add workflow', { ns: 'workflow' }),
'x-component': 'ArrayTable.Addition',
},
},
},
},
} as ISchema
}
initialValues={{ group: fieldSchema?.['x-action-settings']?.triggerWorkflows }}
onSubmit={({ group }) => {
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'] = [ export const actionSettingsItems: SchemaSettingOptions['items'] = [
{ {
name: 'Customize', name: 'Customize',
@ -774,14 +484,6 @@ export const actionSettingsItems: SchemaSettingOptions['items'] = [
return isValid(fieldSchema?.['x-action-settings']?.onSuccess); return isValid(fieldSchema?.['x-action-settings']?.onSuccess);
}, },
}, },
{
name: 'workflowConfig',
Component: WorkflowConfig,
useVisible() {
const fieldSchema = useFieldSchema();
return isValid(fieldSchema?.['x-action-settings']?.triggerWorkflows);
},
},
{ {
name: 'saveMode', name: 'saveMode',
Component: SaveMode, Component: SaveMode,

View File

@ -47,6 +47,7 @@ import { ActionContextProvider } from './context';
import { useGetAriaLabelOfAction } from './hooks/useGetAriaLabelOfAction'; import { useGetAriaLabelOfAction } from './hooks/useGetAriaLabelOfAction';
import { ActionContextProps, ActionProps, ComposedAction } from './types'; import { ActionContextProps, ActionProps, ComposedAction } from './types';
import { linkageAction, setInitialActionState } from './utils'; import { linkageAction, setInitialActionState } from './utils';
import { useApp } from '../../../application';
const useA = () => { const useA = () => {
return { return {
@ -95,7 +96,7 @@ export const Action: ComposedAction = withDynamicSchemaProps(
const { setSubmitted } = useActionContext(); const { setSubmitted } = useActionContext();
const { getAriaLabel } = useGetAriaLabelOfAction(title); const { getAriaLabel } = useGetAriaLabelOfAction(title);
const parentRecordData = useCollectionParentRecordData(); const parentRecordData = useCollectionParentRecordData();
const app = useApp();
useEffect(() => { useEffect(() => {
if (field.stateOfLinkageRules) { if (field.stateOfLinkageRules) {
setInitialActionState(field); setInitialActionState(field);
@ -105,13 +106,16 @@ export const Action: ComposedAction = withDynamicSchemaProps(
.filter((k) => !k.disabled) .filter((k) => !k.disabled)
.forEach((v) => { .forEach((v) => {
v.actions?.forEach((h) => { v.actions?.forEach((h) => {
linkageAction({ linkageAction(
{
operator: h.operator, operator: h.operator,
field, field,
condition: v.condition, condition: v.condition,
variables, variables,
localVariables, localVariables,
}); },
app.jsonLogic,
);
}); });
}); });
}, [field, linkageRules, localVariables, variables]); }, [field, linkageRules, localVariables, variables]);

View File

@ -80,7 +80,8 @@ export const requestSettingsSchema: ISchema = {
}, },
}; };
export const linkageAction = async ({ export const linkageAction = async (
{
operator, operator,
field, field,
condition, condition,
@ -92,13 +93,15 @@ export const linkageAction = async ({
condition; condition;
variables: VariablesContextType; variables: VariablesContextType;
localVariables: VariableOption[]; localVariables: VariableOption[];
}) => { },
jsonLogic: any,
) => {
const disableResult = field?.stateOfLinkageRules?.disabled || [false]; const disableResult = field?.stateOfLinkageRules?.disabled || [false];
const displayResult = field?.stateOfLinkageRules?.display || ['visible']; const displayResult = field?.stateOfLinkageRules?.display || ['visible'];
switch (operator) { switch (operator) {
case ActionType.Visible: case ActionType.Visible:
if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables })) { if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables }, jsonLogic)) {
displayResult.push(operator); displayResult.push(operator);
field.data = field.data || {}; field.data = field.data || {};
field.data.hidden = false; field.data.hidden = false;
@ -110,7 +113,7 @@ export const linkageAction = async ({
field.display = last(displayResult); field.display = last(displayResult);
break; break;
case ActionType.Hidden: case ActionType.Hidden:
if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables })) { if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables }, jsonLogic)) {
field.data = field.data || {}; field.data = field.data || {};
field.data.hidden = true; field.data.hidden = true;
} else { } else {
@ -119,7 +122,7 @@ export const linkageAction = async ({
} }
break; break;
case ActionType.Disabled: case ActionType.Disabled:
if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables })) { if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables }, jsonLogic)) {
disableResult.push(true); disableResult.push(true);
} }
field.stateOfLinkageRules = { field.stateOfLinkageRules = {
@ -130,7 +133,7 @@ export const linkageAction = async ({
field.componentProps['disabled'] = last(disableResult); field.componentProps['disabled'] = last(disableResult);
break; break;
case ActionType.Active: case ActionType.Active:
if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables })) { if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables }, jsonLogic)) {
disableResult.push(false); disableResult.push(false);
} else { } else {
disableResult.push(!!field.componentProps?.['disabled']); disableResult.push(!!field.componentProps?.['disabled']);

View File

@ -16,6 +16,7 @@ import { forEachLinkageRule } from '../../../../schema-settings/LinkageRules/for
import useLocalVariables from '../../../../variables/hooks/useLocalVariables'; import useLocalVariables from '../../../../variables/hooks/useLocalVariables';
import useVariables from '../../../../variables/hooks/useVariables'; import useVariables from '../../../../variables/hooks/useVariables';
import { useSubFormValue } from '../../association-field/hooks'; import { useSubFormValue } from '../../association-field/hooks';
import { useApp } from '../../../../application';
import { isSubMode } from '../../association-field/util'; import { isSubMode } from '../../association-field/util';
const isSubFormOrSubTableField = (fieldSchema: Schema) => { const isSubFormOrSubTableField = (fieldSchema: Schema) => {
@ -45,6 +46,7 @@ export const useLinkageRulesForSubTableOrSubForm = () => {
const variables = useVariables(); const variables = useVariables();
const linkageRules = getLinkageRules(schemaOfSubTableOrSubForm); const linkageRules = getLinkageRules(schemaOfSubTableOrSubForm);
const app = useApp();
useEffect(() => { useEffect(() => {
if (!isSubFormOrSubTableField(fieldSchema)) { if (!isSubFormOrSubTableField(fieldSchema)) {
@ -77,7 +79,8 @@ export const useLinkageRulesForSubTableOrSubForm = () => {
forEachLinkageRule(linkageRules, (action, rule) => { forEachLinkageRule(linkageRules, (action, rule) => {
if (action.targetFields?.includes(fieldSchema.name)) { if (action.targetFields?.includes(fieldSchema.name)) {
disposes.push( disposes.push(
bindLinkageRulesToFiled({ bindLinkageRulesToFiled(
{
field, field,
linkageRules, linkageRules,
formValues: formValue, formValues: formValue,
@ -86,7 +89,9 @@ export const useLinkageRulesForSubTableOrSubForm = () => {
rule, rule,
variables, variables,
variableNameOfLeftCondition: '$iteration', variableNameOfLeftCondition: '$iteration',
}), },
app.jsonLogic,
),
); );
} }
}); });

View File

@ -27,6 +27,7 @@ import { useToken } from '../../../style';
import { useLocalVariables, useVariables } from '../../../variables'; import { useLocalVariables, useVariables } from '../../../variables';
import { useProps } from '../../hooks/useProps'; import { useProps } from '../../hooks/useProps';
import { useFormBlockHeight } from './hook'; import { useFormBlockHeight } from './hook';
import { useApp } from '../../../application';
export interface FormProps extends IFormLayoutProps { export interface FormProps extends IFormLayoutProps {
form?: FormilyForm; form?: FormilyForm;
@ -136,6 +137,7 @@ const WithForm = (props: WithFormProps) => {
const localVariables = useLocalVariables({ currentForm: form }); const localVariables = useLocalVariables({ currentForm: form });
const { templateFinished } = useTemplateBlockContext(); const { templateFinished } = useTemplateBlockContext();
const { loading } = useDataBlockRequest() || {}; const { loading } = useDataBlockRequest() || {};
const app = useApp();
const linkageRules: any[] = const linkageRules: any[] =
(getLinkageRules(fieldSchema) || fieldSchema.parent?.['x-linkage-rules'])?.filter((k) => !k.disabled) || []; (getLinkageRules(fieldSchema) || fieldSchema.parent?.['x-linkage-rules'])?.filter((k) => !k.disabled) || [];
@ -175,7 +177,8 @@ const WithForm = (props: WithFormProps) => {
// 之前使用的 `onFieldReact` 有问题,没有办法被取消监听,所以这里用 `onFieldInit` 和 `reaction` 代替 // 之前使用的 `onFieldReact` 有问题,没有办法被取消监听,所以这里用 `onFieldInit` 和 `reaction` 代替
onFieldInit(`*(${fields})`, (field: any, form) => { onFieldInit(`*(${fields})`, (field: any, form) => {
disposes.push( disposes.push(
bindLinkageRulesToFiled({ bindLinkageRulesToFiled(
{
field, field,
linkageRules, linkageRules,
formValues: form.values, formValues: form.values,
@ -183,7 +186,9 @@ const WithForm = (props: WithFormProps) => {
action, action,
rule, rule,
variables, variables,
}), },
app.jsonLogic,
),
); );
}); });
} }

View File

@ -11,7 +11,7 @@ import { ISchema } from '@formily/react';
import { isArr } from '@formily/shared'; import { isArr } from '@formily/shared';
import { dayjs, getDefaultFormat, str2moment } from '@nocobase/utils/client'; import { dayjs, getDefaultFormat, str2moment } from '@nocobase/utils/client';
import { Tag } from 'antd'; import { Tag } from 'antd';
import React from 'react'; import React, { Component } from 'react';
import { CollectionFieldOptions_deprecated, useCollectionManager_deprecated } from '../../../collection-manager'; import { CollectionFieldOptions_deprecated, useCollectionManager_deprecated } from '../../../collection-manager';
export const useLabelUiSchema = (collectionField: CollectionFieldOptions_deprecated, label: string): ISchema => { export const useLabelUiSchema = (collectionField: CollectionFieldOptions_deprecated, label: string): ISchema => {
@ -30,7 +30,10 @@ export const getDatePickerLabels = (props): string => {
return isArr(labels) ? labels.join('~') : labels; 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 <TitleRenderer value={value} />;
}
if (Array.isArray(labelUiSchema?.enum) && value) { if (Array.isArray(labelUiSchema?.enum) && value) {
const opt: any = labelUiSchema.enum.find((option: any) => option.value === value); const opt: any = labelUiSchema.enum.find((option: any) => option.value === value);
if (isTag) { if (isTag) {

View File

@ -14,7 +14,7 @@ import { VariableOption, VariablesContextType } from '../../../variables/types';
import { isVariable } from '../../../variables/utils/isVariable'; import { isVariable } from '../../../variables/utils/isVariable';
import { transformVariableValue } from '../../../variables/utils/transformVariableValue'; import { transformVariableValue } from '../../../variables/utils/transformVariableValue';
import { inferPickerType } from '../../antd/date-picker/util'; import { inferPickerType } from '../../antd/date-picker/util';
import { getJsonLogic } from '../../common/utils/logic';
type VariablesCtx = { type VariablesCtx = {
/** 当前登录的用户 */ /** 当前登录的用户 */
$user?: Record<string, any>; $user?: Record<string, any>;
@ -76,7 +76,8 @@ function getAllKeys(obj) {
return keys; return keys;
} }
export const conditionAnalyses = async ({ export const conditionAnalyses = async (
{
ruleGroup, ruleGroup,
variables, variables,
localVariables, localVariables,
@ -90,17 +91,19 @@ export const conditionAnalyses = async ({
* @default '$nForm' * @default '$nForm'
*/ */
variableNameOfLeftCondition?: string; variableNameOfLeftCondition?: string;
}) => { },
jsonLogic: any,
) => {
const type = Object.keys(ruleGroup)[0] || '$and'; const type = Object.keys(ruleGroup)[0] || '$and';
const conditions = ruleGroup[type]; const conditions = ruleGroup[type];
let results = conditions.map(async (condition) => { let results = conditions.map(async (condition) => {
if ('$and' in condition || '$or' in 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 logicCalculation = getInnermostKeyAndValue(condition);
const operator = jsonlogic?.key; const operator = logicCalculation?.key;
if (!operator) { if (!operator) {
return true; return true;
@ -113,12 +116,11 @@ export const conditionAnalyses = async ({
}) })
.then(({ value }) => value); .then(({ value }) => value);
const parsingResult = isVariable(jsonlogic?.value) const parsingResult = isVariable(logicCalculation?.value)
? [variables.parseVariable(jsonlogic?.value, localVariables).then(({ value }) => value), targetValue] ? [variables.parseVariable(logicCalculation?.value, localVariables).then(({ value }) => value), targetValue]
: [jsonlogic?.value, targetValue]; : [logicCalculation?.value, targetValue];
try { try {
const jsonLogic = getJsonLogic();
const [value, targetValue] = await Promise.all(parsingResult); const [value, targetValue] = await Promise.all(parsingResult);
const targetCollectionField = await variables.getCollectionField(targetVariableName, localVariables); const targetCollectionField = await variables.getCollectionField(targetVariableName, localVariables);
let currentInputValue = transformVariableValue(targetValue, { targetCollectionField }); let currentInputValue = transformVariableValue(targetValue, { targetCollectionField });

View File

@ -22,6 +22,7 @@ import { linkageAction } from '../../schema-component/antd/action/utils';
import { usePopupUtils } from '../../schema-component/antd/page/pagePopupUtils'; import { usePopupUtils } from '../../schema-component/antd/page/pagePopupUtils';
import { parseVariables } from '../../schema-component/common/utils/uitls'; import { parseVariables } from '../../schema-component/common/utils/uitls';
import { useLocalVariables, useVariables } from '../../variables'; import { useLocalVariables, useVariables } from '../../variables';
import { useApp } from '../../application';
export function useAclCheck(actionPath) { export function useAclCheck(actionPath) {
const aclCheck = useAclCheckFn(); const aclCheck = useAclCheckFn();
@ -73,6 +74,7 @@ const InternalCreateRecordAction = (props: any, ref) => {
const { openPopup } = usePopupUtils(); const { openPopup } = usePopupUtils();
const treeRecordData = useTreeParentRecord(); const treeRecordData = useTreeParentRecord();
const cm = useCollectionManager(); const cm = useCollectionManager();
const app = useApp();
useEffect(() => { useEffect(() => {
field.stateOfLinkageRules = {}; field.stateOfLinkageRules = {};
@ -80,13 +82,16 @@ const InternalCreateRecordAction = (props: any, ref) => {
.filter((k) => !k.disabled) .filter((k) => !k.disabled)
.forEach((v) => { .forEach((v) => {
v.actions?.forEach((h) => { v.actions?.forEach((h) => {
linkageAction({ linkageAction(
{
operator: h.operator, operator: h.operator,
field, field,
condition: v.condition, condition: v.condition,
variables, variables,
localVariables, localVariables,
}); },
app.jsonLogic,
);
}); });
}); });
}, [field, linkageRules, localVariables, variables]); }, [field, linkageRules, localVariables, variables]);
@ -143,7 +148,6 @@ export const CreateAction = observer(
const form = useForm(); const form = useForm();
const variables = useVariables(); const variables = useVariables();
const aclCheck = useAclCheckFn(); const aclCheck = useAclCheckFn();
const enableChildren = fieldSchema['x-enable-children'] || []; const enableChildren = fieldSchema['x-enable-children'] || [];
const allowAddToCurrent = fieldSchema?.['x-allow-add-to-current']; const allowAddToCurrent = fieldSchema?.['x-allow-add-to-current'];
const linkageFromForm = fieldSchema?.['x-component-props']?.['linkageFromForm']; const linkageFromForm = fieldSchema?.['x-component-props']?.['linkageFromForm'];
@ -176,6 +180,7 @@ export const CreateAction = observer(
const compile = useCompile(); const compile = useCompile();
const { designable } = useDesignable(); const { designable } = useDesignable();
const icon = props.icon || null; const icon = props.icon || null;
const app = useApp();
const menuItems = useMemo<MenuProps['items']>(() => { const menuItems = useMemo<MenuProps['items']>(() => {
return inheritsCollections.map((option) => ({ return inheritsCollections.map((option) => ({
key: option.name, key: option.name,
@ -196,13 +201,16 @@ export const CreateAction = observer(
.filter((k) => !k.disabled) .filter((k) => !k.disabled)
.forEach((v) => { .forEach((v) => {
v.actions?.forEach((h) => { v.actions?.forEach((h) => {
linkageAction({ linkageAction(
{
operator: h.operator, operator: h.operator,
field, field,
condition: v.condition, condition: v.condition,
variables, variables,
localVariables, localVariables,
}); },
app.jsonLogic,
);
}); });
}); });
}, [field, linkageRules, localVariables, variables]); }, [field, linkageRules, localVariables, variables]);

View File

@ -20,7 +20,7 @@ import { uid } from '@nocobase/utils/client';
import React, { useMemo, useState } from 'react'; import React, { useMemo, useState } from 'react';
export function ModalActionSchemaInitializerItem(props) { export function ModalActionSchemaInitializerItem(props) {
const { modalSchema = {}, ...otherProps } = props; const { modalSchema = {}, components = {}, ...otherProps } = props;
const { properties, ...others } = modalSchema; const { properties, ...others } = modalSchema;
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const { setVisible: setSchemaInitializerVisible } = useSchemaInitializer(); const { setVisible: setSchemaInitializerVisible } = useSchemaInitializer();
@ -92,7 +92,7 @@ export function ModalActionSchemaInitializerItem(props) {
}} }}
/> />
<ActionContextProvider value={{ visible, setVisible }}> <ActionContextProvider value={{ visible, setVisible }}>
<SchemaComponent components={{ Action }} schema={schema} /> <SchemaComponent components={{ Action, ...components }} schema={schema} />
</ActionContextProvider> </ActionContextProvider>
</> </>
); );

View File

@ -30,3 +30,4 @@ export * from './RecordReadPrettyAssociationFormBlockInitializer';
export * from './SelectActionInitializer'; export * from './SelectActionInitializer';
export * from './SubmitActionInitializer'; export * from './SubmitActionInitializer';
export * from './TableActionColumnInitializer'; export * from './TableActionColumnInitializer';
export * from './ModalActionSchemaInitializerItem';

View File

@ -39,7 +39,8 @@ interface Props {
variableNameOfLeftCondition?: string; variableNameOfLeftCondition?: string;
} }
export function bindLinkageRulesToFiled({ export function bindLinkageRulesToFiled(
{
field, field,
linkageRules, linkageRules,
formValues, formValues,
@ -61,7 +62,9 @@ export function bindLinkageRulesToFiled({
* @default '$nForm' * @default '$nForm'
*/ */
variableNameOfLeftCondition?: string; variableNameOfLeftCondition?: string;
}) { },
jsonLogic: any,
) {
field['initStateOfLinkageRules'] = { field['initStateOfLinkageRules'] = {
display: field.initStateOfLinkageRules?.display || getTempFieldState(true, field.display), display: field.initStateOfLinkageRules?.display || getTempFieldState(true, field.display),
required: field.initStateOfLinkageRules?.required || getTempFieldState(true, field.required || false), required: field.initStateOfLinkageRules?.required || getTempFieldState(true, field.required || false),
@ -89,7 +92,7 @@ export function bindLinkageRulesToFiled({
.join(','); .join(',');
return result; return result;
}, },
getSubscriber({ action, field, rule, variables, localVariables, variableNameOfLeftCondition }), getSubscriber({ action, field, rule, variables, localVariables, variableNameOfLeftCondition }, jsonLogic),
{ fireImmediately: true, equals: _.isEqual }, { fireImmediately: true, equals: _.isEqual },
); );
} }
@ -176,7 +179,8 @@ function getVariableValue(variableString: string, localVariables: VariableOption
return getValuesByPath(ctx, getPath(variableString)); return getValuesByPath(ctx, getPath(variableString));
} }
function getSubscriber({ function getSubscriber(
{
action, action,
field, field,
rule, rule,
@ -194,10 +198,13 @@ function getSubscriber({
* @default '$nForm' * @default '$nForm'
*/ */
variableNameOfLeftCondition?: string; variableNameOfLeftCondition?: string;
}): (value: string, oldValue: string) => void { },
jsonLogic,
): (value: string, oldValue: string) => void {
return () => { return () => {
// 当条件改变触发 reaction 时,会同步收集字段状态,并保存到 field.stateOfLinkageRules 中 // 当条件改变触发 reaction 时,会同步收集字段状态,并保存到 field.stateOfLinkageRules 中
collectFieldStateOfLinkageRules({ collectFieldStateOfLinkageRules(
{
operator: action.operator, operator: action.operator,
value: action.value, value: action.value,
field, field,
@ -205,7 +212,9 @@ function getSubscriber({
variables, variables,
localVariables, localVariables,
variableNameOfLeftCondition, variableNameOfLeftCondition,
}); },
jsonLogic,
);
// 当条件改变时,有可能会触发多个 reaction所以这里需要延迟一下确保所有的 reaction 都执行完毕后, // 当条件改变时,有可能会触发多个 reaction所以这里需要延迟一下确保所有的 reaction 都执行完毕后,
// 再从 field.stateOfLinkageRules 中取值,因为此时 field.stateOfLinkageRules 中的值才是全的。 // 再从 field.stateOfLinkageRules 中取值,因为此时 field.stateOfLinkageRules 中的值才是全的。
@ -286,15 +295,10 @@ function getFieldNameByOperator(operator: ActionType) {
} }
} }
export const collectFieldStateOfLinkageRules = ({ export const collectFieldStateOfLinkageRules = (
operator, { operator, value, field, condition, variables, localVariables, variableNameOfLeftCondition }: Props,
value, jsonLogic: any,
field, ) => {
condition,
variables,
localVariables,
variableNameOfLeftCondition,
}: Props) => {
const requiredResult = field?.stateOfLinkageRules?.required || [field?.initStateOfLinkageRules?.required]; const requiredResult = field?.stateOfLinkageRules?.required || [field?.initStateOfLinkageRules?.required];
const displayResult = field?.stateOfLinkageRules?.display || [field?.initStateOfLinkageRules?.display]; const displayResult = field?.stateOfLinkageRules?.display || [field?.initStateOfLinkageRules?.display];
const patternResult = field?.stateOfLinkageRules?.pattern || [field?.initStateOfLinkageRules?.pattern]; const patternResult = field?.stateOfLinkageRules?.pattern || [field?.initStateOfLinkageRules?.pattern];
@ -304,14 +308,14 @@ export const collectFieldStateOfLinkageRules = ({
switch (operator) { switch (operator) {
case ActionType.Required: case ActionType.Required:
requiredResult.push(getTempFieldState(conditionAnalyses(paramsToGetConditionResult), true)); requiredResult.push(getTempFieldState(conditionAnalyses(paramsToGetConditionResult, jsonLogic), true));
field.stateOfLinkageRules = { field.stateOfLinkageRules = {
...field.stateOfLinkageRules, ...field.stateOfLinkageRules,
required: requiredResult, required: requiredResult,
}; };
break; break;
case ActionType.InRequired: case ActionType.InRequired:
requiredResult.push(getTempFieldState(conditionAnalyses(paramsToGetConditionResult), false)); requiredResult.push(getTempFieldState(conditionAnalyses(paramsToGetConditionResult, jsonLogic), false));
field.stateOfLinkageRules = { field.stateOfLinkageRules = {
...field.stateOfLinkageRules, ...field.stateOfLinkageRules,
required: requiredResult, required: requiredResult,
@ -320,7 +324,7 @@ export const collectFieldStateOfLinkageRules = ({
case ActionType.Visible: case ActionType.Visible:
case ActionType.None: case ActionType.None:
case ActionType.Hidden: case ActionType.Hidden:
displayResult.push(getTempFieldState(conditionAnalyses(paramsToGetConditionResult), operator)); displayResult.push(getTempFieldState(conditionAnalyses(paramsToGetConditionResult, jsonLogic), operator));
field.stateOfLinkageRules = { field.stateOfLinkageRules = {
...field.stateOfLinkageRules, ...field.stateOfLinkageRules,
display: displayResult, display: displayResult,
@ -329,7 +333,7 @@ export const collectFieldStateOfLinkageRules = ({
case ActionType.Editable: case ActionType.Editable:
case ActionType.ReadOnly: case ActionType.ReadOnly:
case ActionType.ReadPretty: case ActionType.ReadPretty:
patternResult.push(getTempFieldState(conditionAnalyses(paramsToGetConditionResult), operator)); patternResult.push(getTempFieldState(conditionAnalyses(paramsToGetConditionResult, jsonLogic), operator));
field.stateOfLinkageRules = { field.stateOfLinkageRules = {
...field.stateOfLinkageRules, ...field.stateOfLinkageRules,
pattern: patternResult, pattern: patternResult,
@ -364,7 +368,7 @@ export const collectFieldStateOfLinkageRules = ({
if (isConditionEmpty(condition)) { if (isConditionEmpty(condition)) {
valueResult.push(getTempFieldState(true, getValue())); valueResult.push(getTempFieldState(true, getValue()));
} else { } else {
valueResult.push(getTempFieldState(conditionAnalyses(paramsToGetConditionResult), getValue())); valueResult.push(getTempFieldState(conditionAnalyses(paramsToGetConditionResult, jsonLogic), getValue()));
} }
field.stateOfLinkageRules = { field.stateOfLinkageRules = {
...field.stateOfLinkageRules, ...field.stateOfLinkageRules,

View File

@ -25,13 +25,13 @@ const getActionValue = (operator, value) => {
} }
}; };
const getSatisfiedActions = async ({ rules, variables, localVariables }) => { const getSatisfiedActions = async ({ rules, variables, localVariables }, jsonLogic) => {
const satisfiedRules = ( const satisfiedRules = (
await Promise.all( await Promise.all(
rules rules
.filter((k) => !k.disabled) .filter((k) => !k.disabled)
.map(async (rule) => { .map(async (rule) => {
if (await conditionAnalyses({ ruleGroup: rule.condition, variables, localVariables })) { if (await conditionAnalyses({ ruleGroup: rule.condition, variables, localVariables }, jsonLogic)) {
return rule; return rule;
} else return null; } else return null;
}), }),
@ -40,15 +40,15 @@ const getSatisfiedActions = async ({ rules, variables, localVariables }) => {
return satisfiedRules.map((rule) => rule.actions).flat(); return satisfiedRules.map((rule) => rule.actions).flat();
}; };
const getSatisfiedValues = async ({ rules, variables, localVariables }) => { const getSatisfiedValues = async ({ rules, variables, localVariables }, jsonLogic) => {
return (await getSatisfiedActions({ rules, variables, localVariables })).map((action) => ({ return (await getSatisfiedActions({ rules, variables, localVariables }, jsonLogic)).map((action) => ({
...action, ...action,
value: getActionValue(action.operator, action.value), value: getActionValue(action.operator, action.value),
})); }));
}; };
export const getSatisfiedValueMap = async ({ rules, variables, localVariables }) => { export const getSatisfiedValueMap = async ({ rules, variables, localVariables }, jsonLogic) => {
const values = await getSatisfiedValues({ rules, variables, localVariables }); const values = await getSatisfiedValues({ rules, variables, localVariables }, jsonLogic);
const valueMap = values.reduce((a, v) => ({ ...a, [v.operator]: v.value }), {}); const valueMap = values.reduce((a, v) => ({ ...a, [v.operator]: v.value }), {});
return valueMap; return valueMap;
}; };

View File

@ -15,7 +15,7 @@ import React, { useCallback, useEffect, useState } from 'react';
import { useLocalVariables, useVariables } from '../../variables'; import { useLocalVariables, useVariables } from '../../variables';
import { getSatisfiedValueMap } from './compute-rules'; import { getSatisfiedValueMap } from './compute-rules';
import { LinkageRuleCategory, LinkageRuleDataKeyMap } from './type'; import { LinkageRuleCategory, LinkageRuleDataKeyMap } from './type';
import { useApp } from '../../application';
export function useSatisfiedActionValues({ export function useSatisfiedActionValues({
formValues, formValues,
category = 'default', category = 'default',
@ -35,10 +35,11 @@ export function useSatisfiedActionValues({
const localVariables = useLocalVariables({ currentForm: { values: formValues } as any }); const localVariables = useLocalVariables({ currentForm: { values: formValues } as any });
const localSchema = schema ?? fieldSchema; const localSchema = schema ?? fieldSchema;
const styleRules = rules ?? localSchema[LinkageRuleDataKeyMap[category]]; const styleRules = rules ?? localSchema[LinkageRuleDataKeyMap[category]];
const app = useApp();
const compute = useCallback(() => { const compute = useCallback(() => {
if (styleRules && formValues) { if (styleRules && formValues) {
getSatisfiedValueMap({ rules: styleRules, variables, localVariables }) getSatisfiedValueMap({ rules: styleRules, variables, localVariables }, app.jsonLogic)
.then((valueMap) => { .then((valueMap) => {
if (!isEmpty(valueMap)) { if (!isEmpty(valueMap)) {
setValueMap(valueMap); setValueMap(valueMap);

View File

@ -19,7 +19,6 @@ import {
useOpenModeContext, useOpenModeContext,
useSchemaToolbar, useSchemaToolbar,
SecondConFirm, SecondConFirm,
WorkflowConfig,
AfterSuccess, AfterSuccess,
RefreshDataBlockRequest, RefreshDataBlockRequest,
} from '@nocobase/client'; } from '@nocobase/client';
@ -188,14 +187,6 @@ export const bulkEditFormSubmitActionSettings = new SchemaSettings({
name: 'secondConfirmation', name: 'secondConfirmation',
Component: SecondConFirm, Component: SecondConFirm,
}, },
{
name: 'workflowConfig',
Component: WorkflowConfig,
useVisible() {
const fieldSchema = useFieldSchema();
return isValid(fieldSchema?.['x-action-settings']?.triggerWorkflows);
},
},
{ {
name: 'afterSuccessfulSubmission', name: 'afterSuccessfulSubmission',
Component: AfterSuccess, Component: AfterSuccess,

View File

@ -25,7 +25,7 @@ test.describe('ReadPrettyFormActionInitializers & CalendarFormActionInitializers
const nocoPage = await mockPage(oneCalenderWithViewAction).waitForInit(); const nocoPage = await mockPage(oneCalenderWithViewAction).waitForInit();
await mockRecord('general', { singleLineText: 'test' }); await mockRecord('general', { singleLineText: 'test' });
await nocoPage.goto(); 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.getByLabel('schema-initializer-ActionBar-details:configureActions-general').hover();
await page.getByRole('menuitem', { name: 'Print' }).click(); await page.getByRole('menuitem', { name: 'Print' }).click();
await page.getByLabel('action-Action-Print-print-general-form').click(); await page.getByLabel('action-Action-Print-print-general-form').click();

View File

@ -8,7 +8,7 @@
*/ */
import { useFieldSchema } from '@formily/react'; 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 { Avatar } from 'antd';
import { createStyles } from 'antd-style'; import { createStyles } from 'antd-style';
import React, { useContext } from 'react'; import React, { useContext } from 'react';
@ -36,13 +36,15 @@ function Button() {
const backgroundColor = fieldSchema['x-component-props']?.['iconColor']; const backgroundColor = fieldSchema['x-component-props']?.['iconColor'];
const { layout } = useContext(WorkbenchBlockContext); const { layout } = useContext(WorkbenchBlockContext);
const { styles, cx } = useStyles(); const { styles, cx } = useStyles();
const compile = useCompile();
const title = compile(fieldSchema.title);
return layout === WorkbenchLayout.Grid ? ( return layout === WorkbenchLayout.Grid ? (
<div title={fieldSchema.title} style={{ width: '100%', overflow: 'hidden' }} className="nb-action-panel-container"> <div title={title} style={{ width: '100%', overflow: 'hidden' }} className="nb-action-panel-container">
<Avatar style={{ backgroundColor }} size={54} icon={<Icon type={icon} />} /> <Avatar style={{ backgroundColor }} size={54} icon={<Icon type={icon} />} />
<div className={cx(styles.title)}>{fieldSchema.title}</div> <div className={cx(styles.title)}>{title}</div>
</div> </div>
) : ( ) : (
<span>{fieldSchema.title}</span> <span>{title}</span>
); );
} }

View File

@ -7,10 +7,15 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * 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 React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { ModalActionSchemaInitializerItem } from './ModalActionSchemaInitializerItem';
export const workbenchActionSettingsCustomRequest = new SchemaSettings({ export const workbenchActionSettingsCustomRequest = new SchemaSettings({
name: 'workbench:actionSettings:customRequest', name: 'workbench:actionSettings:customRequest',
items: [ items: [

View File

@ -13,10 +13,10 @@ import {
SchemaSettingsActionLinkItem, SchemaSettingsActionLinkItem,
useSchemaInitializer, useSchemaInitializer,
useSchemaInitializerItem, useSchemaInitializerItem,
ModalActionSchemaInitializerItem,
} from '@nocobase/client'; } from '@nocobase/client';
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { ModalActionSchemaInitializerItem } from './ModalActionSchemaInitializerItem';
export const workbenchActionSettingsLink = new SchemaSettings({ export const workbenchActionSettingsLink = new SchemaSettings({
name: 'workbench:actionSettings:link', name: 'workbench:actionSettings:link',

View File

@ -13,10 +13,10 @@ import {
SchemaSettings, SchemaSettings,
useSchemaInitializer, useSchemaInitializer,
useOpenModeContext, useOpenModeContext,
ModalActionSchemaInitializerItem,
} from '@nocobase/client'; } from '@nocobase/client';
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { ModalActionSchemaInitializerItem } from './ModalActionSchemaInitializerItem';
export const workbenchActionSettingsPopup = new SchemaSettings({ export const workbenchActionSettingsPopup = new SchemaSettings({
name: 'workbench:actionSettings:popup', name: 'workbench:actionSettings:popup',

View File

@ -13,10 +13,10 @@ import {
SchemaSettings, SchemaSettings,
useSchemaInitializer, useSchemaInitializer,
useSchemaInitializerItem, useSchemaInitializerItem,
ModalActionSchemaInitializerItem,
} from '@nocobase/client'; } from '@nocobase/client';
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { ModalActionSchemaInitializerItem } from './ModalActionSchemaInitializerItem';
export const workbenchActionSettingsScanQrCode = new SchemaSettings({ export const workbenchActionSettingsScanQrCode = new SchemaSettings({
name: 'workbench:actionSettings:scanQrCode', name: 'workbench:actionSettings:scanQrCode',

View File

@ -10,7 +10,7 @@
import { expect, test } from '@nocobase/test/e2e'; import { expect, test } from '@nocobase/test/e2e';
import { backgroundColorFieldBasic } from './templates'; import { backgroundColorFieldBasic } from './templates';
test.describe('Background color field', () => { test.describe('Color field', () => {
test('basic', async ({ mockPage, mockRecords, page }) => { test('basic', async ({ mockPage, mockRecords, page }) => {
const nocoPage = await mockPage(backgroundColorFieldBasic).waitForInit(); const nocoPage = await mockPage(backgroundColorFieldBasic).waitForInit();
await mockRecords('calendar', 3); await mockRecords('calendar', 3);
@ -19,18 +19,22 @@ test.describe('Background color field', () => {
// 1. The default option is Not selected // 1. The default option is Not selected
await page.getByLabel('block-item-CardItem-calendar-').hover(); await page.getByLabel('block-item-CardItem-calendar-').hover();
await page.getByLabel('designer-schema-settings-CardItem-blockSettings:calendar-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 // 2. Switch to the single select option
await page.getByRole('option', { name: 'Single select' }).click(); 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); await page.mouse.move(-300, 0);
// 3. Switch to the radio group option // 3. Switch to the radio group option
await page.getByLabel('block-item-CardItem-calendar-').hover(); await page.getByLabel('block-item-CardItem-calendar-').hover();
await page.getByLabel('designer-schema-settings-CardItem-blockSettings:calendar-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 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();
}); });
}); });

View File

@ -30,10 +30,11 @@ import {
useToken, useToken,
withDynamicSchemaProps, withDynamicSchemaProps,
withSkeletonComponent, withSkeletonComponent,
useApp,
} from '@nocobase/client'; } from '@nocobase/client';
import type { Dayjs } from 'dayjs'; import type { Dayjs } from 'dayjs';
import 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 React, { useCallback, useEffect, useMemo, useState } from 'react';
import { View } from 'react-big-calendar'; import { View } from 'react-big-calendar';
import { i18nt, useTranslation } from '../../locale'; import { i18nt, useTranslation } from '../../locale';
@ -116,6 +117,8 @@ const useEvents = (
); );
const { t } = useTranslation(); const { t } = useTranslation();
const { fields } = useCollection(); 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 labelUiSchema = fields.find((v) => v.name === fieldNames?.title)?.uiSchema;
const enumUiSchema = fields.find((v) => v.name === fieldNames?.colorFieldName); const enumUiSchema = fields.find((v) => v.name === fieldNames?.colorFieldName);
return useMemo(() => { return useMemo(() => {
@ -164,7 +167,10 @@ const useEvents = (
}); });
if (res) return out; 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 = { const event: Event = {
id: get(item, fieldNames.id || 'id'), id: get(item, fieldNames.id || 'id'),
colorFieldValue: item[fieldNames.colorFieldName], colorFieldValue: item[fieldNames.colorFieldName],
@ -275,7 +281,7 @@ export const Calendar: any = withDynamicSchemaProps(
}, [reactBigCalendar]); }, [reactBigCalendar]);
// 新版 UISchema1.0 之后)中已经废弃了 useProps这里之所以继续保留是为了兼容旧版的 UISchema // 新版 UISchema1.0 之后)中已经废弃了 useProps这里之所以继续保留是为了兼容旧版的 UISchema
const { dataSource, fieldNames, showLunar, defaultView } = useProps(props); const { dataSource, fieldNames, showLunar, defaultView, getFontColor, getBackgroundColor } = useProps(props);
const height = useCalenderHeight(); const height = useCalenderHeight();
const [date, setDate] = useState<Date>(new Date()); const [date, setDate] = useState<Date>(new Date());
const [view, setView] = useState<View>(props.defaultView || 'month'); const [view, setView] = useState<View>(props.defaultView || 'month');
@ -285,7 +291,6 @@ export const Calendar: any = withDynamicSchemaProps(
const parentRecordData = useCollectionParentRecordData(); const parentRecordData = useCollectionParentRecordData();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const field = useField(); const field = useField();
const { token } = useToken();
//nint deal with slot select to show create popup //nint deal with slot select to show create popup
const { parseAction } = useACLRoleContext(); const { parseAction } = useACLRoleContext();
const collection = useCollection(); const collection = useCollection();
@ -296,6 +301,8 @@ export const Calendar: any = withDynamicSchemaProps(
const ctx = useActionContext(); const ctx = useActionContext();
const [visibleAddNewer, setVisibleAddNewer] = useState(false); const [visibleAddNewer, setVisibleAddNewer] = useState(false);
const [currentSelectDate, setCurrentSelectDate] = useState(undefined); const [currentSelectDate, setCurrentSelectDate] = useState(undefined);
const colorCollectionField = collection.getField(fieldNames.colorFieldName);
useEffect(() => { useEffect(() => {
setView(props.defaultView); setView(props.defaultView);
}, [props.defaultView]); }, [props.defaultView]);
@ -339,10 +346,17 @@ export const Calendar: any = withDynamicSchemaProps(
const eventPropGetter = (event: Event) => { const eventPropGetter = (event: Event) => {
if (event.colorFieldValue) { if (event.colorFieldValue) {
const fontColor = token[`${getColorString(event.colorFieldValue, enumList)}7`]; const fontColor = getFontColor?.(event.colorFieldValue);
const backgroundColor = token[`${getColorString(event.colorFieldValue, enumList)}1`]; const backgroundColor = getBackgroundColor?.(event.colorFieldValue);
const style = {};
if (fontColor) {
style['color'] = fontColor;
}
if (backgroundColor) {
style['backgroundColor'] = backgroundColor;
}
return { return {
style: { color: fontColor, backgroundColor, border: 'none' }, style,
}; };
} }
}; };
@ -435,7 +449,7 @@ export const Calendar: any = withDynamicSchemaProps(
return; return;
} }
record.__event = { record.__event = {
...event, ...omit(event, 'title'),
start: formatDate(dayjs(event.start)), start: formatDate(dayjs(event.start)),
end: formatDate(dayjs(event.end)), end: formatDate(dayjs(event.end)),
}; };

View File

@ -24,6 +24,7 @@ import {
useDesignable, useDesignable,
useFormBlockContext, useFormBlockContext,
usePopupSettings, usePopupSettings,
useApp,
} from '@nocobase/client'; } from '@nocobase/client';
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import { useTranslation } from '../../locale'; import { useTranslation } from '../../locale';
@ -73,14 +74,17 @@ export const calendarBlockSettings = new SchemaSettings({
const fieldNames = fieldSchema?.['x-decorator-props']?.['fieldNames'] || {}; const fieldNames = fieldSchema?.['x-decorator-props']?.['fieldNames'] || {};
const { service } = useCalendarBlockContext(); const { service } = useCalendarBlockContext();
const { getCollectionFieldsOptions } = useCollectionManager_deprecated(); 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 field = useField();
const { dn } = useDesignable(); const { dn } = useDesignable();
return { return {
title: t('Title field'), title: t('Title field'),
value: fieldNames.title, value: fieldNames.title,
options: getCollectionFieldsOptions(name, 'string'), options: getCollectionFieldsOptions(name, null, Object.keys(titleFieldInterfaces)),
onChange: (title) => { onChange: (title) => {
const fieldNames = field.decoratorProps.fieldNames || {}; const fieldNames = field.decoratorProps.fieldNames || {};
fieldNames['title'] = title; fieldNames['title'] = title;
@ -112,13 +116,14 @@ export const calendarBlockSettings = new SchemaSettings({
const { name } = useCollection(); const { name } = useCollection();
const field = useField(); const field = useField();
const { dn } = useDesignable(); const { dn } = useDesignable();
const fliedList = getCollectionFieldsOptions(name, 'string'); const app = useApp();
const filteredItems = [ const plugin = app.pm.get('calendar') as any;
{ label: t('Not selected'), value: '' }, const { colorFieldInterfaces } = plugin;
...fliedList.filter((item) => item.interface === 'radioGroup' || item.interface === 'select'), const fliedList = getCollectionFieldsOptions(name, null, Object.keys(colorFieldInterfaces));
]; const filteredItems = [{ label: t('Not selected'), value: '' }, ...fliedList];
return { return {
title: t('Background color field'), title: t('Color field'),
value: fieldNames.colorFieldName || '', value: fieldNames.colorFieldName || '',
options: filteredItems, options: filteredItems,
onChange: (colorFieldName: string) => { onChange: (colorFieldName: string) => {
@ -230,10 +235,13 @@ export const calendarBlockSettings = new SchemaSettings({
const { dn } = useDesignable(); const { dn } = useDesignable();
const { service } = useCalendarBlockContext(); const { service } = useCalendarBlockContext();
const { name } = useCollection(); const { name } = useCollection();
const app = useApp();
const plugin = app.pm.get('calendar') as any;
const { dateTimeFields } = plugin;
return { return {
title: t('Start date field'), title: t('Start date field'),
value: fieldNames.start, value: fieldNames.start,
options: getCollectionFieldsOptions(name, ['date', 'datetime', 'dateOnly', 'datetimeNoTz', 'unixTimestamp'], { options: getCollectionFieldsOptions(name, null, dateTimeFields, {
association: ['o2o', 'obo', 'oho', 'm2o'], association: ['o2o', 'obo', 'oho', 'm2o'],
}), }),
onChange: (start) => { onChange: (start) => {
@ -265,10 +273,13 @@ export const calendarBlockSettings = new SchemaSettings({
const { dn } = useDesignable(); const { dn } = useDesignable();
const { name } = useCollection(); const { name } = useCollection();
const fieldNames = fieldSchema?.['x-decorator-props']?.['fieldNames'] || {}; const fieldNames = fieldSchema?.['x-decorator-props']?.['fieldNames'] || {};
const app = useApp();
const plugin = app.pm.get('calendar') as any;
const { dateTimeFields } = plugin;
return { return {
title: t('End date field'), title: t('End date field'),
value: fieldNames.end, value: fieldNames.end,
options: getCollectionFieldsOptions(name, ['date', 'datetime', 'dateOnly', 'datetimeNoTz', 'unixTimestamp'], { options: getCollectionFieldsOptions(name, null, dateTimeFields, {
association: ['o2o', 'obo', 'oho', 'm2o'], association: ['o2o', 'obo', 'oho', 'm2o'],
}), }),
onChange: (end) => { onChange: (end) => {

View File

@ -6,8 +6,8 @@
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import React from 'react';
import { Plugin } from '@nocobase/client'; import { Plugin, useToken } from '@nocobase/client';
import { generateNTemplate } from '../locale'; import { generateNTemplate } from '../locale';
import { CalendarV2 } from './calendar'; import { CalendarV2 } from './calendar';
import { calendarBlockSettings } from './calendar/Calender.Settings'; import { calendarBlockSettings } from './calendar/Calender.Settings';
@ -27,7 +27,79 @@ import {
useCreateCalendarBlock, useCreateCalendarBlock,
} from './schema-initializer/items'; } from './schema-initializer/items';
const TitleRenderer = ({ value }) => {
return <span aria-label="event-title">{value || 'N/A'}</span>;
};
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 { export class PluginCalendarClient extends Plugin {
titleFieldInterfaces: { [T: string]: { TitleRenderer: React.FC<TitleRendererProps> } } = {
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<TitleRendererProps> }) {
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() { async load() {
this.app.dataSourceManager.addCollectionTemplates([CalendarCollectionTemplate]); this.app.dataSourceManager.addCollectionTemplates([CalendarCollectionTemplate]);
this.app.schemaInitializerManager.addItem('page:addBlock', 'dataBlocks.calendar', { this.app.schemaInitializerManager.addItem('page:addBlock', 'dataBlocks.calendar', {

View File

@ -9,8 +9,8 @@
import { ArrayField } from '@formily/core'; import { ArrayField } from '@formily/core';
import { useField, useFieldSchema } from '@formily/react'; import { useField, useFieldSchema } from '@formily/react';
import { BlockProvider, useBlockRequestContext, withDynamicSchemaProps } from '@nocobase/client'; import { BlockProvider, useBlockRequestContext, withDynamicSchemaProps, useApp, useCollection } from '@nocobase/client';
import React, { createContext, useContext, useEffect } from 'react'; import React, { createContext, useContext, useEffect, useState, useMemo, useRef } from 'react';
import { useCalendarBlockParams } from '../hooks/useCalendarBlockParams'; import { useCalendarBlockParams } from '../hooks/useCalendarBlockParams';
export const CalendarBlockContext = createContext<any>({}); export const CalendarBlockContext = createContext<any>({});
@ -57,11 +57,12 @@ export const CalendarBlockProvider = withDynamicSchemaProps(
if (parseVariableLoading) { if (parseVariableLoading) {
return null; return null;
} }
return ( return (
<div key={props.fieldNames.colorFieldName}>
<BlockProvider name="calendar" {...props} params={params}> <BlockProvider name="calendar" {...props} params={params}>
<InternalCalendarBlockProvider {...props} /> <InternalCalendarBlockProvider {...props} />
</BlockProvider> </BlockProvider>
</div>
); );
}, },
{ displayName: 'CalendarBlockProvider' }, { displayName: 'CalendarBlockProvider' },
@ -71,18 +72,40 @@ export const useCalendarBlockContext = () => {
return useContext(CalendarBlockContext); return useContext(CalendarBlockContext);
}; };
const useDefaultGetColor = () => {
return {
getFontColor(value) {
return null;
},
getBackgroundColor(value) {
return null;
},
};
};
export const useCalendarBlockProps = () => { export const useCalendarBlockProps = () => {
const ctx = useCalendarBlockContext(); const ctx = useCalendarBlockContext();
const field = useField<ArrayField>(); const field = useField<ArrayField>();
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(() => { useEffect(() => {
if (!ctx?.service?.loading) { if (!ctx?.service?.loading) {
field.componentProps.dataSource = ctx?.service?.data?.data; field.componentProps.dataSource = ctx?.service?.data?.data;
} }
}, [ctx?.service?.loading]); }, [ctx?.service?.loading]);
return { return {
fieldNames: ctx.fieldNames, fieldNames: ctx.fieldNames,
showLunar: ctx.showLunar, showLunar: ctx.showLunar,
defaultView: ctx.defaultView, defaultView: ctx.defaultView,
fixedBlock: ctx.fixedBlock, fixedBlock: ctx.fixedBlock,
getFontColor,
getBackgroundColor,
}; };
}; };

View File

@ -21,6 +21,8 @@ import {
useGlobalTheme, useGlobalTheme,
useSchemaInitializer, useSchemaInitializer,
useSchemaInitializerItem, useSchemaInitializerItem,
useApp,
useCompile,
} from '@nocobase/client'; } from '@nocobase/client';
import React, { useContext } from 'react'; import React, { useContext } from 'react';
import { useTranslation } from '../../../locale'; import { useTranslation } from '../../../locale';
@ -67,17 +69,23 @@ export const useCreateCalendarBlock = () => {
const { getCollectionField, getCollectionFieldsOptions } = useCollectionManager_deprecated(); const { getCollectionField, getCollectionFieldsOptions } = useCollectionManager_deprecated();
const options = useContext(SchemaOptionsContext); const options = useContext(SchemaOptionsContext);
const { theme } = useGlobalTheme(); const { theme } = useGlobalTheme();
const app = useApp();
const plugin = app.pm.get('calendar') as any;
const { titleFieldInterfaces, dateTimeFieldInterfaces } = plugin;
const createCalendarBlock = async ({ item }) => { const createCalendarBlock = async ({ item }) => {
const stringFieldsOptions = getCollectionFieldsOptions(item.name, 'string', { dataSource: item.dataSource }); const titleFieldsOptions = getCollectionFieldsOptions(
const dateFieldsOptions = getCollectionFieldsOptions(
item.name, item.name,
['date', 'datetime', 'dateOnly', 'datetimeNoTz', 'unixTimestamp'], null,
Object.keys(titleFieldInterfaces).map((v) => v || v),
{ {
association: ['o2o', 'obo', 'oho', 'm2o'],
dataSource: item.dataSource, dataSource: item.dataSource,
}, },
); );
const dateFieldsOptions = getCollectionFieldsOptions(item.name, null, dateTimeFieldInterfaces, {
association: ['o2o', 'obo', 'oho', 'm2o'],
dataSource: item.dataSource,
});
const values = await FormDialog( const values = await FormDialog(
t('Create calendar block'), t('Create calendar block'),
@ -90,7 +98,7 @@ export const useCreateCalendarBlock = () => {
properties: { properties: {
title: { title: {
title: t('Title field'), title: t('Title field'),
enum: stringFieldsOptions, enum: titleFieldsOptions,
required: true, required: true,
'x-component': 'Select', 'x-component': 'Select',
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',

View File

@ -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", "Configure calendar": "Configure calendar",
"Title field": "Title field", "Title field": "Title field",
"Custom title": "Custom title", "Custom title": "Custom title",
@ -60,6 +42,6 @@ export default {
"Monthly": "Monthly", "Monthly": "Monthly",
"Yearly": "Yearly", "Yearly": "Yearly",
"Repeats": "Repeats", "Repeats": "Repeats",
"Background color field": "Background color field", "Color field": "Color field",
"Not selected": "Not selected", "Not selected": "Not selected"
}; }

View File

@ -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", "Configure calendar": "Configurar calendario",
"Title field": "Campo de título", "Title field": "Campo de título",
"Custom title": "Título personalizado", "Custom title": "Título personalizado",
@ -60,6 +42,6 @@ export default {
"Monthly": "Mensual", "Monthly": "Mensual",
"Yearly": "Anual", "Yearly": "Anual",
"Repeats": "se repite", "Repeats": "se repite",
"Background color field": "Campo de color de fondo", "Color field": "Campo de color",
"Not selected": "No seleccionado", "Not selected": "No seleccionado"
}; }

View File

@ -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", "Configure calendar": "Configurer le calendrier",
"Title field": "Champ de titre", "Title field": "Champ de titre",
"Custom title": "Titre personnalisé", "Custom title": "Titre personnalisé",
@ -51,4 +42,4 @@ export default {
"Monthly": "Mensuel", "Monthly": "Mensuel",
"Yearly": "Annuel", "Yearly": "Annuel",
"Repeats": "Répétitions" "Repeats": "Répétitions"
}; }

View File

@ -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": "タイトルフィールド",
"Start date field": "開始日フィールド", "Start date field": "開始日フィールド",
@ -61,5 +43,5 @@ export default {
"Monthly": "毎月", "Monthly": "毎月",
"Yearly": "毎年", "Yearly": "毎年",
"Repeats": "繰り返し", "Repeats": "繰り返し",
"Update record": "レコードを更新する", "Update record": "レコードを更新する"
}; }

View File

@ -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": "캘린더 구성", "Configure calendar": "캘린더 구성",
"Title field": "제목 필드", "Title field": "제목 필드",
"Custom title": "사용자 정의 제목", "Custom title": "사용자 정의 제목",
@ -52,4 +43,4 @@ export default {
"Monthly": "매월", "Monthly": "매월",
"Yearly": "매년", "Yearly": "매년",
"Repeats": "반복" "Repeats": "반복"
}; }

View File

@ -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", "Configure calendar": "Configurar calendário",
"Title field": "Campo de título", "Title field": "Campo de título",
"Custom title": "Título personalizado", "Custom title": "Título personalizado",

View File

@ -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": "Настроить календарь", "Configure calendar": "Настроить календарь",
"Title field": "Поле заголовка", "Title field": "Поле заголовка",
"Start date field": "Поле даты начала", "Start date field": "Поле даты начала",

View File

@ -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", "Configure calendar": "Takvimi yapılandır",
"Title field": "Başlık alanı", "Title field": "Başlık alanı",
"Start date field": "Başlangıç tarihi alanı", "Start date field": "Başlangıç tarihi alanı",

View File

@ -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": "Налаштувати календар", "Configure calendar": "Налаштувати календар",
"Title field": "Поле заголовка", "Title field": "Поле заголовка",
"Custom title": "Власний заголовок", "Custom title": "Власний заголовок",
@ -51,4 +42,4 @@ export default {
"Monthly": "Щомісяця", "Monthly": "Щомісяця",
"Yearly": "Щороку", "Yearly": "Щороку",
"Repeats": "Повторюється" "Repeats": "Повторюється"
}; }

View File

@ -47,7 +47,7 @@
"Month": "月", "Month": "月",
"Week": "周", "Week": "周",
"{{count}} more items": "{{count}} 更多事项", "{{count}} more items": "{{count}} 更多事项",
"Background color field": "背景颜色字段", "Color field": "颜色字段",
"Not selected": "未选择", "Not selected": "未选择",
"Default view": "默认视图", "Default view": "默认视图",
"Event open mode": "事项打开方式" "Event open mode": "事项打开方式"

View File

@ -27,10 +27,6 @@ test.describe('create collection with preset fields', () => {
//默认提交的数据符合预期 //默认提交的数据符合预期
expect(postData).toMatchObject({ expect(postData).toMatchObject({
autoGenId: false, autoGenId: false,
createdAt: true,
createdBy: true,
updatedAt: true,
updatedBy: true,
fields: expect.arrayContaining([ fields: expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
name: 'id', name: 'id',
@ -79,10 +75,6 @@ test.describe('create collection with preset fields', () => {
//提交的数据符合预期 //提交的数据符合预期
expect(postData).toMatchObject({ expect(postData).toMatchObject({
autoGenId: false, autoGenId: false,
createdAt: false,
createdBy: false,
updatedAt: false,
updatedBy: false,
fields: expect.arrayContaining([ fields: expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
name: 'id', name: 'id',
@ -110,10 +102,6 @@ test.describe('create collection with preset fields', () => {
//提交的数据符合预期 //提交的数据符合预期
expect(postData).toMatchObject({ expect(postData).toMatchObject({
autoGenId: false, autoGenId: false,
createdAt: true,
createdBy: false,
updatedAt: false,
updatedBy: false,
fields: expect.arrayContaining([ fields: expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
name: 'createdAt', name: 'createdAt',
@ -141,10 +129,6 @@ test.describe('create collection with preset fields', () => {
//提交的数据符合预期 //提交的数据符合预期
expect(postData).toMatchObject({ expect(postData).toMatchObject({
autoGenId: false, autoGenId: false,
createdAt: false,
createdBy: true,
updatedAt: false,
updatedBy: false,
fields: expect.arrayContaining([ fields: expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
name: 'createdBy', name: 'createdBy',
@ -172,10 +156,6 @@ test.describe('create collection with preset fields', () => {
//提交的数据符合预期 //提交的数据符合预期
expect(postData).toMatchObject({ expect(postData).toMatchObject({
autoGenId: false, autoGenId: false,
createdAt: false,
createdBy: false,
updatedAt: false,
updatedBy: true,
fields: expect.arrayContaining([ fields: expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
name: 'updatedBy', name: 'updatedBy',
@ -203,10 +183,6 @@ test.describe('create collection with preset fields', () => {
//提交的数据符合预期 //提交的数据符合预期
expect(postData).toMatchObject({ expect(postData).toMatchObject({
autoGenId: false, autoGenId: false,
createdAt: false,
createdBy: false,
updatedAt: true,
updatedBy: false,
fields: expect.arrayContaining([ fields: expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
name: 'updatedAt', name: 'updatedAt',
@ -233,10 +209,6 @@ test.describe('create collection with preset fields', () => {
//提交的数据符合预期 //提交的数据符合预期
expect(postData).toMatchObject({ expect(postData).toMatchObject({
autoGenId: false, autoGenId: false,
createdAt: false,
createdBy: false,
updatedAt: false,
updatedBy: false,
fields: [], fields: [],
}); });
}); });

View File

@ -8,9 +8,131 @@
*/ */
import { Plugin } from '@nocobase/client'; 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<string, any>;
field?: string;
[T: string]: any;
};
};
class PluginDataSourceMainClient extends Plugin { 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; export default PluginDataSourceMainClient;

View File

@ -20,6 +20,7 @@ describe('collections repository', () => {
await agent.resource('collections').create({ await agent.resource('collections').create({
values: { values: {
name: 'tags', name: 'tags',
createdAt: true,
fields: [ fields: [
{ {
name: 'title', name: 'title',
@ -31,6 +32,7 @@ describe('collections repository', () => {
await agent.resource('collections').create({ await agent.resource('collections').create({
values: { values: {
name: 'foos', name: 'foos',
createdAt: true,
fields: [ fields: [
{ {
name: 'title', name: 'title',
@ -49,6 +51,7 @@ describe('collections repository', () => {
await agent.resource('collections').create({ await agent.resource('collections').create({
values: { values: {
name: 'comments', name: 'comments',
createdAt: true,
fields: [ fields: [
{ {
name: 'title', name: 'title',
@ -60,6 +63,7 @@ describe('collections repository', () => {
await agent.resource('collections').create({ await agent.resource('collections').create({
values: { values: {
name: 'posts', name: 'posts',
createdAt: true,
fields: [ fields: [
{ {
name: 'title', name: 'title',
@ -329,16 +333,17 @@ describe('collections repository', () => {
}, },
}); });
const postId = response.body.data.id; const postId = response.body.data.id;
const response1 = await agent.resource('posts.tags', postId).list({ // const response1 = await agent.resource('posts.tags', postId).list({
appends: ['foos'], // appends: ['foos'],
page: 1, // page: 1,
pageSize: 20, // pageSize: 20,
sort: ['-createdAt', '-id'], // 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 () => { it('case 11', async () => {
@ -486,6 +491,7 @@ describe('collections repository', () => {
.create({ .create({
values: { values: {
name: 'test', name: 'test',
createdAt: true,
}, },
}); });
@ -518,6 +524,7 @@ describe('collections repository', () => {
.create({ .create({
values: { values: {
name: 'test', name: 'test',
createdAt: true,
fields: [ fields: [
{ {
name: 'testField', name: 'testField',

View File

@ -377,7 +377,24 @@ export class PluginDataSourceMainServer extends Plugin {
} }
await next(); 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', 'list', 'loggedIn');
this.app.acl.allow('collections', 'listMeta', 'loggedIn'); this.app.acl.allow('collections', 'listMeta', 'loggedIn');
this.app.acl.allow('collectionCategories', 'list', 'loggedIn'); this.app.acl.allow('collectionCategories', 'list', 'loggedIn');

View File

@ -8,14 +8,17 @@
*/ */
import React from 'react'; 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) => { 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 compile = useCompile();
const { interfaces } = useCollectionManager_deprecated(); 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) => ({ const options = fields.map((field) => ({
label: compile(field.uiSchema.title), label: compile(field.uiSchema.title),

View File

@ -9,11 +9,38 @@
import { Plugin } from '@nocobase/client'; import { Plugin } from '@nocobase/client';
import { Formula } from './components'; import { Formula } from './components';
import { renderExpressionDescription } from './scopes';
import { FormulaFieldInterface } from './interfaces/formula';
import { FormulaComponentFieldSettings } from './FormulaComponentFieldSettings'; import { FormulaComponentFieldSettings } from './FormulaComponentFieldSettings';
import { FormulaFieldInterface } from './interfaces/formula';
import { renderExpressionDescription } from './scopes';
export class PluginFieldFormulaClient extends Plugin { 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() { async load() {
this.app.addComponents({ this.app.addComponents({
Formula, Formula,

View File

@ -150,31 +150,6 @@ export class FormulaFieldInterface extends CollectionFieldInterface {
'x-component': 'Formula.Expression', 'x-component': 'Formula.Expression',
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component-props': { 'x-component-props': {
supports: [
'checkbox',
'number',
'percent',
'integer',
'number',
'percent',
'input',
'textarea',
'email',
'phone',
'datetime',
'createdAt',
'updatedAt',
'radioGroup',
'checkboxGroup',
'select',
'multipleSelect',
// 'json'
],
useCurrentFields: '{{ useCurrentFields }}', useCurrentFields: '{{ useCurrentFields }}',
// evaluate(exp: string) { // evaluate(exp: string) {
// const { values } = useForm(); // const { values } = useForm();

View File

@ -121,9 +121,11 @@ const RuleTypes = {
number: t('Number', { ns: NAMESPACE }), number: t('Number', { ns: NAMESPACE }),
lowercase: t('Lowercase letters', { ns: NAMESPACE }), lowercase: t('Lowercase letters', { ns: NAMESPACE }),
uppercase: t('Uppercase letters', { ns: NAMESPACE }), uppercase: t('Uppercase letters', { ns: NAMESPACE }),
symbol: t('Symbols', { ns: NAMESPACE }) symbol: t('Symbols', { ns: NAMESPACE }),
}; };
return <code>{value?.map(charset => charsetLabels[charset]).join(', ') || t('Number', { ns: NAMESPACE })}</code>; return (
<code>{value?.map((charset) => charsetLabels[charset]).join(', ') || t('Number', { ns: NAMESPACE })}</code>
);
}, },
}, },
fieldset: { fieldset: {
@ -154,14 +156,14 @@ const RuleTypes = {
{ value: 'number', label: `{{t("Number", { ns: "${NAMESPACE}" })}}` }, { value: 'number', label: `{{t("Number", { ns: "${NAMESPACE}" })}}` },
{ value: 'lowercase', label: `{{t("Lowercase letters", { ns: "${NAMESPACE}" })}}` }, { value: 'lowercase', label: `{{t("Lowercase letters", { ns: "${NAMESPACE}" })}}` },
{ value: 'uppercase', label: `{{t("Uppercase 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, required: true,
default: ['number'], default: ['number'],
'x-validator': { 'x-validator': {
minItems: 1, 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: { defaults: {

View File

@ -301,7 +301,7 @@ const CHAR_SETS = {
lowercase: 'abcdefghijklmnopqrstuvwxyz', lowercase: 'abcdefghijklmnopqrstuvwxyz',
uppercase: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', uppercase: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
// 符号只保留常用且安全的符号,有需要的可以自己加比如[]{}|;:,.<>放在链接或者文件名里容易出问题的字符 // 符号只保留常用且安全的符号,有需要的可以自己加比如[]{}|;:,.<>放在链接或者文件名里容易出问题的字符
symbol: '!@#$%^&*_-+' symbol: '!@#$%^&*_-+',
} as const; } as const;
interface RandomCharOptions { interface RandomCharOptions {
@ -317,21 +317,16 @@ sequencePatterns.register('randomChar', {
if (!options?.charsets || options.charsets.length === 0) { if (!options?.charsets || options.charsets.length === 0) {
return 'At least one character set should be selected'; 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 'Invalid charset selected';
} }
return null; return null;
}, },
generate(instance: any, options: RandomCharOptions) { generate(instance: any, options: RandomCharOptions) {
const { const { length = 6, charsets = ['number'] } = options;
length = 6,
charsets = ['number']
} = options;
const chars = [...new Set( const chars = [...new Set(charsets.reduce((acc, charset) => acc + CHAR_SETS[charset], ''))];
charsets.reduce((acc, charset) => acc + CHAR_SETS[charset], '')
)];
const getRandomChar = () => { const getRandomChar = () => {
const randomIndex = Math.floor(Math.random() * chars.length); const randomIndex = Math.floor(Math.random() * chars.length);
@ -352,20 +347,27 @@ sequencePatterns.register('randomChar', {
}, },
getMatcher(options: RandomCharOptions) { getMatcher(options: RandomCharOptions) {
const pattern = [...new Set( const pattern = [
...new Set(
(options.charsets || ['number']).reduce((acc, charset) => { (options.charsets || ['number']).reduce((acc, charset) => {
switch (charset) { switch (charset) {
case 'number': return acc + '0-9'; case 'number':
case 'lowercase': return acc + 'a-z'; return acc + '0-9';
case 'uppercase': return acc + 'A-Z'; case 'lowercase':
case 'symbol': return acc + CHAR_SETS.symbol.replace('-', '').replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&') + '-'; return acc + 'a-z';
default: return acc; case 'uppercase':
return acc + 'A-Z';
case 'symbol':
return acc + CHAR_SETS.symbol.replace('-', '').replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&') + '-';
default:
return acc;
} }
}, '') }, ''),
)].join(''); ),
].join('');
return `[${pattern}]{${options.length || 6}}`; return `[${pattern}]{${options.length || 6}}`;
} },
}); });
interface PatternConfig { interface PatternConfig {

View File

@ -22,6 +22,7 @@ import {
SchemaComponent, SchemaComponent,
SchemaComponentOptions, SchemaComponentOptions,
useAPIClient, useAPIClient,
useApp,
useCollectionManager_deprecated, useCollectionManager_deprecated,
useGlobalTheme, useGlobalTheme,
useSchemaInitializer, useSchemaInitializer,
@ -153,10 +154,13 @@ export const useCreateKanbanBlock = () => {
const options = useContext(SchemaOptionsContext); const options = useContext(SchemaOptionsContext);
const { theme } = useGlobalTheme(); const { theme } = useGlobalTheme();
const api = useAPIClient(); const api = useAPIClient();
const app = useApp();
const plugin = app.pm.get('kanban') as any;
const groupFieldInterfaces = plugin.getGroupFieldInterface() || [];
const createKanbanBlock = async ({ item }) => { const createKanbanBlock = async ({ item }) => {
const collectionFields = getCollectionFields(item.name, item.dataSource); const collectionFields = getCollectionFields(item.name, item.dataSource);
const fields = collectionFields const fields = collectionFields
?.filter((field) => ['select', 'radioGroup'].includes(field.interface)) ?.filter((field) => Object.keys(groupFieldInterfaces).find((v) => v === field.interface))
?.map((field) => { ?.map((field) => {
return { return {
label: field?.uiSchema?.title, label: field?.uiSchema?.title,
@ -218,13 +222,16 @@ export function useCreateAssociationKanbanBlock() {
const { theme } = useGlobalTheme(); const { theme } = useGlobalTheme();
const { getCollectionFields } = useCollectionManager_deprecated(); const { getCollectionFields } = useCollectionManager_deprecated();
const api = useAPIClient(); const api = useAPIClient();
const app = useApp();
const createAssociationKanbanBlock = async ({ item }) => { const createAssociationKanbanBlock = async ({ item }) => {
console.log(item);
const field = item.associationField; const field = item.associationField;
const collectionFields = getCollectionFields(item.name, item.dataSource); const collectionFields = getCollectionFields(item.name, item.dataSource);
const plugin = app.pm.get('kanban') as any;
const groupFieldInterfaces = plugin.getGroupFieldInterface() || [];
const fields = collectionFields const fields = collectionFields
?.filter((field) => ['select', 'radioGroup'].includes(field.interface)) ?.filter((field) => Object.keys(groupFieldInterfaces).find((v) => v === field.interface))
?.map((field) => { ?.map((field) => {
return { return {
label: field?.uiSchema?.title, label: field?.uiSchema?.title,

View File

@ -12,14 +12,15 @@ import { useField, useFieldSchema } from '@formily/react';
import { import {
BlockProvider, BlockProvider,
useACLRoleContext, useACLRoleContext,
useAPIClient,
useBlockRequestContext, useBlockRequestContext,
useCollection, useCollection,
useCollection_deprecated, useCollection_deprecated,
useApp,
} from '@nocobase/client'; } from '@nocobase/client';
import { Spin } from 'antd'; import { Spin } from 'antd';
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
import React, { createContext, useCallback, useContext, useEffect, useState } from 'react'; import React, { createContext, useCallback, useContext, useEffect, useState } from 'react';
import { toColumns } from './Kanban';
export const KanbanBlockContext = createContext<any>({}); export const KanbanBlockContext = createContext<any>({});
KanbanBlockContext.displayName = 'KanbanBlockContext'; KanbanBlockContext.displayName = 'KanbanBlockContext';
@ -93,20 +94,54 @@ const useDisableCardDrag = () => {
return !result; return !result;
}; };
export const toColumns = (groupCollectionField: any, dataSource: Array<any> = [], 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 = () => { export const useKanbanBlockProps = () => {
const field = useField<ArrayField>(); const field = useField<ArrayField>();
const ctx = useKanbanBlockContext(); const ctx = useKanbanBlockContext();
const [dataSource, setDataSource] = useState([]); const [dataSource, setDataSource] = useState([]);
const primaryKey = useCollection()?.getPrimaryKey(); 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(() => { 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) { if (isEqual(field.value, data) && dataSource === field.value) {
return; return;
} }
field.value = data; field.value = data;
setDataSource(field.value); setDataSource(field.value);
}, [ctx?.service?.loading]); }, [ctx?.service?.loading, options]);
const disableCardDrag = useDisableCardDrag(); const disableCardDrag = useDisableCardDrag();

View File

@ -28,6 +28,6 @@ export const useKanbanBlockHeight = () => {
const blockTitleHeaderHeight = title ? token.fontSizeLG * token.lineHeightLG + token.padding * 2 - 1 : 0; const blockTitleHeaderHeight = title ? token.fontSizeLG * token.lineHeightLG + token.padding * 2 - 1 : 0;
const footerheight = token.controlPaddingHorizontal + token.margin + token.paddingLG - token.marginXS; const footerHeight = token.controlPaddingHorizontal + token.margin + token.paddingLG - token.marginXS;
return height - actionBarHeight - kanbanHeaderHeight - footerheight - blockTitleHeaderHeight; return height - actionBarHeight - kanbanHeaderHeight - footerHeight - blockTitleHeaderHeight;
}; };

View File

@ -43,7 +43,45 @@ const KanbanPluginProvider = React.memo((props) => {
}); });
KanbanPluginProvider.displayName = 'KanbanPluginProvider'; 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 { 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() { async load() {
this.app.use(KanbanPluginProvider); this.app.use(KanbanPluginProvider);
this.app.schemaInitializerManager.add(kanbanCardInitializers_deprecated); this.app.schemaInitializerManager.add(kanbanCardInitializers_deprecated);

View File

@ -101,7 +101,7 @@ export const mapBlockSettings = new SchemaSettings({
const { dn } = useDesignable(); const { dn } = useDesignable();
const { service } = useMapBlockContext(); const { service } = useMapBlockContext();
const { name } = useCollection(); 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'], association: ['o2o', 'obo', 'oho', 'o2m', 'm2o', 'm2m'],
}); });
return { return {
@ -164,7 +164,7 @@ export const mapBlockSettings = new SchemaSettings({
const { getCollectionFieldsOptions } = useCollectionManager_deprecated(); const { getCollectionFieldsOptions } = useCollectionManager_deprecated();
const { name } = useCollection(); const { name } = useCollection();
const fieldNames = fieldSchema?.['x-decorator-props']?.['fieldNames'] || {}; 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'], association: ['o2o', 'obo', 'oho', 'o2m', 'm2o', 'm2m'],
}); });
const isPointField = findNestedOption(fieldNames.field, mapFieldOptions)?.type === 'point'; const isPointField = findNestedOption(fieldNames.field, mapFieldOptions)?.type === 'point';

View File

@ -38,11 +38,11 @@ export const MapBlockInitializer = () => {
componentType={`Map`} componentType={`Map`}
icon={<TableOutlined />} icon={<TableOutlined />}
onCreateBlockSchema={async ({ item }) => { 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'], association: ['o2o', 'obo', 'oho', 'o2m', 'm2o', 'm2m'],
dataSource: item.dataSource, dataSource: item.dataSource,
}); });
const markerFieldOptions = getCollectionFieldsOptions(item.name, 'string', { const markerFieldOptions = getCollectionFieldsOptions(item.name, 'string', null, {
dataSource: item.dataSource, dataSource: item.dataSource,
}); });
const values = await FormDialog( const values = await FormDialog(

View File

@ -9,7 +9,7 @@
import { get, pick } from 'lodash'; import { get, pick } from 'lodash';
import { BelongsTo, HasOne } from 'sequelize'; 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 Application, { DefaultContext } from '@nocobase/server';
import { Context as ActionContext, Next } from '@nocobase/actions'; import { Context as ActionContext, Next } from '@nocobase/actions';
@ -27,14 +27,10 @@ export default class extends Trigger {
const self = this; const self = this;
async function triggerWorkflowActionMiddleware(context: Context, next: Next) { async function triggerWorkflowActionMiddleware(context: Context, next: Next) {
const { resourceName, actionName } = context.action;
if (resourceName === 'workflows' && actionName === 'trigger') {
return self.workflowTriggerAction(context, next);
}
await next(); await next();
const { actionName } = context.action;
if (!['create', 'update'].includes(actionName)) { if (!['create', 'update'].includes(actionName)) {
return; return;
} }
@ -45,20 +41,17 @@ export default class extends Trigger {
workflow.app.dataSourceManager.use(triggerWorkflowActionMiddleware); workflow.app.dataSourceManager.use(triggerWorkflowActionMiddleware);
} }
/** getTargetCollection(collection: Collection, association: string) {
* @deprecated if (!association) {
*/ return collection;
async workflowTriggerAction(context: Context, next: Next) {
const { triggerWorkflows } = context.action.params;
if (!triggerWorkflows) {
return context.throw(400);
} }
context.status = 202; let targetCollection = collection;
await next(); 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) { private async collectionTriggerAction(context: Context) {
@ -76,7 +69,6 @@ export default class extends Trigger {
return; return;
} }
const fullCollectionName = joinCollectionName(dataSourceHeader, collection.name);
const { currentUser, currentRole } = context.state; const { currentUser, currentRole } = context.state;
const { model: UserModel } = this.workflow.db.getCollection('users'); const { model: UserModel } = this.workflow.db.getCollection('users');
const userInfo = { const userInfo = {
@ -92,9 +84,8 @@ export default class extends Trigger {
const globalWorkflows = new Map(); const globalWorkflows = new Map();
const localWorkflows = new Map(); const localWorkflows = new Map();
workflows.forEach((item) => { workflows.forEach((item) => {
if (resourceName === 'workflows' && actionName === 'trigger') { const targetCollection = this.getTargetCollection(collection, triggersKeysMap.get(item.key));
localWorkflows.set(item.key, item); if (item.config.collection === joinCollectionName(dataSourceHeader, targetCollection.name)) {
} else if (item.config.collection === fullCollectionName) {
if (item.config.global) { if (item.config.global) {
if (item.config.actions?.includes(actionName)) { if (item.config.actions?.includes(actionName)) {
globalWorkflows.set(item.key, item); globalWorkflows.set(item.key, item);

View File

@ -207,6 +207,7 @@ describe('workflow > action-trigger', () => {
type: 'action', type: 'action',
config: { config: {
collection: 'posts', 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', () => { describe('context data path', () => {
it('level: 1', async () => { it('level: 1', async () => {
const workflow = await WorkflowModel.create({ const workflow = await WorkflowModel.create({
enabled: true, enabled: true,
type: 'action', type: 'action',
config: { 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' } }, values: { title: 't1', category: { title: 'c1' } },
triggerWorkflows: `${workflow.key}!category`, triggerWorkflows: `${workflow.key}!category`,
}); });
expect(res1.status).toBe(202); expect(res1.status).toBe(200);
await sleep(500); await sleep(500);
@ -462,15 +329,15 @@ describe('workflow > action-trigger', () => {
enabled: true, enabled: true,
type: 'action', type: 'action',
config: { 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' } } }, values: { content: 'comment1', post: { category: { title: 'c1' } } },
triggerWorkflows: `${workflow.key}!post.category`, triggerWorkflows: `${workflow.key}!post.category`,
}); });
expect(res1.status).toBe(202); expect(res1.status).toBe(200);
await sleep(500); 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' }, values: { title: 't1' },
triggerWorkflows: `${w1.key}`, triggerWorkflows: `${w1.key}`,
}); });
expect(res1.status).toBe(202); expect(res1.status).toBe(200);
await sleep(500); await sleep(500);
@ -586,11 +453,11 @@ describe('workflow > action-trigger', () => {
enabled: true, enabled: true,
}); });
const res3 = await userAgents[0].resource('workflows').trigger({ const res3 = await userAgents[0].resource('posts').create({
values: { title: 't2' }, values: { title: 't2' },
triggerWorkflows: `${w1.key}`, triggerWorkflows: `${w1.key}`,
}); });
expect(res3.status).toBe(202); expect(res3.status).toBe(200);
await sleep(500); 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' }, values: { title: 't1' },
triggerWorkflows: `${workflow.key}`, triggerWorkflows: `${workflow.key}`,
}); });
expect(res1.status).toBe(202); expect(res1.status).toBe(200);
await sleep(500); await sleep(500);

View File

@ -157,6 +157,7 @@ function ExecuteActionButton() {
scope={{ scope={{
useCancelAction, useCancelAction,
useExecuteConfirmAction, useExecuteConfirmAction,
...trigger.scope,
}} }}
schema={{ schema={{
name: `trigger-modal-${workflow.type}-${workflow.id}`, name: `trigger-modal-${workflow.type}-${workflow.id}`,

View File

@ -7,11 +7,7 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import React from 'react'; import { PagePopups, Plugin, useCompile } from '@nocobase/client';
import { useFieldSchema } from '@formily/react';
import { isValid } from '@formily/shared';
import { PagePopups, Plugin, useCompile, WorkflowConfig } from '@nocobase/client';
import { Registry } from '@nocobase/utils/client'; import { Registry } from '@nocobase/utils/client';
// import { ExecutionPage } from './ExecutionPage'; // import { ExecutionPage } from './ExecutionPage';
@ -35,9 +31,13 @@ import UpdateInstruction from './nodes/update';
import DestroyInstruction from './nodes/destroy'; import DestroyInstruction from './nodes/destroy';
import { getWorkflowDetailPath, getWorkflowExecutionsPath } from './utils'; import { getWorkflowDetailPath, getWorkflowExecutionsPath } from './utils';
import { lang, NAMESPACE } from './locale'; import { lang, NAMESPACE } from './locale';
import { customizeSubmitToWorkflowActionSettings } from './settings/customizeSubmitToWorkflowActionSettings';
import { VariableOption } from './variable'; import { VariableOption } from './variable';
import { WorkflowTasks, TasksProvider, TaskTypeOptions } from './WorkflowTasks'; import { WorkflowTasks, TasksProvider, TaskTypeOptions } from './WorkflowTasks';
import { BindWorkflowConfig } from './settings/BindWorkflowConfig';
const workflowConfigSettings = {
Component: BindWorkflowConfig,
};
type InstructionGroup = { type InstructionGroup = {
key?: string; key?: string;
@ -138,15 +138,13 @@ export default class PluginWorkflowClient extends Plugin {
this.app.use(TasksProvider); this.app.use(TasksProvider);
this.app.schemaSettingsManager.add(customizeSubmitToWorkflowActionSettings); this.app.schemaSettingsManager.addItem('actionSettings:submit', 'workflowConfig', workflowConfigSettings);
this.app.schemaSettingsManager.addItem('actionSettings:createSubmit', 'workflowConfig', workflowConfigSettings);
this.app.schemaSettingsManager.addItem('actionSettings:delete', 'workflowConfig', { this.app.schemaSettingsManager.addItem('actionSettings:updateSubmit', 'workflowConfig', workflowConfigSettings);
Component: WorkflowConfig, this.app.schemaSettingsManager.addItem('actionSettings:saveRecord', 'workflowConfig', workflowConfigSettings);
useVisible() { this.app.schemaSettingsManager.addItem('actionSettings:updateRecord', 'workflowConfig', workflowConfigSettings);
const fieldSchema = useFieldSchema(); this.app.schemaSettingsManager.addItem('actionSettings:delete', 'workflowConfig', workflowConfigSettings);
return isValid(fieldSchema?.['x-action-settings']?.triggerWorkflows); this.app.schemaSettingsManager.addItem('actionSettings:bulkEditSubmit', 'workflowConfig', workflowConfigSettings);
},
});
this.registerInstructionGroup('control', { key: 'control', label: `{{t("Control", { ns: "${NAMESPACE}" })}}` }); this.registerInstructionGroup('control', { key: 'control', label: `{{t("Control", { ns: "${NAMESPACE}" })}}` });
this.registerInstructionGroup('calculation', { this.registerInstructionGroup('calculation', {
@ -195,3 +193,4 @@ export * from './hooks';
export { default as useStyles } from './style'; export { default as useStyles } from './style';
export * from './variable'; export * from './variable';
export * from './ExecutionContextProvider'; export * from './ExecutionContextProvider';
export * from './settings/BindWorkflowConfig';

View File

@ -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 (
<DataSourceProvider dataSource="main">
<RemoteSelect
manual={false}
placeholder={t('Select workflow', { ns: 'workflow' })}
fieldNames={{
label: 'title',
value: 'key',
}}
service={{
resource: 'workflows',
action: 'list',
params: {
filter: {
type: workflowTypes,
enabled: true,
'config.collection': workflowCollection,
},
},
}}
optionFilter={optionFilter}
optionRender={({ label, data }) => {
const typeOption = triggerOptions.find((item) => item.value === data.type);
return typeOption ? (
<Flex justify="space-between">
<span>{label}</span>
<Tag color={typeOption.color}>{compile(typeOption.label)}</Tag>
</Flex>
) : (
label
);
}}
{...props}
/>
</DataSourceProvider>
);
}
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 (
<SchemaSettingsActionModalItem
title={t('Bind workflows', { ns: 'workflow' })}
scope={{
fieldFilter(field) {
return ['belongsTo', 'hasOne'].includes(field.type);
},
}}
components={{
Alert,
ArrayTable,
WorkflowSelect,
}}
schema={
{
type: 'void',
title: t('Bind workflows', { ns: 'workflow' }),
properties: {
description: description && {
type: 'void',
'x-component': 'Alert',
'x-component-props': {
message: description,
style: {
marginBottom: '1em',
},
},
},
group: {
type: 'array',
'x-component': 'ArrayTable',
'x-decorator': 'FormItem',
items: {
type: 'object',
properties: {
sort: {
type: 'void',
'x-component': 'ArrayTable.Column',
'x-component-props': { width: 50, title: '', align: 'center' },
properties: {
sort: {
type: 'void',
'x-component': 'ArrayTable.SortHandle',
},
},
},
context: {
type: 'void',
'x-component': 'ArrayTable.Column',
'x-component-props': {
title: t('Trigger data context', { ns: 'workflow' }),
width: 200,
},
properties: {
context: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'AppendsTreeSelect',
'x-component-props': {
placeholder: t('Select context', { ns: 'workflow' }),
popupMatchSelectWidth: false,
collection: `${
collection.dataSource && collection.dataSource !== 'main' ? `${collection.dataSource}:` : ''
}${collection.name}`,
filter: '{{ fieldFilter }}',
rootOption: {
label: t('Full form data', { ns: 'workflow' }),
value: '',
},
allowClear: false,
loadData: buttonAction === 'destroy' ? null : undefined,
},
default: '',
},
},
},
workflowKey: {
type: 'void',
'x-component': 'ArrayTable.Column',
'x-component-props': {
title: t('Workflow', { ns: 'workflow' }),
},
properties: {
workflowKey: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'WorkflowSelect',
'x-component-props': {
placeholder: t('Select workflow', { ns: 'workflow' }),
actionType,
formAction,
buttonAction,
},
required: true,
},
},
},
operations: {
type: 'void',
'x-component': 'ArrayTable.Column',
'x-component-props': {
width: 32,
},
properties: {
remove: {
type: 'void',
'x-component': 'ArrayTable.Remove',
},
},
},
},
},
properties: {
add: {
type: 'void',
title: t('Add workflow', { ns: 'workflow' }),
'x-component': 'ArrayTable.Addition',
},
},
},
},
} as ISchema
}
initialValues={{ group: fieldSchema?.['x-action-settings']?.triggerWorkflows }}
onSubmit={({ group }) => {
fieldSchema['x-action-settings']['triggerWorkflows'] = group;
dn.emit('patch', {
schema: {
['x-uid']: fieldSchema['x-uid'],
'x-action-settings': fieldSchema['x-action-settings'],
},
});
}}
/>
);
}

View File

@ -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;
},
},
],
});

View File

@ -102,9 +102,9 @@ export async function sync(context: Context, next) {
* @deprecated * @deprecated
* Keep for action trigger compatibility * Keep for action trigger compatibility
*/ */
export async function trigger(context: Context, next) { // export async function trigger(context: Context, next) {
return next(); // return next();
} // }
export async function execute(context: Context, next) { export async function execute(context: Context, next) {
const plugin = context.app.pm.get(Plugin) as Plugin; const plugin = context.app.pm.get(Plugin) as Plugin;