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",
"npmClient": "yarn",
"useWorkspaces": true,
"npmClientArgs": [
"--ignore-engines"
],
"npmClientArgs": ["--ignore-engines"],
"command": {
"version": {
"forcePublish": true,

View File

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

View File

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

View File

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

View File

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

View File

@ -76,4 +76,5 @@ export class CreatedByFieldInterface extends CollectionFieldInterface {
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 = {
operators: operators.id,
};
description = '{{t("Primary key, unique identifier, self growth") }}';
titleUsable = true;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,25 +7,16 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { ArrayTable } from '@formily/antd-v5';
import { onFieldValueChange } from '@formily/core';
import { ISchema, useField, useFieldSchema, useForm, useFormEffects } from '@formily/react';
import { ISchema, useField, useFieldSchema } from '@formily/react';
import { isValid, uid } from '@formily/shared';
import { Alert, Flex, ModalProps, Tag } from 'antd';
import React, { useCallback, useMemo, useState } from 'react';
import { ModalProps } from 'antd';
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { RemoteSelect, useCompile, useDesignable } from '../..';
import { useCompile, useDesignable } from '../..';
import { isInitializersSame, useApp } from '../../../application';
import { usePlugin } from '../../../application/hooks';
import { SchemaSettingOptions, SchemaSettings } from '../../../application/schema-settings';
import { useSchemaToolbar } from '../../../application/schema-toolbar';
import { useFormBlockContext } from '../../../block-provider/FormBlockProvider';
import {
joinCollectionName,
useCollectionManager_deprecated,
useCollection_deprecated,
} from '../../../collection-manager';
import { DataSourceProvider, useDataSourceKey } from '../../../data-source';
import { useCollectionManager_deprecated, useCollection_deprecated } from '../../../collection-manager';
import { FlagProvider } from '../../../flag-provider';
import { SaveMode } from '../../../modules/actions/submit/createSubmitActionSettings';
import { useOpenModeContext } from '../../../modules/popup/OpenModeProvider';
@ -392,287 +383,6 @@ export function RemoveButton(
);
}
function WorkflowSelect({ formAction, buttonAction, actionType, ...props }) {
const { t } = useTranslation();
const index = ArrayTable.useIndex();
const { setValuesIn } = useForm();
const baseCollection = useCollection_deprecated();
const { getCollection } = useCollectionManager_deprecated();
const dataSourceKey = useDataSourceKey();
const [workflowCollection, setWorkflowCollection] = useState(joinCollectionName(dataSourceKey, baseCollection.name));
const compile = useCompile();
const workflowPlugin = usePlugin('workflow') as any;
const triggerOptions = workflowPlugin.useTriggersOptions();
const workflowTypes = useMemo(
() =>
triggerOptions
.filter((item) => {
return typeof item.options.isActionTriggerable === 'function' || item.options.isActionTriggerable === true;
})
.map((item) => item.value),
[triggerOptions],
);
useFormEffects(() => {
onFieldValueChange(`group[${index}].context`, (field) => {
let collection: any = baseCollection;
if (field.value) {
const paths = field.value.split('.');
for (let i = 0; i < paths.length && collection; i++) {
const path = paths[i];
const associationField = collection.fields.find((f) => f.name === path);
if (associationField) {
collection = getCollection(associationField.target, dataSourceKey);
}
}
}
setWorkflowCollection(joinCollectionName(dataSourceKey, collection.name));
setValuesIn(`group[${index}].workflowKey`, null);
});
});
const optionFilter = useCallback(
({ key, type, config }) => {
if (key === props.value) {
return true;
}
const trigger = workflowPlugin.triggers.get(type);
if (trigger.isActionTriggerable === true) {
return true;
}
if (typeof trigger.isActionTriggerable === 'function') {
return trigger.isActionTriggerable(config, {
action: actionType,
formAction,
buttonAction,
/**
* @deprecated
*/
direct: buttonAction === 'customize:triggerWorkflows',
});
}
return false;
},
[props.value, workflowPlugin.triggers, formAction, buttonAction, actionType],
);
return (
<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'] = [
{
name: 'Customize',
@ -774,14 +484,6 @@ export const actionSettingsItems: SchemaSettingOptions['items'] = [
return isValid(fieldSchema?.['x-action-settings']?.onSuccess);
},
},
{
name: 'workflowConfig',
Component: WorkflowConfig,
useVisible() {
const fieldSchema = useFieldSchema();
return isValid(fieldSchema?.['x-action-settings']?.triggerWorkflows);
},
},
{
name: 'saveMode',
Component: SaveMode,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

@ -25,7 +25,7 @@ test.describe('ReadPrettyFormActionInitializers & CalendarFormActionInitializers
const nocoPage = await mockPage(oneCalenderWithViewAction).waitForInit();
await mockRecord('general', { singleLineText: 'test' });
await nocoPage.goto();
await page.getByTitle('test').click();
await page.getByLabel('block-item-CardItem-general-').getByLabel('event-title').click();
await page.getByLabel('schema-initializer-ActionBar-details:configureActions-general').hover();
await page.getByRole('menuitem', { name: 'Print' }).click();
await page.getByLabel('action-Action-Print-print-general-form').click();

View File

@ -8,7 +8,7 @@
*/
import { useFieldSchema } from '@formily/react';
import { Action, Icon, useComponent, withDynamicSchemaProps } from '@nocobase/client';
import { Action, Icon, useCompile, useComponent, withDynamicSchemaProps } from '@nocobase/client';
import { Avatar } from 'antd';
import { createStyles } from 'antd-style';
import React, { useContext } from 'react';
@ -36,13 +36,15 @@ function Button() {
const backgroundColor = fieldSchema['x-component-props']?.['iconColor'];
const { layout } = useContext(WorkbenchBlockContext);
const { styles, cx } = useStyles();
const compile = useCompile();
const title = compile(fieldSchema.title);
return layout === WorkbenchLayout.Grid ? (
<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} />} />
<div className={cx(styles.title)}>{fieldSchema.title}</div>
<div className={cx(styles.title)}>{title}</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.
*/
import { ButtonEditor, SchemaSettings, SchemaSettingsActionLinkItem, useSchemaInitializer } from '@nocobase/client';
import {
ButtonEditor,
SchemaSettings,
SchemaSettingsActionLinkItem,
useSchemaInitializer,
ModalActionSchemaInitializerItem,
} from '@nocobase/client';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { ModalActionSchemaInitializerItem } from './ModalActionSchemaInitializerItem';
export const workbenchActionSettingsCustomRequest = new SchemaSettings({
name: 'workbench:actionSettings:customRequest',
items: [

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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",
"Title field": "Champ de titre",
"Custom title": "Titre personnalisé",
@ -51,4 +42,4 @@ export default {
"Monthly": "Mensuel",
"Yearly": "Annuel",
"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": "カレンダーの設定",
"Title field": "タイトルフィールド",
"Start date field": "開始日フィールド",
@ -61,5 +43,5 @@ export default {
"Monthly": "毎月",
"Yearly": "毎年",
"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": "캘린더 구성",
"Title field": "제목 필드",
"Custom title": "사용자 정의 제목",
@ -52,4 +43,4 @@ export default {
"Monthly": "매월",
"Yearly": "매년",
"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",
"Title field": "Campo de título",
"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": "Настроить календарь",
"Title 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",
"Title field": "Başlık 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": "Налаштувати календар",
"Title field": "Поле заголовка",
"Custom title": "Власний заголовок",
@ -51,4 +42,4 @@ export default {
"Monthly": "Щомісяця",
"Yearly": "Щороку",
"Repeats": "Повторюється"
};
}

View File

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

View File

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

View File

@ -8,9 +8,131 @@
*/
import { Plugin } from '@nocobase/client';
import { orderBy, reject } from 'lodash';
type PresetFieldConfig = {
order: number; // 定义字段的顺序。
description: string; // 字段描述
value: {
name: string;
interface: string;
type: string;
uiSchema: Record<string, any>;
field?: string;
[T: string]: any;
};
};
class PluginDataSourceMainClient extends Plugin {
async load() {}
collectionPresetFields: { order: number; value: any }[] = [];
addCollectionPresetField(config: PresetFieldConfig) {
this.collectionPresetFields.push(config);
}
removeCollectionPresetField(fieldName: string) {
this.collectionPresetFields = reject(this.collectionPresetFields, (v) => v.value.name === fieldName);
}
getCollectionPresetFields() {
return orderBy(this.collectionPresetFields, ['order'], ['asc']);
}
async load() {
this.addCollectionPresetField({
order: 100,
description: '{{t("Primary key, unique identifier, self growth") }}',
value: {
name: 'id',
type: 'bigInt',
autoIncrement: true,
primaryKey: true,
allowNull: false,
uiSchema: {
type: 'number',
title: '{{t("ID")}}',
'x-component': 'InputNumber',
'x-read-pretty': true,
},
interface: 'integer',
},
});
this.addCollectionPresetField({
order: 200,
description: '{{t("Store the creation time of each record")}}',
value: {
name: 'createdAt',
interface: 'createdAt',
type: 'date',
field: 'createdAt',
uiSchema: {
type: 'datetime',
title: '{{t("Created at")}}',
'x-component': 'DatePicker',
'x-component-props': {},
'x-read-pretty': true,
},
},
});
this.addCollectionPresetField({
order: 300,
description: '{{t("Store the creation user of each record") }}',
value: {
name: 'createdBy',
interface: 'createdBy',
type: 'belongsTo',
target: 'users',
foreignKey: 'createdById',
uiSchema: {
type: 'object',
title: '{{t("Created by")}}',
'x-component': 'AssociationField',
'x-component-props': {
fieldNames: {
value: 'id',
label: 'nickname',
},
},
'x-read-pretty': true,
},
},
});
this.addCollectionPresetField({
order: 400,
description: '{{t("Store the last update time of each record")}}',
value: {
type: 'date',
field: 'updatedAt',
name: 'updatedAt',
interface: 'updatedAt',
uiSchema: {
type: 'datetime',
title: '{{t("Last updated at")}}',
'x-component': 'DatePicker',
'x-component-props': {},
'x-read-pretty': true,
},
},
});
this.addCollectionPresetField({
order: 500,
description: '{{t("Store the last update user of each record")}}',
value: {
type: 'belongsTo',
target: 'users',
foreignKey: 'updatedById',
name: 'updatedBy',
interface: 'updatedBy',
uiSchema: {
type: 'object',
title: '{{t("Last updated by")}}',
'x-component': 'AssociationField',
'x-component-props': {
fieldNames: {
value: 'id',
label: 'nickname',
},
},
'x-read-pretty': true,
},
},
});
}
}
export default PluginDataSourceMainClient;

View File

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

View File

@ -377,7 +377,24 @@ export class PluginDataSourceMainServer extends Plugin {
}
await next();
});
this.app.resourceManager.use(async function pushUISchemaWhenUpdateCollectionField(ctx, next) {
const { resourceName, actionName } = ctx.action;
if (resourceName === 'collections' && actionName === 'create') {
const { values } = ctx.action.params;
const keys = Object.keys(values);
const presetKeys = ['createdAt', 'createdBy', 'updatedAt', 'updatedBy'];
for (const presetKey of presetKeys) {
if (keys.includes(presetKey)) {
continue;
}
values[presetKey] = !!values.fields?.find((v) => v.name === presetKey);
}
ctx.action.mergeParams({
values,
});
}
await next();
});
this.app.acl.allow('collections', 'list', 'loggedIn');
this.app.acl.allow('collections', 'listMeta', 'loggedIn');
this.app.acl.allow('collectionCategories', 'list', 'loggedIn');

View File

@ -8,14 +8,17 @@
*/
import React from 'react';
import { useCollectionManager_deprecated, useCompile, Variable } from '@nocobase/client';
import { useCollectionManager_deprecated, useCompile, Variable, useApp } from '@nocobase/client';
export const Expression = (props) => {
const { value = '', supports = [], useCurrentFields, onChange } = props;
const { value = '', useCurrentFields, onChange } = props;
const app = useApp();
const plugin = app.pm.get('field-formula') as any;
const { expressionFields } = plugin;
const compile = useCompile();
const { interfaces } = useCollectionManager_deprecated();
const fields = (useCurrentFields?.() ?? []).filter((field) => supports.includes(field.interface));
const fields = (useCurrentFields?.() ?? []).filter((field) => expressionFields.includes(field.interface));
const options = fields.map((field) => ({
label: compile(field.uiSchema.title),

View File

@ -9,11 +9,38 @@
import { Plugin } from '@nocobase/client';
import { Formula } from './components';
import { renderExpressionDescription } from './scopes';
import { FormulaFieldInterface } from './interfaces/formula';
import { FormulaComponentFieldSettings } from './FormulaComponentFieldSettings';
import { FormulaFieldInterface } from './interfaces/formula';
import { renderExpressionDescription } from './scopes';
export class PluginFieldFormulaClient extends Plugin {
expressionFields = [
'checkbox',
'number',
'percent',
'integer',
'number',
'percent',
'input',
'textarea',
'email',
'phone',
'datetime',
'createdAt',
'updatedAt',
'radioGroup',
'checkboxGroup',
'select',
'multipleSelect',
];
registerExpressionFieldInterface(data: string | string[]) {
if (Array.isArray(data)) {
const result = this.expressionFields.concat(data);
this.expressionFields = result;
} else {
this.expressionFields.push(data);
}
}
async load() {
this.app.addComponents({
Formula,

View File

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

View File

@ -121,9 +121,11 @@ const RuleTypes = {
number: t('Number', { ns: NAMESPACE }),
lowercase: t('Lowercase letters', { ns: NAMESPACE }),
uppercase: t('Uppercase letters', { ns: NAMESPACE }),
symbol: t('Symbols', { ns: NAMESPACE })
symbol: t('Symbols', { ns: NAMESPACE }),
};
return <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: {
@ -154,14 +156,14 @@ const RuleTypes = {
{ value: 'number', label: `{{t("Number", { ns: "${NAMESPACE}" })}}` },
{ value: 'lowercase', label: `{{t("Lowercase letters", { ns: "${NAMESPACE}" })}}` },
{ value: 'uppercase', label: `{{t("Uppercase letters", { ns: "${NAMESPACE}" })}}` },
{ value: 'symbol', label: `{{t("Symbols", { ns: "${NAMESPACE}" })}}` }
{ value: 'symbol', label: `{{t("Symbols", { ns: "${NAMESPACE}" })}}` },
],
required: true,
default: ['number'],
'x-validator': {
minItems: 1,
message: `{{t("At least one character set should be selected", { ns: "${NAMESPACE}" })}}`
}
message: `{{t("At least one character set should be selected", { ns: "${NAMESPACE}" })}}`,
},
},
},
defaults: {

View File

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

View File

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

View File

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

View File

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

View File

@ -43,7 +43,45 @@ const KanbanPluginProvider = React.memo((props) => {
});
KanbanPluginProvider.displayName = 'KanbanPluginProvider';
type GroupOption = {
value: string | number;
label: string;
color: string;
};
type CollectionField = {
name: string;
type: string;
interface: string;
[key: string]: any; // 扩展字段
};
type Option = { color?: string; label: string; value: string };
type GroupOptions = { options: Option[]; loading?: boolean };
type GetGroupOptions = (collectionField: string) => GroupOptions;
type UseGetGroupOptions = (collectionField: CollectionField) => { options: GroupOption[] };
const useDefaultGroupFieldsOptions = (collectionField) => {
return { options: collectionField.uiSchema.enum };
};
class PluginKanbanClient extends Plugin {
groupFields: { [T: string]: { useGetGroupOptions: GetGroupOptions } } = {
select: { useGetGroupOptions: useDefaultGroupFieldsOptions },
radioGroup: { useGetGroupOptions: useDefaultGroupFieldsOptions },
};
registerGroupFieldInterface(interfaceName: string, options: { useGetGroupOptions: GetGroupOptions }) {
this.groupFields[interfaceName] = options;
}
getGroupFieldInterface(key) {
if (key) {
return this.groupFields[key];
}
return this.groupFields;
}
async load() {
this.app.use(KanbanPluginProvider);
this.app.schemaInitializerManager.add(kanbanCardInitializers_deprecated);

View File

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

View File

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

View File

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

View File

@ -207,6 +207,7 @@ describe('workflow > action-trigger', () => {
type: 'action',
config: {
collection: 'posts',
appends: ['createdBy'],
},
});
@ -300,155 +301,21 @@ describe('workflow > action-trigger', () => {
});
});
/**
* @deprecated
*/
describe('directly trigger', () => {
it('no collection configured should not be triggered', async () => {
const workflow = await WorkflowModel.create({
enabled: true,
type: 'action',
});
const res1 = await userAgents[0].resource('workflows').trigger({
values: { title: 't1' },
triggerWorkflows: `${workflow.key}`,
});
expect(res1.status).toBe(202);
await sleep(500);
const e1s = await workflow.getExecutions();
expect(e1s.length).toBe(0);
});
it('trigger on form data', async () => {
const workflow = await WorkflowModel.create({
enabled: true,
type: 'action',
config: {
collection: 'posts',
appends: ['createdBy'],
},
});
const res1 = await userAgents[0].resource('workflows').trigger({
values: { title: 't1' },
triggerWorkflows: `${workflow.key}`,
});
expect(res1.status).toBe(202);
await sleep(500);
const [e1] = await workflow.getExecutions();
expect(e1.status).toBe(EXECUTION_STATUS.RESOLVED);
expect(e1.context.data).toMatchObject({ title: 't1' });
expect(e1.context.data.createdBy).toBeUndefined();
});
it('trigger on record data', async () => {
const workflow = await WorkflowModel.create({
enabled: true,
type: 'action',
config: {
collection: 'posts',
appends: ['createdBy'],
},
});
const post = await PostRepo.create({
values: { title: 't1', createdBy: users[0].id },
});
const res1 = await userAgents[0].resource('workflows').trigger({
values: post.toJSON(),
triggerWorkflows: `${workflow.key}`,
});
expect(res1.status).toBe(202);
await sleep(500);
const [e1] = await workflow.getExecutions();
expect(e1.status).toBe(EXECUTION_STATUS.RESOLVED);
expect(e1.context.data).toMatchObject({ title: 't1' });
expect(e1.context.data).toHaveProperty('createdBy');
expect(e1.context.data.createdBy.id).toBe(users[0].id);
});
it('multi trigger', async () => {
const w1 = await WorkflowModel.create({
enabled: true,
type: 'action',
config: {
collection: 'posts',
},
});
const w2 = await WorkflowModel.create({
enabled: true,
type: 'action',
config: {
collection: 'posts',
},
});
const res1 = await userAgents[0].resource('workflows').trigger({
values: { title: 't1' },
triggerWorkflows: `${w1.key},${w2.key}`,
});
expect(res1.status).toBe(202);
await sleep(500);
const [e1] = await w1.getExecutions();
expect(e1.status).toBe(EXECUTION_STATUS.RESOLVED);
expect(e1.context.data).toMatchObject({ title: 't1' });
const [e2] = await w2.getExecutions();
expect(e2.status).toBe(EXECUTION_STATUS.RESOLVED);
expect(e2.context.data).toMatchObject({ title: 't1' });
});
it('user submitted form', async () => {
const workflow = await WorkflowModel.create({
enabled: true,
type: 'action',
config: {
collection: 'posts',
appends: ['createdBy'],
},
});
const res1 = await userAgents[0].resource('posts').create({
values: { title: 't1' },
triggerWorkflows: `${workflow.key}`,
});
expect(res1.status).toBe(200);
await sleep(500);
const [e1] = await workflow.getExecutions();
expect(e1.status).toBe(EXECUTION_STATUS.RESOLVED);
expect(e1.context.user).toBeDefined();
expect(e1.context.user.id).toBe(users[0].id);
});
});
describe('context data path', () => {
it('level: 1', async () => {
const workflow = await WorkflowModel.create({
enabled: true,
type: 'action',
config: {
collection: 'posts',
collection: 'categories',
},
});
const res1 = await userAgents[0].resource('workflows').trigger({
const res1 = await userAgents[0].resource('posts').create({
values: { title: 't1', category: { title: 'c1' } },
triggerWorkflows: `${workflow.key}!category`,
});
expect(res1.status).toBe(202);
expect(res1.status).toBe(200);
await sleep(500);
@ -462,15 +329,15 @@ describe('workflow > action-trigger', () => {
enabled: true,
type: 'action',
config: {
collection: 'posts',
collection: 'categories',
},
});
const res1 = await userAgents[0].resource('workflows').trigger({
const res1 = await userAgents[0].resource('comments').create({
values: { content: 'comment1', post: { category: { title: 'c1' } } },
triggerWorkflows: `${workflow.key}!post.category`,
});
expect(res1.status).toBe(202);
expect(res1.status).toBe(200);
await sleep(500);
@ -563,11 +430,11 @@ describe('workflow > action-trigger', () => {
},
});
const res1 = await userAgents[0].resource('workflows').trigger({
const res1 = await userAgents[0].resource('posts').create({
values: { title: 't1' },
triggerWorkflows: `${w1.key}`,
});
expect(res1.status).toBe(202);
expect(res1.status).toBe(200);
await sleep(500);
@ -586,11 +453,11 @@ describe('workflow > action-trigger', () => {
enabled: true,
});
const res3 = await userAgents[0].resource('workflows').trigger({
const res3 = await userAgents[0].resource('posts').create({
values: { title: 't2' },
triggerWorkflows: `${w1.key}`,
});
expect(res3.status).toBe(202);
expect(res3.status).toBe(200);
await sleep(500);
@ -769,11 +636,11 @@ describe('workflow > action-trigger', () => {
},
});
const res1 = await userAgents[0].resource('workflows').trigger({
const res1 = await userAgents[0].resource('posts').create({
values: { title: 't1' },
triggerWorkflows: `${workflow.key}`,
});
expect(res1.status).toBe(202);
expect(res1.status).toBe(200);
await sleep(500);

View File

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

View File

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

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
* Keep for action trigger compatibility
*/
export async function trigger(context: Context, next) {
return next();
}
// export async function trigger(context: Context, next) {
// return next();
// }
export async function execute(context: Context, next) {
const plugin = context.app.pm.get(Plugin) as Plugin;