mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-07-01 02:32:19 +08:00
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:
parent
459a721e98
commit
5fbc7697c6
@ -2,9 +2,7 @@
|
||||
"version": "1.6.0-beta.8",
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"npmClientArgs": [
|
||||
"--ignore-engines"
|
||||
],
|
||||
"npmClientArgs": ["--ignore-engines"],
|
||||
"command": {
|
||||
"version": {
|
||||
"forcePublish": true,
|
||||
|
@ -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() {
|
||||
|
@ -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";
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -76,4 +76,5 @@ export class CreatedByFieldInterface extends CollectionFieldInterface {
|
||||
schema['x-component-props']['ellipsis'] = true;
|
||||
}
|
||||
}
|
||||
description = '{{t("Store the creation user of each record")}}';
|
||||
}
|
||||
|
@ -55,5 +55,7 @@ export class IdFieldInterface extends CollectionFieldInterface {
|
||||
filterable = {
|
||||
operators: operators.id,
|
||||
};
|
||||
|
||||
description = '{{t("Primary key, unique identifier, self growth") }}';
|
||||
titleUsable = true;
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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")}}';
|
||||
}
|
||||
|
@ -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 });
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
@ -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,
|
||||
|
@ -22,9 +22,6 @@ export const CreateSubmitActionInitializer = (props) => {
|
||||
type: 'primary',
|
||||
htmlType: 'submit',
|
||||
},
|
||||
'x-action-settings': {
|
||||
triggerWorkflows: [],
|
||||
},
|
||||
};
|
||||
return <ActionInitializerItem {...props} schema={schema} />;
|
||||
};
|
||||
|
@ -23,9 +23,6 @@ export const UpdateSubmitActionInitializer = (props) => {
|
||||
type: 'primary',
|
||||
htmlType: 'submit',
|
||||
},
|
||||
'x-action-settings': {
|
||||
triggerWorkflows: [],
|
||||
},
|
||||
};
|
||||
return <ActionInitializerItem {...props} schema={schema} />;
|
||||
};
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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]);
|
||||
|
@ -80,25 +80,28 @@ export const requestSettingsSchema: ISchema = {
|
||||
},
|
||||
};
|
||||
|
||||
export const linkageAction = async ({
|
||||
export const linkageAction = async (
|
||||
{
|
||||
operator,
|
||||
field,
|
||||
condition,
|
||||
variables,
|
||||
localVariables,
|
||||
}: {
|
||||
}: {
|
||||
operator;
|
||||
field;
|
||||
condition;
|
||||
variables: VariablesContextType;
|
||||
localVariables: VariableOption[];
|
||||
}) => {
|
||||
},
|
||||
jsonLogic: any,
|
||||
) => {
|
||||
const disableResult = field?.stateOfLinkageRules?.disabled || [false];
|
||||
const displayResult = field?.stateOfLinkageRules?.display || ['visible'];
|
||||
|
||||
switch (operator) {
|
||||
case ActionType.Visible:
|
||||
if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables })) {
|
||||
if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables }, jsonLogic)) {
|
||||
displayResult.push(operator);
|
||||
field.data = field.data || {};
|
||||
field.data.hidden = false;
|
||||
@ -110,7 +113,7 @@ export const linkageAction = async ({
|
||||
field.display = last(displayResult);
|
||||
break;
|
||||
case ActionType.Hidden:
|
||||
if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables })) {
|
||||
if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables }, jsonLogic)) {
|
||||
field.data = field.data || {};
|
||||
field.data.hidden = true;
|
||||
} else {
|
||||
@ -119,7 +122,7 @@ export const linkageAction = async ({
|
||||
}
|
||||
break;
|
||||
case ActionType.Disabled:
|
||||
if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables })) {
|
||||
if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables }, jsonLogic)) {
|
||||
disableResult.push(true);
|
||||
}
|
||||
field.stateOfLinkageRules = {
|
||||
@ -130,7 +133,7 @@ export const linkageAction = async ({
|
||||
field.componentProps['disabled'] = last(disableResult);
|
||||
break;
|
||||
case ActionType.Active:
|
||||
if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables })) {
|
||||
if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables }, jsonLogic)) {
|
||||
disableResult.push(false);
|
||||
} else {
|
||||
disableResult.push(!!field.componentProps?.['disabled']);
|
||||
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
@ -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,
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
@ -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) {
|
@ -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,12 +76,13 @@ function getAllKeys(obj) {
|
||||
return keys;
|
||||
}
|
||||
|
||||
export const conditionAnalyses = async ({
|
||||
export const conditionAnalyses = async (
|
||||
{
|
||||
ruleGroup,
|
||||
variables,
|
||||
localVariables,
|
||||
variableNameOfLeftCondition,
|
||||
}: {
|
||||
}: {
|
||||
ruleGroup;
|
||||
variables: VariablesContextType;
|
||||
localVariables: VariableOption[];
|
||||
@ -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 });
|
||||
|
@ -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]);
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
@ -30,3 +30,4 @@ export * from './RecordReadPrettyAssociationFormBlockInitializer';
|
||||
export * from './SelectActionInitializer';
|
||||
export * from './SubmitActionInitializer';
|
||||
export * from './TableActionColumnInitializer';
|
||||
export * from './ModalActionSchemaInitializerItem';
|
||||
|
@ -39,7 +39,8 @@ interface Props {
|
||||
variableNameOfLeftCondition?: string;
|
||||
}
|
||||
|
||||
export function bindLinkageRulesToFiled({
|
||||
export function bindLinkageRulesToFiled(
|
||||
{
|
||||
field,
|
||||
linkageRules,
|
||||
formValues,
|
||||
@ -48,7 +49,7 @@ export function bindLinkageRulesToFiled({
|
||||
rule,
|
||||
variables,
|
||||
variableNameOfLeftCondition,
|
||||
}: {
|
||||
}: {
|
||||
field: any;
|
||||
linkageRules: any[];
|
||||
formValues: any;
|
||||
@ -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,14 +179,15 @@ function getVariableValue(variableString: string, localVariables: VariableOption
|
||||
return getValuesByPath(ctx, getPath(variableString));
|
||||
}
|
||||
|
||||
function getSubscriber({
|
||||
function getSubscriber(
|
||||
{
|
||||
action,
|
||||
field,
|
||||
rule,
|
||||
variables,
|
||||
localVariables,
|
||||
variableNameOfLeftCondition,
|
||||
}: {
|
||||
}: {
|
||||
action: any;
|
||||
field: any;
|
||||
rule: any;
|
||||
@ -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,
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
|
@ -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();
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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: [
|
||||
|
@ -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',
|
||||
|
@ -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',
|
||||
|
@ -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',
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
@ -30,10 +30,11 @@ import {
|
||||
useToken,
|
||||
withDynamicSchemaProps,
|
||||
withSkeletonComponent,
|
||||
useApp,
|
||||
} from '@nocobase/client';
|
||||
import type { Dayjs } from 'dayjs';
|
||||
import dayjs from 'dayjs';
|
||||
import { cloneDeep, get } from 'lodash';
|
||||
import { cloneDeep, get, omit } from 'lodash';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { View } from 'react-big-calendar';
|
||||
import { i18nt, useTranslation } from '../../locale';
|
||||
@ -116,6 +117,8 @@ const useEvents = (
|
||||
);
|
||||
const { t } = useTranslation();
|
||||
const { fields } = useCollection();
|
||||
const app = useApp();
|
||||
const plugin = app.pm.get('calendar') as any;
|
||||
const labelUiSchema = fields.find((v) => v.name === fieldNames?.title)?.uiSchema;
|
||||
const enumUiSchema = fields.find((v) => v.name === fieldNames?.colorFieldName);
|
||||
return useMemo(() => {
|
||||
@ -164,7 +167,10 @@ const useEvents = (
|
||||
});
|
||||
|
||||
if (res) return out;
|
||||
const title = getLabelFormatValue(labelUiSchema, get(item, fieldNames.title), true);
|
||||
const targetTitleCollectionField = fields.find((v) => v.name === fieldNames.title);
|
||||
const targetTitle = plugin.getTitleFieldInterface(targetTitleCollectionField.interface);
|
||||
const title = getLabelFormatValue(labelUiSchema, get(item, fieldNames.title), true, targetTitle?.TitleRenderer);
|
||||
|
||||
const event: Event = {
|
||||
id: get(item, fieldNames.id || 'id'),
|
||||
colorFieldValue: item[fieldNames.colorFieldName],
|
||||
@ -275,7 +281,7 @@ export const Calendar: any = withDynamicSchemaProps(
|
||||
}, [reactBigCalendar]);
|
||||
|
||||
// 新版 UISchema(1.0 之后)中已经废弃了 useProps,这里之所以继续保留是为了兼容旧版的 UISchema
|
||||
const { dataSource, fieldNames, showLunar, defaultView } = useProps(props);
|
||||
const { dataSource, fieldNames, showLunar, defaultView, getFontColor, getBackgroundColor } = useProps(props);
|
||||
const height = useCalenderHeight();
|
||||
const [date, setDate] = useState<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)),
|
||||
};
|
||||
|
@ -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) => {
|
||||
|
@ -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', {
|
||||
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
@ -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',
|
||||
|
@ -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"
|
||||
}
|
@ -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"
|
||||
}
|
@ -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"
|
||||
};
|
||||
}
|
@ -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": "レコードを更新する"
|
||||
}
|
@ -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": "반복"
|
||||
};
|
||||
}
|
@ -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",
|
||||
|
@ -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": "Поле даты начала",
|
||||
|
@ -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ı",
|
||||
|
@ -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": "Повторюється"
|
||||
};
|
||||
}
|
@ -47,7 +47,7 @@
|
||||
"Month": "月",
|
||||
"Week": "周",
|
||||
"{{count}} more items": "{{count}} 更多事项",
|
||||
"Background color field": "背景颜色字段",
|
||||
"Color field": "颜色字段",
|
||||
"Not selected": "未选择",
|
||||
"Default view": "默认视图",
|
||||
"Event open mode": "事项打开方式"
|
||||
|
@ -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: [],
|
||||
});
|
||||
});
|
||||
|
@ -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;
|
||||
|
@ -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',
|
||||
|
@ -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');
|
||||
|
@ -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),
|
||||
|
@ -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,
|
||||
|
@ -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();
|
||||
|
@ -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: {
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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);
|
||||
|
@ -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';
|
||||
|
@ -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(
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
||||
|
@ -157,6 +157,7 @@ function ExecuteActionButton() {
|
||||
scope={{
|
||||
useCancelAction,
|
||||
useExecuteConfirmAction,
|
||||
...trigger.scope,
|
||||
}}
|
||||
schema={{
|
||||
name: `trigger-modal-${workflow.type}-${workflow.id}`,
|
||||
|
@ -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';
|
||||
|
@ -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'],
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
@ -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;
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
@ -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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user