mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-07-01 10:42: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",
|
"version": "1.6.0-beta.8",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"useWorkspaces": true,
|
"useWorkspaces": true,
|
||||||
"npmClientArgs": [
|
"npmClientArgs": ["--ignore-engines"],
|
||||||
"--ignore-engines"
|
|
||||||
],
|
|
||||||
"command": {
|
"command": {
|
||||||
"version": {
|
"version": {
|
||||||
"forcePublish": true,
|
"forcePublish": true,
|
||||||
|
@ -44,8 +44,14 @@ import type { CollectionFieldInterfaceFactory } from '../data-source';
|
|||||||
import { OpenModeProvider } from '../modules/popup/OpenModeProvider';
|
import { OpenModeProvider } from '../modules/popup/OpenModeProvider';
|
||||||
import { AppSchemaComponentProvider } from './AppSchemaComponentProvider';
|
import { AppSchemaComponentProvider } from './AppSchemaComponentProvider';
|
||||||
import type { Plugin } from './Plugin';
|
import type { Plugin } from './Plugin';
|
||||||
|
import { getOperators } from './globalOperators';
|
||||||
import type { RequireJS } from './utils/requirejs';
|
import type { RequireJS } from './utils/requirejs';
|
||||||
|
|
||||||
|
type JsonLogic = {
|
||||||
|
addOperation: (name: string, fn?: any) => void;
|
||||||
|
rmOperation: (name: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
define: RequireJS['define'];
|
define: RequireJS['define'];
|
||||||
@ -100,7 +106,7 @@ export class Application {
|
|||||||
public dataSourceManager: DataSourceManager;
|
public dataSourceManager: DataSourceManager;
|
||||||
public name: string;
|
public name: string;
|
||||||
public globalVars: Record<string, any> = {};
|
public globalVars: Record<string, any> = {};
|
||||||
|
public jsonLogic: JsonLogic;
|
||||||
loading = true;
|
loading = true;
|
||||||
maintained = false;
|
maintained = false;
|
||||||
maintaining = false;
|
maintaining = false;
|
||||||
@ -155,6 +161,7 @@ export class Application {
|
|||||||
this.apiClient.auth.locale = lng;
|
this.apiClient.auth.locale = lng;
|
||||||
});
|
});
|
||||||
this.initListeners();
|
this.initListeners();
|
||||||
|
this.jsonLogic = getOperators();
|
||||||
}
|
}
|
||||||
|
|
||||||
private initListeners() {
|
private initListeners() {
|
||||||
|
@ -9,13 +9,11 @@
|
|||||||
|
|
||||||
/* globals define,module */
|
/* globals define,module */
|
||||||
|
|
||||||
import dayjs from 'dayjs';
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Using a Universal Module Loader that should be browser, require, and AMD friendly
|
Using a Universal Module Loader that should be browser, require, and AMD friendly
|
||||||
http://ricostacruz.com/cheatsheets/umdjs.html
|
http://ricostacruz.com/cheatsheets/umdjs.html
|
||||||
*/
|
*/
|
||||||
export function getJsonLogic() {
|
export function getOperators() {
|
||||||
'use strict';
|
'use strict';
|
||||||
/* globals console:false */
|
/* globals console:false */
|
||||||
|
|
||||||
@ -359,12 +357,12 @@ export function getJsonLogic() {
|
|||||||
return !!value;
|
return !!value;
|
||||||
};
|
};
|
||||||
|
|
||||||
jsonLogic.get_operator = function (logic) {
|
jsonLogic.getOperator = function (logic) {
|
||||||
return Object.keys(logic)[0];
|
return Object.keys(logic)[0];
|
||||||
};
|
};
|
||||||
|
|
||||||
jsonLogic.get_values = function (logic) {
|
jsonLogic.getValues = function (logic) {
|
||||||
return logic[jsonLogic.get_operator(logic)];
|
return logic[jsonLogic.getOperator(logic)];
|
||||||
};
|
};
|
||||||
|
|
||||||
jsonLogic.apply = function (logic, data) {
|
jsonLogic.apply = function (logic, data) {
|
||||||
@ -379,7 +377,7 @@ export function getJsonLogic() {
|
|||||||
return logic;
|
return logic;
|
||||||
}
|
}
|
||||||
|
|
||||||
var op = jsonLogic.get_operator(logic);
|
var op = jsonLogic.getOperator(logic);
|
||||||
var values = logic[op];
|
var values = logic[op];
|
||||||
var i;
|
var i;
|
||||||
var current;
|
var current;
|
||||||
@ -543,7 +541,7 @@ export function getJsonLogic() {
|
|||||||
var collection = [];
|
var collection = [];
|
||||||
|
|
||||||
if (jsonLogic.is_logic(logic)) {
|
if (jsonLogic.is_logic(logic)) {
|
||||||
var op = jsonLogic.get_operator(logic);
|
var op = jsonLogic.getOperator(logic);
|
||||||
var values = logic[op];
|
var values = logic[op];
|
||||||
|
|
||||||
if (!Array.isArray(values)) {
|
if (!Array.isArray(values)) {
|
||||||
@ -564,11 +562,11 @@ export function getJsonLogic() {
|
|||||||
return arrayUnique(collection);
|
return arrayUnique(collection);
|
||||||
};
|
};
|
||||||
|
|
||||||
jsonLogic.add_operation = function (name, code) {
|
jsonLogic.addOperation = function (name, code) {
|
||||||
operations[name] = code;
|
operations[name] = code;
|
||||||
};
|
};
|
||||||
|
|
||||||
jsonLogic.rm_operation = function (name) {
|
jsonLogic.rmOperation = function (name) {
|
||||||
delete operations[name];
|
delete operations[name];
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -593,8 +591,8 @@ export function getJsonLogic() {
|
|||||||
|
|
||||||
if (jsonLogic.is_logic(pattern)) {
|
if (jsonLogic.is_logic(pattern)) {
|
||||||
if (jsonLogic.is_logic(rule)) {
|
if (jsonLogic.is_logic(rule)) {
|
||||||
var pattern_op = jsonLogic.get_operator(pattern);
|
var pattern_op = jsonLogic.getOperator(pattern);
|
||||||
var rule_op = jsonLogic.get_operator(rule);
|
var rule_op = jsonLogic.getOperator(rule);
|
||||||
|
|
||||||
if (pattern_op === '@' || pattern_op === rule_op) {
|
if (pattern_op === '@' || pattern_op === rule_op) {
|
||||||
// echo "\nOperators match, go deeper\n";
|
// echo "\nOperators match, go deeper\n";
|
@ -133,7 +133,8 @@ export const useCollectionManager_deprecated = (dataSourceName?: string) => {
|
|||||||
const getCollectionFieldsOptions = useCallback(
|
const getCollectionFieldsOptions = useCallback(
|
||||||
(
|
(
|
||||||
collectionName: string,
|
collectionName: string,
|
||||||
type: string | string[] = 'string',
|
type?: string | string[],
|
||||||
|
interfaces?: string | string[],
|
||||||
opts?: {
|
opts?: {
|
||||||
dataSource?: string;
|
dataSource?: string;
|
||||||
cached?: Record<string, any>;
|
cached?: Record<string, any>;
|
||||||
@ -183,9 +184,12 @@ export const useCollectionManager_deprecated = (dataSourceName?: string) => {
|
|||||||
return _.cloneDeep(cached[collectionName]);
|
return _.cloneDeep(cached[collectionName]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof type === 'string') {
|
if (type && typeof type === 'string') {
|
||||||
type = [type];
|
type = [type];
|
||||||
}
|
}
|
||||||
|
if (interfaces && typeof interfaces === 'string') {
|
||||||
|
interfaces = [interfaces];
|
||||||
|
}
|
||||||
const fields = getCollectionFields(collectionName, customDataSourceNameValue);
|
const fields = getCollectionFields(collectionName, customDataSourceNameValue);
|
||||||
const options = fields
|
const options = fields
|
||||||
?.filter(
|
?.filter(
|
||||||
@ -193,7 +197,8 @@ export const useCollectionManager_deprecated = (dataSourceName?: string) => {
|
|||||||
field.interface &&
|
field.interface &&
|
||||||
!exceptInterfaces.includes(field.interface) &&
|
!exceptInterfaces.includes(field.interface) &&
|
||||||
(allowAllTypes ||
|
(allowAllTypes ||
|
||||||
type.includes(field.type) ||
|
(type && type.includes(field.type)) ||
|
||||||
|
(interfaces && interfaces.includes(field.interface)) ||
|
||||||
(association && field.target && field.target !== collectionName && Array.isArray(association)
|
(association && field.target && field.target !== collectionName && Array.isArray(association)
|
||||||
? association.includes(field.interface)
|
? association.includes(field.interface)
|
||||||
: false)),
|
: false)),
|
||||||
@ -207,7 +212,7 @@ export const useCollectionManager_deprecated = (dataSourceName?: string) => {
|
|||||||
if (association && field.target) {
|
if (association && field.target) {
|
||||||
result.children = collectionNames.includes(field.target)
|
result.children = collectionNames.includes(field.target)
|
||||||
? []
|
? []
|
||||||
: getCollectionFieldsOptions(field.target, type, {
|
: getCollectionFieldsOptions(field.target, type, interfaces, {
|
||||||
...opts,
|
...opts,
|
||||||
cached,
|
cached,
|
||||||
dataSource: customDataSourceNameValue,
|
dataSource: customDataSourceNameValue,
|
||||||
|
@ -28,6 +28,7 @@ export class CreatedAtFieldInterface extends CollectionFieldInterface {
|
|||||||
'x-read-pretty': true,
|
'x-read-pretty': true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
description = '{{t("Store the creation time of each record")}}';
|
||||||
availableTypes = [];
|
availableTypes = [];
|
||||||
properties = {
|
properties = {
|
||||||
...defaultProps,
|
...defaultProps,
|
||||||
|
@ -76,4 +76,5 @@ export class CreatedByFieldInterface extends CollectionFieldInterface {
|
|||||||
schema['x-component-props']['ellipsis'] = true;
|
schema['x-component-props']['ellipsis'] = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
description = '{{t("Store the creation user of each record")}}';
|
||||||
}
|
}
|
||||||
|
@ -55,5 +55,7 @@ export class IdFieldInterface extends CollectionFieldInterface {
|
|||||||
filterable = {
|
filterable = {
|
||||||
operators: operators.id,
|
operators: operators.id,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
description = '{{t("Primary key, unique identifier, self growth") }}';
|
||||||
titleUsable = true;
|
titleUsable = true;
|
||||||
}
|
}
|
||||||
|
@ -21,13 +21,14 @@ export class UpdatedAtFieldInterface extends CollectionFieldInterface {
|
|||||||
type: 'date',
|
type: 'date',
|
||||||
field: 'updatedAt',
|
field: 'updatedAt',
|
||||||
uiSchema: {
|
uiSchema: {
|
||||||
type: 'string',
|
type: 'datetime',
|
||||||
title: '{{t("Last updated at")}}',
|
title: '{{t("Last updated at")}}',
|
||||||
'x-component': 'DatePicker',
|
'x-component': 'DatePicker',
|
||||||
'x-component-props': {},
|
'x-component-props': {},
|
||||||
'x-read-pretty': true,
|
'x-read-pretty': true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
description = '{{t("Store the last update time of each record")}}';
|
||||||
availableTypes = [];
|
availableTypes = [];
|
||||||
properties = {
|
properties = {
|
||||||
...defaultProps,
|
...defaultProps,
|
||||||
|
@ -75,4 +75,5 @@ export class UpdatedByFieldInterface extends CollectionFieldInterface {
|
|||||||
schema['x-component-props']['ellipsis'] = true;
|
schema['x-component-props']['ellipsis'] = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
description = '{{t("Store the last update user of each record")}}';
|
||||||
}
|
}
|
||||||
|
@ -9,106 +9,27 @@
|
|||||||
|
|
||||||
import { observer, useForm } from '@formily/react';
|
import { observer, useForm } from '@formily/react';
|
||||||
import { Table, Tag } from 'antd';
|
import { Table, Tag } from 'antd';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useCollectionManager_deprecated } from '../../';
|
import { useCollectionManager_deprecated } from '../../';
|
||||||
import { useCompile } from '../../../';
|
import { useCompile, useApp } from '../../../';
|
||||||
|
|
||||||
const getDefaultCollectionFields = (presetFields, values) => {
|
const getDefaultCollectionFields = (presetFields, values, collectionPresetFields) => {
|
||||||
if (values?.template === 'view' || values?.template === 'sql') {
|
if (values?.template === 'view' || values?.template === 'sql') {
|
||||||
return values.fields;
|
return values.fields;
|
||||||
}
|
}
|
||||||
const defaults = values.fields
|
const fields =
|
||||||
? [...values.fields].filter((v) => {
|
values.fields?.filter((v) => {
|
||||||
return !['id', 'createdBy', 'updatedAt', 'createdAt', 'updatedBy'].includes(v.name);
|
const item = collectionPresetFields.find((i) => i.value.name === v.name);
|
||||||
})
|
return !item;
|
||||||
: [];
|
}) || [];
|
||||||
if (presetFields.find((v) => v.name === 'id')) {
|
presetFields.map((v) => {
|
||||||
defaults.push({
|
const item = collectionPresetFields.find((i) => i.value.name === v);
|
||||||
name: 'id',
|
item && fields.push(item.value);
|
||||||
type: 'bigInt',
|
|
||||||
autoIncrement: true,
|
|
||||||
primaryKey: true,
|
|
||||||
allowNull: false,
|
|
||||||
uiSchema: { type: 'number', title: '{{t("ID")}}', 'x-component': 'InputNumber', 'x-read-pretty': true },
|
|
||||||
interface: 'integer',
|
|
||||||
});
|
});
|
||||||
}
|
return fields;
|
||||||
if (presetFields.find((v) => v.name === 'createdAt')) {
|
|
||||||
defaults.push({
|
|
||||||
name: 'createdAt',
|
|
||||||
interface: 'createdAt',
|
|
||||||
type: 'date',
|
|
||||||
field: 'createdAt',
|
|
||||||
uiSchema: {
|
|
||||||
type: 'datetime',
|
|
||||||
title: '{{t("Created at")}}',
|
|
||||||
'x-component': 'DatePicker',
|
|
||||||
'x-component-props': {},
|
|
||||||
'x-read-pretty': true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (presetFields.find((v) => v.name === 'createdBy')) {
|
|
||||||
defaults.push({
|
|
||||||
name: 'createdBy',
|
|
||||||
interface: 'createdBy',
|
|
||||||
type: 'belongsTo',
|
|
||||||
target: 'users',
|
|
||||||
foreignKey: 'createdById',
|
|
||||||
uiSchema: {
|
|
||||||
type: 'object',
|
|
||||||
title: '{{t("Created by")}}',
|
|
||||||
'x-component': 'AssociationField',
|
|
||||||
'x-component-props': {
|
|
||||||
fieldNames: {
|
|
||||||
value: 'id',
|
|
||||||
label: 'nickname',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'x-read-pretty': true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (presetFields.find((v) => v.name === 'updatedAt')) {
|
|
||||||
defaults.push({
|
|
||||||
type: 'date',
|
|
||||||
field: 'updatedAt',
|
|
||||||
name: 'updatedAt',
|
|
||||||
interface: 'updatedAt',
|
|
||||||
uiSchema: {
|
|
||||||
type: 'string',
|
|
||||||
title: '{{t("Last updated at")}}',
|
|
||||||
'x-component': 'DatePicker',
|
|
||||||
'x-component-props': {},
|
|
||||||
'x-read-pretty': true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (presetFields.find((v) => v.name === 'updatedBy')) {
|
|
||||||
defaults.push({
|
|
||||||
type: 'belongsTo',
|
|
||||||
target: 'users',
|
|
||||||
foreignKey: 'updatedById',
|
|
||||||
name: 'updatedBy',
|
|
||||||
interface: 'updatedBy',
|
|
||||||
uiSchema: {
|
|
||||||
type: 'object',
|
|
||||||
title: '{{t("Last updated by")}}',
|
|
||||||
'x-component': 'AssociationField',
|
|
||||||
'x-component-props': {
|
|
||||||
fieldNames: {
|
|
||||||
value: 'id',
|
|
||||||
label: 'nickname',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'x-read-pretty': true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// 其他
|
|
||||||
return defaults;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PresetFields = observer(
|
export const PresetFields = observer(
|
||||||
(props: any) => {
|
(props: any) => {
|
||||||
const { getInterface } = useCollectionManager_deprecated();
|
const { getInterface } = useCollectionManager_deprecated();
|
||||||
@ -116,11 +37,26 @@ export const PresetFields = observer(
|
|||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
|
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const app = useApp();
|
||||||
|
const mainDataSourcePlugin: any = app.pm.get('data-source-main');
|
||||||
|
const collectionPresetFields = mainDataSourcePlugin.getCollectionPresetFields();
|
||||||
|
|
||||||
|
const presetFieldsDataSource = useMemo(() => {
|
||||||
|
return collectionPresetFields.map((v) => {
|
||||||
|
return {
|
||||||
|
field: v.value.uiSchema.title,
|
||||||
|
interface: v.value.interface,
|
||||||
|
description: v.description,
|
||||||
|
name: v.value.name,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
const column = [
|
const column = [
|
||||||
{
|
{
|
||||||
title: t('Field'),
|
title: t('Field'),
|
||||||
dataIndex: 'field',
|
dataIndex: 'field',
|
||||||
key: 'field',
|
key: 'field',
|
||||||
|
render: (value) => compile(value),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t('Interface'),
|
title: t('Interface'),
|
||||||
@ -132,61 +68,19 @@ export const PresetFields = observer(
|
|||||||
title: t('Description'),
|
title: t('Description'),
|
||||||
dataIndex: 'description',
|
dataIndex: 'description',
|
||||||
key: 'description',
|
key: 'description',
|
||||||
},
|
render: (value) => compile(value),
|
||||||
];
|
|
||||||
const dataSource = [
|
|
||||||
{
|
|
||||||
field: t('ID'),
|
|
||||||
interface: 'integer',
|
|
||||||
description: t('Primary key, unique identifier, self growth'),
|
|
||||||
name: 'id',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: t('Created at'),
|
|
||||||
interface: 'createdAt',
|
|
||||||
description: t('Store the creation time of each record'),
|
|
||||||
name: 'createdAt',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: t('Last updated at'),
|
|
||||||
interface: 'updatedAt',
|
|
||||||
description: t('Store the last update time of each record'),
|
|
||||||
name: 'updatedAt',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: t('Created by'),
|
|
||||||
interface: 'createdBy',
|
|
||||||
description: t('Store the creation user of each record'),
|
|
||||||
name: 'createdBy',
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
field: t('Last updated by'),
|
|
||||||
interface: 'updatedBy',
|
|
||||||
description: t('Store the last update user of each record'),
|
|
||||||
name: 'updatedBy',
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const config = {
|
const initialValue = presetFieldsDataSource.map((v) => v.name);
|
||||||
autoGenId: false,
|
|
||||||
createdAt: true,
|
|
||||||
createdBy: true,
|
|
||||||
updatedAt: true,
|
|
||||||
updatedBy: true,
|
|
||||||
};
|
|
||||||
const initialValue = ['id', 'createdAt', 'createdBy', 'updatedAt', 'updatedBy'];
|
|
||||||
setSelectedRowKeys(initialValue);
|
setSelectedRowKeys(initialValue);
|
||||||
form.setValues({ ...form.values, ...config });
|
form.setValues({ ...form.values, autoGenId: false });
|
||||||
}, []);
|
}, [presetFieldsDataSource]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fields = getDefaultCollectionFields(
|
const fields = getDefaultCollectionFields(
|
||||||
selectedRowKeys.map((v) => {
|
selectedRowKeys.map((v) => v),
|
||||||
return {
|
|
||||||
name: v,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
form.values,
|
form.values,
|
||||||
|
collectionPresetFields,
|
||||||
);
|
);
|
||||||
form.setValuesIn('fields', fields);
|
form.setValuesIn('fields', fields);
|
||||||
}, [selectedRowKeys]);
|
}, [selectedRowKeys]);
|
||||||
@ -197,7 +91,7 @@ export const PresetFields = observer(
|
|||||||
rowKey="name"
|
rowKey="name"
|
||||||
bordered
|
bordered
|
||||||
scroll={{ x: 600 }}
|
scroll={{ x: 600 }}
|
||||||
dataSource={dataSource}
|
dataSource={presetFieldsDataSource}
|
||||||
columns={column}
|
columns={column}
|
||||||
rowSelection={{
|
rowSelection={{
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
@ -206,21 +100,10 @@ export const PresetFields = observer(
|
|||||||
name: record.name,
|
name: record.name,
|
||||||
disabled: props?.disabled || props?.presetFieldsDisabledIncludes?.includes?.(record.name),
|
disabled: props?.disabled || props?.presetFieldsDisabledIncludes?.includes?.(record.name),
|
||||||
}),
|
}),
|
||||||
onChange: (_, selectedRows) => {
|
onChange: (selectedKeys, selectedRows) => {
|
||||||
const fields = getDefaultCollectionFields(selectedRows, form.values);
|
const fields = getDefaultCollectionFields(selectedKeys, form.values, collectionPresetFields);
|
||||||
const config = {
|
setSelectedRowKeys(selectedKeys);
|
||||||
autoGenId: false,
|
form.setValues({ ...form.values, fields, autoGenId: false });
|
||||||
createdAt: !!fields.find((v) => v.name === 'createdAt'),
|
|
||||||
createdBy: !!fields.find((v) => v.name === 'createdBy'),
|
|
||||||
updatedAt: !!fields.find((v) => v.name === 'updatedAt'),
|
|
||||||
updatedBy: !!fields.find((v) => v.name === 'updatedBy'),
|
|
||||||
};
|
|
||||||
setSelectedRowKeys(
|
|
||||||
fields?.map?.((v) => {
|
|
||||||
return v.name;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
form.setValues({ ...form.values, fields, ...config });
|
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -19,7 +19,6 @@ import {
|
|||||||
RemoveButton,
|
RemoveButton,
|
||||||
SecondConFirm,
|
SecondConFirm,
|
||||||
SkipValidation,
|
SkipValidation,
|
||||||
WorkflowConfig,
|
|
||||||
} from '../../../schema-component/antd/action/Action.Designer';
|
} from '../../../schema-component/antd/action/Action.Designer';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -53,10 +52,6 @@ export const customizeSaveRecordActionSettings = new SchemaSettings({
|
|||||||
name: 'afterSuccessfulSubmission',
|
name: 'afterSuccessfulSubmission',
|
||||||
Component: AfterSuccess,
|
Component: AfterSuccess,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'bindWorkflow',
|
|
||||||
Component: WorkflowConfig,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'refreshDataBlockRequest',
|
name: 'refreshDataBlockRequest',
|
||||||
Component: RefreshDataBlockRequest,
|
Component: RefreshDataBlockRequest,
|
||||||
|
@ -22,9 +22,6 @@ export const CreateSubmitActionInitializer = (props) => {
|
|||||||
type: 'primary',
|
type: 'primary',
|
||||||
htmlType: 'submit',
|
htmlType: 'submit',
|
||||||
},
|
},
|
||||||
'x-action-settings': {
|
|
||||||
triggerWorkflows: [],
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
return <ActionInitializerItem {...props} schema={schema} />;
|
return <ActionInitializerItem {...props} schema={schema} />;
|
||||||
};
|
};
|
||||||
|
@ -23,9 +23,6 @@ export const UpdateSubmitActionInitializer = (props) => {
|
|||||||
type: 'primary',
|
type: 'primary',
|
||||||
htmlType: 'submit',
|
htmlType: 'submit',
|
||||||
},
|
},
|
||||||
'x-action-settings': {
|
|
||||||
triggerWorkflows: [],
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
return <ActionInitializerItem {...props} schema={schema} />;
|
return <ActionInitializerItem {...props} schema={schema} />;
|
||||||
};
|
};
|
||||||
|
@ -24,7 +24,6 @@ import {
|
|||||||
RemoveButton,
|
RemoveButton,
|
||||||
SecondConFirm,
|
SecondConFirm,
|
||||||
SkipValidation,
|
SkipValidation,
|
||||||
WorkflowConfig,
|
|
||||||
} from '../../../schema-component/antd/action/Action.Designer';
|
} from '../../../schema-component/antd/action/Action.Designer';
|
||||||
import { useCollectionState } from '../../../schema-settings/DataTemplates/hooks/useCollectionState';
|
import { useCollectionState } from '../../../schema-settings/DataTemplates/hooks/useCollectionState';
|
||||||
import { SchemaSettingsModalItem } from '../../../schema-settings/SchemaSettings';
|
import { SchemaSettingsModalItem } from '../../../schema-settings/SchemaSettings';
|
||||||
@ -154,14 +153,6 @@ export const createSubmitActionSettings = new SchemaSettings({
|
|||||||
name: 'secondConfirmation',
|
name: 'secondConfirmation',
|
||||||
Component: SecondConFirm,
|
Component: SecondConFirm,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'workflowConfig',
|
|
||||||
Component: WorkflowConfig,
|
|
||||||
useVisible() {
|
|
||||||
const fieldSchema = useFieldSchema();
|
|
||||||
return isValid(fieldSchema?.['x-action-settings']?.triggerWorkflows);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'saveMode',
|
name: 'saveMode',
|
||||||
Component: SaveMode,
|
Component: SaveMode,
|
||||||
|
@ -19,7 +19,6 @@ import {
|
|||||||
RemoveButton,
|
RemoveButton,
|
||||||
SecondConFirm,
|
SecondConFirm,
|
||||||
SkipValidation,
|
SkipValidation,
|
||||||
WorkflowConfig,
|
|
||||||
} from '../../../schema-component/antd/action/Action.Designer';
|
} from '../../../schema-component/antd/action/Action.Designer';
|
||||||
import { SaveMode } from './createSubmitActionSettings';
|
import { SaveMode } from './createSubmitActionSettings';
|
||||||
import { SchemaSettingsLinkageRules } from '../../../schema-settings';
|
import { SchemaSettingsLinkageRules } from '../../../schema-settings';
|
||||||
@ -56,14 +55,6 @@ export const updateSubmitActionSettings = new SchemaSettings({
|
|||||||
name: 'secondConfirmation',
|
name: 'secondConfirmation',
|
||||||
Component: SecondConFirm,
|
Component: SecondConFirm,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'workflowConfig',
|
|
||||||
Component: WorkflowConfig,
|
|
||||||
useVisible() {
|
|
||||||
const fieldSchema = useFieldSchema();
|
|
||||||
return isValid(fieldSchema?.['x-action-settings']?.triggerWorkflows);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'assignFieldValues',
|
name: 'assignFieldValues',
|
||||||
Component: AssignedFieldValues,
|
Component: AssignedFieldValues,
|
||||||
@ -120,14 +111,6 @@ export const submitActionSettings = new SchemaSettings({
|
|||||||
name: 'secondConfirmation',
|
name: 'secondConfirmation',
|
||||||
Component: SecondConFirm,
|
Component: SecondConFirm,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'workflowConfig',
|
|
||||||
Component: WorkflowConfig,
|
|
||||||
useVisible() {
|
|
||||||
const fieldSchema = useFieldSchema();
|
|
||||||
return isValid(fieldSchema?.['x-action-settings']?.triggerWorkflows);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'saveMode',
|
name: 'saveMode',
|
||||||
Component: SaveMode,
|
Component: SaveMode,
|
||||||
|
@ -18,7 +18,6 @@ import {
|
|||||||
ButtonEditor,
|
ButtonEditor,
|
||||||
RemoveButton,
|
RemoveButton,
|
||||||
SecondConFirm,
|
SecondConFirm,
|
||||||
WorkflowConfig,
|
|
||||||
RefreshDataBlockRequest,
|
RefreshDataBlockRequest,
|
||||||
} from '../../../schema-component/antd/action/Action.Designer';
|
} from '../../../schema-component/antd/action/Action.Designer';
|
||||||
import { SchemaSettingsLinkageRules } from '../../../schema-settings';
|
import { SchemaSettingsLinkageRules } from '../../../schema-settings';
|
||||||
@ -58,14 +57,6 @@ export const customizeUpdateRecordActionSettings = new SchemaSettings({
|
|||||||
name: 'afterSuccessfulSubmission',
|
name: 'afterSuccessfulSubmission',
|
||||||
Component: AfterSuccess,
|
Component: AfterSuccess,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'workflowConfig',
|
|
||||||
Component: WorkflowConfig,
|
|
||||||
useVisible() {
|
|
||||||
const fieldSchema = useFieldSchema();
|
|
||||||
return isValid(fieldSchema?.['x-action-settings']?.triggerWorkflows);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'refreshDataBlockRequest',
|
name: 'refreshDataBlockRequest',
|
||||||
Component: RefreshDataBlockRequest,
|
Component: RefreshDataBlockRequest,
|
||||||
|
@ -7,25 +7,16 @@
|
|||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ArrayTable } from '@formily/antd-v5';
|
import { ISchema, useField, useFieldSchema } from '@formily/react';
|
||||||
import { onFieldValueChange } from '@formily/core';
|
|
||||||
import { ISchema, useField, useFieldSchema, useForm, useFormEffects } from '@formily/react';
|
|
||||||
import { isValid, uid } from '@formily/shared';
|
import { isValid, uid } from '@formily/shared';
|
||||||
import { Alert, Flex, ModalProps, Tag } from 'antd';
|
import { ModalProps } from 'antd';
|
||||||
import React, { useCallback, useMemo, useState } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { RemoteSelect, useCompile, useDesignable } from '../..';
|
import { useCompile, useDesignable } from '../..';
|
||||||
import { isInitializersSame, useApp } from '../../../application';
|
import { isInitializersSame, useApp } from '../../../application';
|
||||||
import { usePlugin } from '../../../application/hooks';
|
|
||||||
import { SchemaSettingOptions, SchemaSettings } from '../../../application/schema-settings';
|
import { SchemaSettingOptions, SchemaSettings } from '../../../application/schema-settings';
|
||||||
import { useSchemaToolbar } from '../../../application/schema-toolbar';
|
import { useSchemaToolbar } from '../../../application/schema-toolbar';
|
||||||
import { useFormBlockContext } from '../../../block-provider/FormBlockProvider';
|
import { useCollectionManager_deprecated, useCollection_deprecated } from '../../../collection-manager';
|
||||||
import {
|
|
||||||
joinCollectionName,
|
|
||||||
useCollectionManager_deprecated,
|
|
||||||
useCollection_deprecated,
|
|
||||||
} from '../../../collection-manager';
|
|
||||||
import { DataSourceProvider, useDataSourceKey } from '../../../data-source';
|
|
||||||
import { FlagProvider } from '../../../flag-provider';
|
import { FlagProvider } from '../../../flag-provider';
|
||||||
import { SaveMode } from '../../../modules/actions/submit/createSubmitActionSettings';
|
import { SaveMode } from '../../../modules/actions/submit/createSubmitActionSettings';
|
||||||
import { useOpenModeContext } from '../../../modules/popup/OpenModeProvider';
|
import { useOpenModeContext } from '../../../modules/popup/OpenModeProvider';
|
||||||
@ -392,287 +383,6 @@ export function RemoveButton(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function WorkflowSelect({ formAction, buttonAction, actionType, ...props }) {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const index = ArrayTable.useIndex();
|
|
||||||
const { setValuesIn } = useForm();
|
|
||||||
const baseCollection = useCollection_deprecated();
|
|
||||||
const { getCollection } = useCollectionManager_deprecated();
|
|
||||||
const dataSourceKey = useDataSourceKey();
|
|
||||||
const [workflowCollection, setWorkflowCollection] = useState(joinCollectionName(dataSourceKey, baseCollection.name));
|
|
||||||
const compile = useCompile();
|
|
||||||
|
|
||||||
const workflowPlugin = usePlugin('workflow') as any;
|
|
||||||
const triggerOptions = workflowPlugin.useTriggersOptions();
|
|
||||||
const workflowTypes = useMemo(
|
|
||||||
() =>
|
|
||||||
triggerOptions
|
|
||||||
.filter((item) => {
|
|
||||||
return typeof item.options.isActionTriggerable === 'function' || item.options.isActionTriggerable === true;
|
|
||||||
})
|
|
||||||
.map((item) => item.value),
|
|
||||||
[triggerOptions],
|
|
||||||
);
|
|
||||||
|
|
||||||
useFormEffects(() => {
|
|
||||||
onFieldValueChange(`group[${index}].context`, (field) => {
|
|
||||||
let collection: any = baseCollection;
|
|
||||||
if (field.value) {
|
|
||||||
const paths = field.value.split('.');
|
|
||||||
for (let i = 0; i < paths.length && collection; i++) {
|
|
||||||
const path = paths[i];
|
|
||||||
const associationField = collection.fields.find((f) => f.name === path);
|
|
||||||
if (associationField) {
|
|
||||||
collection = getCollection(associationField.target, dataSourceKey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setWorkflowCollection(joinCollectionName(dataSourceKey, collection.name));
|
|
||||||
setValuesIn(`group[${index}].workflowKey`, null);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const optionFilter = useCallback(
|
|
||||||
({ key, type, config }) => {
|
|
||||||
if (key === props.value) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const trigger = workflowPlugin.triggers.get(type);
|
|
||||||
if (trigger.isActionTriggerable === true) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (typeof trigger.isActionTriggerable === 'function') {
|
|
||||||
return trigger.isActionTriggerable(config, {
|
|
||||||
action: actionType,
|
|
||||||
formAction,
|
|
||||||
buttonAction,
|
|
||||||
/**
|
|
||||||
* @deprecated
|
|
||||||
*/
|
|
||||||
direct: buttonAction === 'customize:triggerWorkflows',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
[props.value, workflowPlugin.triggers, formAction, buttonAction, actionType],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DataSourceProvider dataSource="main">
|
|
||||||
<RemoteSelect
|
|
||||||
manual={false}
|
|
||||||
placeholder={t('Select workflow', { ns: 'workflow' })}
|
|
||||||
fieldNames={{
|
|
||||||
label: 'title',
|
|
||||||
value: 'key',
|
|
||||||
}}
|
|
||||||
service={{
|
|
||||||
resource: 'workflows',
|
|
||||||
action: 'list',
|
|
||||||
params: {
|
|
||||||
filter: {
|
|
||||||
type: workflowTypes,
|
|
||||||
enabled: true,
|
|
||||||
'config.collection': workflowCollection,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
optionFilter={optionFilter}
|
|
||||||
optionRender={({ label, data }) => {
|
|
||||||
const typeOption = triggerOptions.find((item) => item.value === data.type);
|
|
||||||
return typeOption ? (
|
|
||||||
<Flex justify="space-between">
|
|
||||||
<span>{label}</span>
|
|
||||||
<Tag color={typeOption.color}>{compile(typeOption.label)}</Tag>
|
|
||||||
</Flex>
|
|
||||||
) : (
|
|
||||||
label
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</DataSourceProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function WorkflowConfig() {
|
|
||||||
const { dn } = useDesignable();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const fieldSchema = useFieldSchema();
|
|
||||||
const collection = useCollection_deprecated();
|
|
||||||
// TODO(refactor): should refactor for getting certain action type, better from 'x-action'.
|
|
||||||
const formBlock = useFormBlockContext();
|
|
||||||
/**
|
|
||||||
* @deprecated
|
|
||||||
*/
|
|
||||||
const actionType = formBlock?.type || fieldSchema['x-action'];
|
|
||||||
const formAction = formBlock?.type;
|
|
||||||
const buttonAction = fieldSchema['x-action'];
|
|
||||||
|
|
||||||
const description = {
|
|
||||||
submit: t('Support pre-action event (local mode), post-action event (local mode), and approval event here.', {
|
|
||||||
ns: 'workflow',
|
|
||||||
}),
|
|
||||||
'customize:save': t(
|
|
||||||
'Support pre-action event (local mode), post-action event (local mode), and approval event here.',
|
|
||||||
{
|
|
||||||
ns: 'workflow',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
'customize:update': t(
|
|
||||||
'Support pre-action event (local mode), post-action event (local mode), and approval event here.',
|
|
||||||
{ ns: 'workflow' },
|
|
||||||
),
|
|
||||||
'customize:triggerWorkflows': t(
|
|
||||||
'Workflow will be triggered directly once the button clicked, without data saving. Only supports to be bound with "Custom action event".',
|
|
||||||
{ ns: '@nocobase/plugin-workflow-custom-action-trigger' },
|
|
||||||
),
|
|
||||||
'customize:triggerWorkflows_deprecated': t(
|
|
||||||
'"Submit to workflow" to "Post-action event" is deprecated, please use "Custom action event" instead.',
|
|
||||||
{ ns: 'workflow' },
|
|
||||||
),
|
|
||||||
destroy: t('Workflow will be triggered before deleting succeeded (only supports pre-action event in local mode).', {
|
|
||||||
ns: 'workflow',
|
|
||||||
}),
|
|
||||||
}[fieldSchema?.['x-action']];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SchemaSettingsActionModalItem
|
|
||||||
title={t('Bind workflows', { ns: 'workflow' })}
|
|
||||||
scope={{
|
|
||||||
fieldFilter(field) {
|
|
||||||
return ['belongsTo', 'hasOne'].includes(field.type);
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
components={{
|
|
||||||
Alert,
|
|
||||||
ArrayTable,
|
|
||||||
WorkflowSelect,
|
|
||||||
}}
|
|
||||||
schema={
|
|
||||||
{
|
|
||||||
type: 'void',
|
|
||||||
title: t('Bind workflows', { ns: 'workflow' }),
|
|
||||||
properties: {
|
|
||||||
description: description && {
|
|
||||||
type: 'void',
|
|
||||||
'x-component': 'Alert',
|
|
||||||
'x-component-props': {
|
|
||||||
message: description,
|
|
||||||
style: {
|
|
||||||
marginBottom: '1em',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
group: {
|
|
||||||
type: 'array',
|
|
||||||
'x-component': 'ArrayTable',
|
|
||||||
'x-decorator': 'FormItem',
|
|
||||||
items: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
sort: {
|
|
||||||
type: 'void',
|
|
||||||
'x-component': 'ArrayTable.Column',
|
|
||||||
'x-component-props': { width: 50, title: '', align: 'center' },
|
|
||||||
properties: {
|
|
||||||
sort: {
|
|
||||||
type: 'void',
|
|
||||||
'x-component': 'ArrayTable.SortHandle',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
context: {
|
|
||||||
type: 'void',
|
|
||||||
'x-component': 'ArrayTable.Column',
|
|
||||||
'x-component-props': {
|
|
||||||
title: t('Trigger data context', { ns: 'workflow' }),
|
|
||||||
width: 200,
|
|
||||||
},
|
|
||||||
properties: {
|
|
||||||
context: {
|
|
||||||
type: 'string',
|
|
||||||
'x-decorator': 'FormItem',
|
|
||||||
'x-component': 'AppendsTreeSelect',
|
|
||||||
'x-component-props': {
|
|
||||||
placeholder: t('Select context', { ns: 'workflow' }),
|
|
||||||
popupMatchSelectWidth: false,
|
|
||||||
collection: `${
|
|
||||||
collection.dataSource && collection.dataSource !== 'main' ? `${collection.dataSource}:` : ''
|
|
||||||
}${collection.name}`,
|
|
||||||
filter: '{{ fieldFilter }}',
|
|
||||||
rootOption: {
|
|
||||||
label: t('Full form data', { ns: 'workflow' }),
|
|
||||||
value: '',
|
|
||||||
},
|
|
||||||
allowClear: false,
|
|
||||||
loadData: buttonAction === 'destroy' ? null : undefined,
|
|
||||||
},
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
workflowKey: {
|
|
||||||
type: 'void',
|
|
||||||
'x-component': 'ArrayTable.Column',
|
|
||||||
'x-component-props': {
|
|
||||||
title: t('Workflow', { ns: 'workflow' }),
|
|
||||||
},
|
|
||||||
properties: {
|
|
||||||
workflowKey: {
|
|
||||||
type: 'number',
|
|
||||||
'x-decorator': 'FormItem',
|
|
||||||
'x-component': 'WorkflowSelect',
|
|
||||||
'x-component-props': {
|
|
||||||
placeholder: t('Select workflow', { ns: 'workflow' }),
|
|
||||||
actionType,
|
|
||||||
formAction,
|
|
||||||
buttonAction,
|
|
||||||
},
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
operations: {
|
|
||||||
type: 'void',
|
|
||||||
'x-component': 'ArrayTable.Column',
|
|
||||||
'x-component-props': {
|
|
||||||
width: 32,
|
|
||||||
},
|
|
||||||
properties: {
|
|
||||||
remove: {
|
|
||||||
type: 'void',
|
|
||||||
'x-component': 'ArrayTable.Remove',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
properties: {
|
|
||||||
add: {
|
|
||||||
type: 'void',
|
|
||||||
title: t('Add workflow', { ns: 'workflow' }),
|
|
||||||
'x-component': 'ArrayTable.Addition',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as ISchema
|
|
||||||
}
|
|
||||||
initialValues={{ group: fieldSchema?.['x-action-settings']?.triggerWorkflows }}
|
|
||||||
onSubmit={({ group }) => {
|
|
||||||
fieldSchema['x-action-settings']['triggerWorkflows'] = group;
|
|
||||||
dn.emit('patch', {
|
|
||||||
schema: {
|
|
||||||
['x-uid']: fieldSchema['x-uid'],
|
|
||||||
'x-action-settings': fieldSchema['x-action-settings'],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const actionSettingsItems: SchemaSettingOptions['items'] = [
|
export const actionSettingsItems: SchemaSettingOptions['items'] = [
|
||||||
{
|
{
|
||||||
name: 'Customize',
|
name: 'Customize',
|
||||||
@ -774,14 +484,6 @@ export const actionSettingsItems: SchemaSettingOptions['items'] = [
|
|||||||
return isValid(fieldSchema?.['x-action-settings']?.onSuccess);
|
return isValid(fieldSchema?.['x-action-settings']?.onSuccess);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'workflowConfig',
|
|
||||||
Component: WorkflowConfig,
|
|
||||||
useVisible() {
|
|
||||||
const fieldSchema = useFieldSchema();
|
|
||||||
return isValid(fieldSchema?.['x-action-settings']?.triggerWorkflows);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'saveMode',
|
name: 'saveMode',
|
||||||
Component: SaveMode,
|
Component: SaveMode,
|
||||||
|
@ -47,6 +47,7 @@ import { ActionContextProvider } from './context';
|
|||||||
import { useGetAriaLabelOfAction } from './hooks/useGetAriaLabelOfAction';
|
import { useGetAriaLabelOfAction } from './hooks/useGetAriaLabelOfAction';
|
||||||
import { ActionContextProps, ActionProps, ComposedAction } from './types';
|
import { ActionContextProps, ActionProps, ComposedAction } from './types';
|
||||||
import { linkageAction, setInitialActionState } from './utils';
|
import { linkageAction, setInitialActionState } from './utils';
|
||||||
|
import { useApp } from '../../../application';
|
||||||
|
|
||||||
const useA = () => {
|
const useA = () => {
|
||||||
return {
|
return {
|
||||||
@ -95,7 +96,7 @@ export const Action: ComposedAction = withDynamicSchemaProps(
|
|||||||
const { setSubmitted } = useActionContext();
|
const { setSubmitted } = useActionContext();
|
||||||
const { getAriaLabel } = useGetAriaLabelOfAction(title);
|
const { getAriaLabel } = useGetAriaLabelOfAction(title);
|
||||||
const parentRecordData = useCollectionParentRecordData();
|
const parentRecordData = useCollectionParentRecordData();
|
||||||
|
const app = useApp();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (field.stateOfLinkageRules) {
|
if (field.stateOfLinkageRules) {
|
||||||
setInitialActionState(field);
|
setInitialActionState(field);
|
||||||
@ -105,13 +106,16 @@ export const Action: ComposedAction = withDynamicSchemaProps(
|
|||||||
.filter((k) => !k.disabled)
|
.filter((k) => !k.disabled)
|
||||||
.forEach((v) => {
|
.forEach((v) => {
|
||||||
v.actions?.forEach((h) => {
|
v.actions?.forEach((h) => {
|
||||||
linkageAction({
|
linkageAction(
|
||||||
|
{
|
||||||
operator: h.operator,
|
operator: h.operator,
|
||||||
field,
|
field,
|
||||||
condition: v.condition,
|
condition: v.condition,
|
||||||
variables,
|
variables,
|
||||||
localVariables,
|
localVariables,
|
||||||
});
|
},
|
||||||
|
app.jsonLogic,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}, [field, linkageRules, localVariables, variables]);
|
}, [field, linkageRules, localVariables, variables]);
|
||||||
|
@ -80,7 +80,8 @@ export const requestSettingsSchema: ISchema = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const linkageAction = async ({
|
export const linkageAction = async (
|
||||||
|
{
|
||||||
operator,
|
operator,
|
||||||
field,
|
field,
|
||||||
condition,
|
condition,
|
||||||
@ -92,13 +93,15 @@ export const linkageAction = async ({
|
|||||||
condition;
|
condition;
|
||||||
variables: VariablesContextType;
|
variables: VariablesContextType;
|
||||||
localVariables: VariableOption[];
|
localVariables: VariableOption[];
|
||||||
}) => {
|
},
|
||||||
|
jsonLogic: any,
|
||||||
|
) => {
|
||||||
const disableResult = field?.stateOfLinkageRules?.disabled || [false];
|
const disableResult = field?.stateOfLinkageRules?.disabled || [false];
|
||||||
const displayResult = field?.stateOfLinkageRules?.display || ['visible'];
|
const displayResult = field?.stateOfLinkageRules?.display || ['visible'];
|
||||||
|
|
||||||
switch (operator) {
|
switch (operator) {
|
||||||
case ActionType.Visible:
|
case ActionType.Visible:
|
||||||
if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables })) {
|
if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables }, jsonLogic)) {
|
||||||
displayResult.push(operator);
|
displayResult.push(operator);
|
||||||
field.data = field.data || {};
|
field.data = field.data || {};
|
||||||
field.data.hidden = false;
|
field.data.hidden = false;
|
||||||
@ -110,7 +113,7 @@ export const linkageAction = async ({
|
|||||||
field.display = last(displayResult);
|
field.display = last(displayResult);
|
||||||
break;
|
break;
|
||||||
case ActionType.Hidden:
|
case ActionType.Hidden:
|
||||||
if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables })) {
|
if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables }, jsonLogic)) {
|
||||||
field.data = field.data || {};
|
field.data = field.data || {};
|
||||||
field.data.hidden = true;
|
field.data.hidden = true;
|
||||||
} else {
|
} else {
|
||||||
@ -119,7 +122,7 @@ export const linkageAction = async ({
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case ActionType.Disabled:
|
case ActionType.Disabled:
|
||||||
if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables })) {
|
if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables }, jsonLogic)) {
|
||||||
disableResult.push(true);
|
disableResult.push(true);
|
||||||
}
|
}
|
||||||
field.stateOfLinkageRules = {
|
field.stateOfLinkageRules = {
|
||||||
@ -130,7 +133,7 @@ export const linkageAction = async ({
|
|||||||
field.componentProps['disabled'] = last(disableResult);
|
field.componentProps['disabled'] = last(disableResult);
|
||||||
break;
|
break;
|
||||||
case ActionType.Active:
|
case ActionType.Active:
|
||||||
if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables })) {
|
if (await conditionAnalyses({ ruleGroup: condition, variables, localVariables }, jsonLogic)) {
|
||||||
disableResult.push(false);
|
disableResult.push(false);
|
||||||
} else {
|
} else {
|
||||||
disableResult.push(!!field.componentProps?.['disabled']);
|
disableResult.push(!!field.componentProps?.['disabled']);
|
||||||
|
@ -16,6 +16,7 @@ import { forEachLinkageRule } from '../../../../schema-settings/LinkageRules/for
|
|||||||
import useLocalVariables from '../../../../variables/hooks/useLocalVariables';
|
import useLocalVariables from '../../../../variables/hooks/useLocalVariables';
|
||||||
import useVariables from '../../../../variables/hooks/useVariables';
|
import useVariables from '../../../../variables/hooks/useVariables';
|
||||||
import { useSubFormValue } from '../../association-field/hooks';
|
import { useSubFormValue } from '../../association-field/hooks';
|
||||||
|
import { useApp } from '../../../../application';
|
||||||
import { isSubMode } from '../../association-field/util';
|
import { isSubMode } from '../../association-field/util';
|
||||||
|
|
||||||
const isSubFormOrSubTableField = (fieldSchema: Schema) => {
|
const isSubFormOrSubTableField = (fieldSchema: Schema) => {
|
||||||
@ -45,6 +46,7 @@ export const useLinkageRulesForSubTableOrSubForm = () => {
|
|||||||
const variables = useVariables();
|
const variables = useVariables();
|
||||||
|
|
||||||
const linkageRules = getLinkageRules(schemaOfSubTableOrSubForm);
|
const linkageRules = getLinkageRules(schemaOfSubTableOrSubForm);
|
||||||
|
const app = useApp();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isSubFormOrSubTableField(fieldSchema)) {
|
if (!isSubFormOrSubTableField(fieldSchema)) {
|
||||||
@ -77,7 +79,8 @@ export const useLinkageRulesForSubTableOrSubForm = () => {
|
|||||||
forEachLinkageRule(linkageRules, (action, rule) => {
|
forEachLinkageRule(linkageRules, (action, rule) => {
|
||||||
if (action.targetFields?.includes(fieldSchema.name)) {
|
if (action.targetFields?.includes(fieldSchema.name)) {
|
||||||
disposes.push(
|
disposes.push(
|
||||||
bindLinkageRulesToFiled({
|
bindLinkageRulesToFiled(
|
||||||
|
{
|
||||||
field,
|
field,
|
||||||
linkageRules,
|
linkageRules,
|
||||||
formValues: formValue,
|
formValues: formValue,
|
||||||
@ -86,7 +89,9 @@ export const useLinkageRulesForSubTableOrSubForm = () => {
|
|||||||
rule,
|
rule,
|
||||||
variables,
|
variables,
|
||||||
variableNameOfLeftCondition: '$iteration',
|
variableNameOfLeftCondition: '$iteration',
|
||||||
}),
|
},
|
||||||
|
app.jsonLogic,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -27,6 +27,7 @@ import { useToken } from '../../../style';
|
|||||||
import { useLocalVariables, useVariables } from '../../../variables';
|
import { useLocalVariables, useVariables } from '../../../variables';
|
||||||
import { useProps } from '../../hooks/useProps';
|
import { useProps } from '../../hooks/useProps';
|
||||||
import { useFormBlockHeight } from './hook';
|
import { useFormBlockHeight } from './hook';
|
||||||
|
import { useApp } from '../../../application';
|
||||||
|
|
||||||
export interface FormProps extends IFormLayoutProps {
|
export interface FormProps extends IFormLayoutProps {
|
||||||
form?: FormilyForm;
|
form?: FormilyForm;
|
||||||
@ -136,6 +137,7 @@ const WithForm = (props: WithFormProps) => {
|
|||||||
const localVariables = useLocalVariables({ currentForm: form });
|
const localVariables = useLocalVariables({ currentForm: form });
|
||||||
const { templateFinished } = useTemplateBlockContext();
|
const { templateFinished } = useTemplateBlockContext();
|
||||||
const { loading } = useDataBlockRequest() || {};
|
const { loading } = useDataBlockRequest() || {};
|
||||||
|
const app = useApp();
|
||||||
const linkageRules: any[] =
|
const linkageRules: any[] =
|
||||||
(getLinkageRules(fieldSchema) || fieldSchema.parent?.['x-linkage-rules'])?.filter((k) => !k.disabled) || [];
|
(getLinkageRules(fieldSchema) || fieldSchema.parent?.['x-linkage-rules'])?.filter((k) => !k.disabled) || [];
|
||||||
|
|
||||||
@ -175,7 +177,8 @@ const WithForm = (props: WithFormProps) => {
|
|||||||
// 之前使用的 `onFieldReact` 有问题,没有办法被取消监听,所以这里用 `onFieldInit` 和 `reaction` 代替
|
// 之前使用的 `onFieldReact` 有问题,没有办法被取消监听,所以这里用 `onFieldInit` 和 `reaction` 代替
|
||||||
onFieldInit(`*(${fields})`, (field: any, form) => {
|
onFieldInit(`*(${fields})`, (field: any, form) => {
|
||||||
disposes.push(
|
disposes.push(
|
||||||
bindLinkageRulesToFiled({
|
bindLinkageRulesToFiled(
|
||||||
|
{
|
||||||
field,
|
field,
|
||||||
linkageRules,
|
linkageRules,
|
||||||
formValues: form.values,
|
formValues: form.values,
|
||||||
@ -183,7 +186,9 @@ const WithForm = (props: WithFormProps) => {
|
|||||||
action,
|
action,
|
||||||
rule,
|
rule,
|
||||||
variables,
|
variables,
|
||||||
}),
|
},
|
||||||
|
app.jsonLogic,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@ import { ISchema } from '@formily/react';
|
|||||||
import { isArr } from '@formily/shared';
|
import { isArr } from '@formily/shared';
|
||||||
import { dayjs, getDefaultFormat, str2moment } from '@nocobase/utils/client';
|
import { dayjs, getDefaultFormat, str2moment } from '@nocobase/utils/client';
|
||||||
import { Tag } from 'antd';
|
import { Tag } from 'antd';
|
||||||
import React from 'react';
|
import React, { Component } from 'react';
|
||||||
import { CollectionFieldOptions_deprecated, useCollectionManager_deprecated } from '../../../collection-manager';
|
import { CollectionFieldOptions_deprecated, useCollectionManager_deprecated } from '../../../collection-manager';
|
||||||
|
|
||||||
export const useLabelUiSchema = (collectionField: CollectionFieldOptions_deprecated, label: string): ISchema => {
|
export const useLabelUiSchema = (collectionField: CollectionFieldOptions_deprecated, label: string): ISchema => {
|
||||||
@ -30,7 +30,10 @@ export const getDatePickerLabels = (props): string => {
|
|||||||
return isArr(labels) ? labels.join('~') : labels;
|
return isArr(labels) ? labels.join('~') : labels;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getLabelFormatValue = (labelUiSchema: ISchema, value: any, isTag = false): any => {
|
export const getLabelFormatValue = (labelUiSchema: ISchema, value: any, isTag = false, TitleRenderer?: any): any => {
|
||||||
|
if (TitleRenderer) {
|
||||||
|
return <TitleRenderer value={value} />;
|
||||||
|
}
|
||||||
if (Array.isArray(labelUiSchema?.enum) && value) {
|
if (Array.isArray(labelUiSchema?.enum) && value) {
|
||||||
const opt: any = labelUiSchema.enum.find((option: any) => option.value === value);
|
const opt: any = labelUiSchema.enum.find((option: any) => option.value === value);
|
||||||
if (isTag) {
|
if (isTag) {
|
@ -14,7 +14,7 @@ import { VariableOption, VariablesContextType } from '../../../variables/types';
|
|||||||
import { isVariable } from '../../../variables/utils/isVariable';
|
import { isVariable } from '../../../variables/utils/isVariable';
|
||||||
import { transformVariableValue } from '../../../variables/utils/transformVariableValue';
|
import { transformVariableValue } from '../../../variables/utils/transformVariableValue';
|
||||||
import { inferPickerType } from '../../antd/date-picker/util';
|
import { inferPickerType } from '../../antd/date-picker/util';
|
||||||
import { getJsonLogic } from '../../common/utils/logic';
|
|
||||||
type VariablesCtx = {
|
type VariablesCtx = {
|
||||||
/** 当前登录的用户 */
|
/** 当前登录的用户 */
|
||||||
$user?: Record<string, any>;
|
$user?: Record<string, any>;
|
||||||
@ -76,7 +76,8 @@ function getAllKeys(obj) {
|
|||||||
return keys;
|
return keys;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const conditionAnalyses = async ({
|
export const conditionAnalyses = async (
|
||||||
|
{
|
||||||
ruleGroup,
|
ruleGroup,
|
||||||
variables,
|
variables,
|
||||||
localVariables,
|
localVariables,
|
||||||
@ -90,17 +91,19 @@ export const conditionAnalyses = async ({
|
|||||||
* @default '$nForm'
|
* @default '$nForm'
|
||||||
*/
|
*/
|
||||||
variableNameOfLeftCondition?: string;
|
variableNameOfLeftCondition?: string;
|
||||||
}) => {
|
},
|
||||||
|
jsonLogic: any,
|
||||||
|
) => {
|
||||||
const type = Object.keys(ruleGroup)[0] || '$and';
|
const type = Object.keys(ruleGroup)[0] || '$and';
|
||||||
const conditions = ruleGroup[type];
|
const conditions = ruleGroup[type];
|
||||||
|
|
||||||
let results = conditions.map(async (condition) => {
|
let results = conditions.map(async (condition) => {
|
||||||
if ('$and' in condition || '$or' in condition) {
|
if ('$and' in condition || '$or' in condition) {
|
||||||
return await conditionAnalyses({ ruleGroup: condition, variables, localVariables });
|
return await conditionAnalyses({ ruleGroup: condition, variables, localVariables }, jsonLogic);
|
||||||
}
|
}
|
||||||
|
|
||||||
const jsonlogic = getInnermostKeyAndValue(condition);
|
const logicCalculation = getInnermostKeyAndValue(condition);
|
||||||
const operator = jsonlogic?.key;
|
const operator = logicCalculation?.key;
|
||||||
|
|
||||||
if (!operator) {
|
if (!operator) {
|
||||||
return true;
|
return true;
|
||||||
@ -113,12 +116,11 @@ export const conditionAnalyses = async ({
|
|||||||
})
|
})
|
||||||
.then(({ value }) => value);
|
.then(({ value }) => value);
|
||||||
|
|
||||||
const parsingResult = isVariable(jsonlogic?.value)
|
const parsingResult = isVariable(logicCalculation?.value)
|
||||||
? [variables.parseVariable(jsonlogic?.value, localVariables).then(({ value }) => value), targetValue]
|
? [variables.parseVariable(logicCalculation?.value, localVariables).then(({ value }) => value), targetValue]
|
||||||
: [jsonlogic?.value, targetValue];
|
: [logicCalculation?.value, targetValue];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const jsonLogic = getJsonLogic();
|
|
||||||
const [value, targetValue] = await Promise.all(parsingResult);
|
const [value, targetValue] = await Promise.all(parsingResult);
|
||||||
const targetCollectionField = await variables.getCollectionField(targetVariableName, localVariables);
|
const targetCollectionField = await variables.getCollectionField(targetVariableName, localVariables);
|
||||||
let currentInputValue = transformVariableValue(targetValue, { targetCollectionField });
|
let currentInputValue = transformVariableValue(targetValue, { targetCollectionField });
|
||||||
|
@ -22,6 +22,7 @@ import { linkageAction } from '../../schema-component/antd/action/utils';
|
|||||||
import { usePopupUtils } from '../../schema-component/antd/page/pagePopupUtils';
|
import { usePopupUtils } from '../../schema-component/antd/page/pagePopupUtils';
|
||||||
import { parseVariables } from '../../schema-component/common/utils/uitls';
|
import { parseVariables } from '../../schema-component/common/utils/uitls';
|
||||||
import { useLocalVariables, useVariables } from '../../variables';
|
import { useLocalVariables, useVariables } from '../../variables';
|
||||||
|
import { useApp } from '../../application';
|
||||||
|
|
||||||
export function useAclCheck(actionPath) {
|
export function useAclCheck(actionPath) {
|
||||||
const aclCheck = useAclCheckFn();
|
const aclCheck = useAclCheckFn();
|
||||||
@ -73,6 +74,7 @@ const InternalCreateRecordAction = (props: any, ref) => {
|
|||||||
const { openPopup } = usePopupUtils();
|
const { openPopup } = usePopupUtils();
|
||||||
const treeRecordData = useTreeParentRecord();
|
const treeRecordData = useTreeParentRecord();
|
||||||
const cm = useCollectionManager();
|
const cm = useCollectionManager();
|
||||||
|
const app = useApp();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
field.stateOfLinkageRules = {};
|
field.stateOfLinkageRules = {};
|
||||||
@ -80,13 +82,16 @@ const InternalCreateRecordAction = (props: any, ref) => {
|
|||||||
.filter((k) => !k.disabled)
|
.filter((k) => !k.disabled)
|
||||||
.forEach((v) => {
|
.forEach((v) => {
|
||||||
v.actions?.forEach((h) => {
|
v.actions?.forEach((h) => {
|
||||||
linkageAction({
|
linkageAction(
|
||||||
|
{
|
||||||
operator: h.operator,
|
operator: h.operator,
|
||||||
field,
|
field,
|
||||||
condition: v.condition,
|
condition: v.condition,
|
||||||
variables,
|
variables,
|
||||||
localVariables,
|
localVariables,
|
||||||
});
|
},
|
||||||
|
app.jsonLogic,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}, [field, linkageRules, localVariables, variables]);
|
}, [field, linkageRules, localVariables, variables]);
|
||||||
@ -143,7 +148,6 @@ export const CreateAction = observer(
|
|||||||
const form = useForm();
|
const form = useForm();
|
||||||
const variables = useVariables();
|
const variables = useVariables();
|
||||||
const aclCheck = useAclCheckFn();
|
const aclCheck = useAclCheckFn();
|
||||||
|
|
||||||
const enableChildren = fieldSchema['x-enable-children'] || [];
|
const enableChildren = fieldSchema['x-enable-children'] || [];
|
||||||
const allowAddToCurrent = fieldSchema?.['x-allow-add-to-current'];
|
const allowAddToCurrent = fieldSchema?.['x-allow-add-to-current'];
|
||||||
const linkageFromForm = fieldSchema?.['x-component-props']?.['linkageFromForm'];
|
const linkageFromForm = fieldSchema?.['x-component-props']?.['linkageFromForm'];
|
||||||
@ -176,6 +180,7 @@ export const CreateAction = observer(
|
|||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
const { designable } = useDesignable();
|
const { designable } = useDesignable();
|
||||||
const icon = props.icon || null;
|
const icon = props.icon || null;
|
||||||
|
const app = useApp();
|
||||||
const menuItems = useMemo<MenuProps['items']>(() => {
|
const menuItems = useMemo<MenuProps['items']>(() => {
|
||||||
return inheritsCollections.map((option) => ({
|
return inheritsCollections.map((option) => ({
|
||||||
key: option.name,
|
key: option.name,
|
||||||
@ -196,13 +201,16 @@ export const CreateAction = observer(
|
|||||||
.filter((k) => !k.disabled)
|
.filter((k) => !k.disabled)
|
||||||
.forEach((v) => {
|
.forEach((v) => {
|
||||||
v.actions?.forEach((h) => {
|
v.actions?.forEach((h) => {
|
||||||
linkageAction({
|
linkageAction(
|
||||||
|
{
|
||||||
operator: h.operator,
|
operator: h.operator,
|
||||||
field,
|
field,
|
||||||
condition: v.condition,
|
condition: v.condition,
|
||||||
variables,
|
variables,
|
||||||
localVariables,
|
localVariables,
|
||||||
});
|
},
|
||||||
|
app.jsonLogic,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}, [field, linkageRules, localVariables, variables]);
|
}, [field, linkageRules, localVariables, variables]);
|
||||||
|
@ -20,7 +20,7 @@ import { uid } from '@nocobase/utils/client';
|
|||||||
import React, { useMemo, useState } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
|
|
||||||
export function ModalActionSchemaInitializerItem(props) {
|
export function ModalActionSchemaInitializerItem(props) {
|
||||||
const { modalSchema = {}, ...otherProps } = props;
|
const { modalSchema = {}, components = {}, ...otherProps } = props;
|
||||||
const { properties, ...others } = modalSchema;
|
const { properties, ...others } = modalSchema;
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
const { setVisible: setSchemaInitializerVisible } = useSchemaInitializer();
|
const { setVisible: setSchemaInitializerVisible } = useSchemaInitializer();
|
||||||
@ -92,7 +92,7 @@ export function ModalActionSchemaInitializerItem(props) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ActionContextProvider value={{ visible, setVisible }}>
|
<ActionContextProvider value={{ visible, setVisible }}>
|
||||||
<SchemaComponent components={{ Action }} schema={schema} />
|
<SchemaComponent components={{ Action, ...components }} schema={schema} />
|
||||||
</ActionContextProvider>
|
</ActionContextProvider>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
@ -30,3 +30,4 @@ export * from './RecordReadPrettyAssociationFormBlockInitializer';
|
|||||||
export * from './SelectActionInitializer';
|
export * from './SelectActionInitializer';
|
||||||
export * from './SubmitActionInitializer';
|
export * from './SubmitActionInitializer';
|
||||||
export * from './TableActionColumnInitializer';
|
export * from './TableActionColumnInitializer';
|
||||||
|
export * from './ModalActionSchemaInitializerItem';
|
||||||
|
@ -39,7 +39,8 @@ interface Props {
|
|||||||
variableNameOfLeftCondition?: string;
|
variableNameOfLeftCondition?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function bindLinkageRulesToFiled({
|
export function bindLinkageRulesToFiled(
|
||||||
|
{
|
||||||
field,
|
field,
|
||||||
linkageRules,
|
linkageRules,
|
||||||
formValues,
|
formValues,
|
||||||
@ -61,7 +62,9 @@ export function bindLinkageRulesToFiled({
|
|||||||
* @default '$nForm'
|
* @default '$nForm'
|
||||||
*/
|
*/
|
||||||
variableNameOfLeftCondition?: string;
|
variableNameOfLeftCondition?: string;
|
||||||
}) {
|
},
|
||||||
|
jsonLogic: any,
|
||||||
|
) {
|
||||||
field['initStateOfLinkageRules'] = {
|
field['initStateOfLinkageRules'] = {
|
||||||
display: field.initStateOfLinkageRules?.display || getTempFieldState(true, field.display),
|
display: field.initStateOfLinkageRules?.display || getTempFieldState(true, field.display),
|
||||||
required: field.initStateOfLinkageRules?.required || getTempFieldState(true, field.required || false),
|
required: field.initStateOfLinkageRules?.required || getTempFieldState(true, field.required || false),
|
||||||
@ -89,7 +92,7 @@ export function bindLinkageRulesToFiled({
|
|||||||
.join(',');
|
.join(',');
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
getSubscriber({ action, field, rule, variables, localVariables, variableNameOfLeftCondition }),
|
getSubscriber({ action, field, rule, variables, localVariables, variableNameOfLeftCondition }, jsonLogic),
|
||||||
{ fireImmediately: true, equals: _.isEqual },
|
{ fireImmediately: true, equals: _.isEqual },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -176,7 +179,8 @@ function getVariableValue(variableString: string, localVariables: VariableOption
|
|||||||
return getValuesByPath(ctx, getPath(variableString));
|
return getValuesByPath(ctx, getPath(variableString));
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSubscriber({
|
function getSubscriber(
|
||||||
|
{
|
||||||
action,
|
action,
|
||||||
field,
|
field,
|
||||||
rule,
|
rule,
|
||||||
@ -194,10 +198,13 @@ function getSubscriber({
|
|||||||
* @default '$nForm'
|
* @default '$nForm'
|
||||||
*/
|
*/
|
||||||
variableNameOfLeftCondition?: string;
|
variableNameOfLeftCondition?: string;
|
||||||
}): (value: string, oldValue: string) => void {
|
},
|
||||||
|
jsonLogic,
|
||||||
|
): (value: string, oldValue: string) => void {
|
||||||
return () => {
|
return () => {
|
||||||
// 当条件改变触发 reaction 时,会同步收集字段状态,并保存到 field.stateOfLinkageRules 中
|
// 当条件改变触发 reaction 时,会同步收集字段状态,并保存到 field.stateOfLinkageRules 中
|
||||||
collectFieldStateOfLinkageRules({
|
collectFieldStateOfLinkageRules(
|
||||||
|
{
|
||||||
operator: action.operator,
|
operator: action.operator,
|
||||||
value: action.value,
|
value: action.value,
|
||||||
field,
|
field,
|
||||||
@ -205,7 +212,9 @@ function getSubscriber({
|
|||||||
variables,
|
variables,
|
||||||
localVariables,
|
localVariables,
|
||||||
variableNameOfLeftCondition,
|
variableNameOfLeftCondition,
|
||||||
});
|
},
|
||||||
|
jsonLogic,
|
||||||
|
);
|
||||||
|
|
||||||
// 当条件改变时,有可能会触发多个 reaction,所以这里需要延迟一下,确保所有的 reaction 都执行完毕后,
|
// 当条件改变时,有可能会触发多个 reaction,所以这里需要延迟一下,确保所有的 reaction 都执行完毕后,
|
||||||
// 再从 field.stateOfLinkageRules 中取值,因为此时 field.stateOfLinkageRules 中的值才是全的。
|
// 再从 field.stateOfLinkageRules 中取值,因为此时 field.stateOfLinkageRules 中的值才是全的。
|
||||||
@ -286,15 +295,10 @@ function getFieldNameByOperator(operator: ActionType) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const collectFieldStateOfLinkageRules = ({
|
export const collectFieldStateOfLinkageRules = (
|
||||||
operator,
|
{ operator, value, field, condition, variables, localVariables, variableNameOfLeftCondition }: Props,
|
||||||
value,
|
jsonLogic: any,
|
||||||
field,
|
) => {
|
||||||
condition,
|
|
||||||
variables,
|
|
||||||
localVariables,
|
|
||||||
variableNameOfLeftCondition,
|
|
||||||
}: Props) => {
|
|
||||||
const requiredResult = field?.stateOfLinkageRules?.required || [field?.initStateOfLinkageRules?.required];
|
const requiredResult = field?.stateOfLinkageRules?.required || [field?.initStateOfLinkageRules?.required];
|
||||||
const displayResult = field?.stateOfLinkageRules?.display || [field?.initStateOfLinkageRules?.display];
|
const displayResult = field?.stateOfLinkageRules?.display || [field?.initStateOfLinkageRules?.display];
|
||||||
const patternResult = field?.stateOfLinkageRules?.pattern || [field?.initStateOfLinkageRules?.pattern];
|
const patternResult = field?.stateOfLinkageRules?.pattern || [field?.initStateOfLinkageRules?.pattern];
|
||||||
@ -304,14 +308,14 @@ export const collectFieldStateOfLinkageRules = ({
|
|||||||
|
|
||||||
switch (operator) {
|
switch (operator) {
|
||||||
case ActionType.Required:
|
case ActionType.Required:
|
||||||
requiredResult.push(getTempFieldState(conditionAnalyses(paramsToGetConditionResult), true));
|
requiredResult.push(getTempFieldState(conditionAnalyses(paramsToGetConditionResult, jsonLogic), true));
|
||||||
field.stateOfLinkageRules = {
|
field.stateOfLinkageRules = {
|
||||||
...field.stateOfLinkageRules,
|
...field.stateOfLinkageRules,
|
||||||
required: requiredResult,
|
required: requiredResult,
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
case ActionType.InRequired:
|
case ActionType.InRequired:
|
||||||
requiredResult.push(getTempFieldState(conditionAnalyses(paramsToGetConditionResult), false));
|
requiredResult.push(getTempFieldState(conditionAnalyses(paramsToGetConditionResult, jsonLogic), false));
|
||||||
field.stateOfLinkageRules = {
|
field.stateOfLinkageRules = {
|
||||||
...field.stateOfLinkageRules,
|
...field.stateOfLinkageRules,
|
||||||
required: requiredResult,
|
required: requiredResult,
|
||||||
@ -320,7 +324,7 @@ export const collectFieldStateOfLinkageRules = ({
|
|||||||
case ActionType.Visible:
|
case ActionType.Visible:
|
||||||
case ActionType.None:
|
case ActionType.None:
|
||||||
case ActionType.Hidden:
|
case ActionType.Hidden:
|
||||||
displayResult.push(getTempFieldState(conditionAnalyses(paramsToGetConditionResult), operator));
|
displayResult.push(getTempFieldState(conditionAnalyses(paramsToGetConditionResult, jsonLogic), operator));
|
||||||
field.stateOfLinkageRules = {
|
field.stateOfLinkageRules = {
|
||||||
...field.stateOfLinkageRules,
|
...field.stateOfLinkageRules,
|
||||||
display: displayResult,
|
display: displayResult,
|
||||||
@ -329,7 +333,7 @@ export const collectFieldStateOfLinkageRules = ({
|
|||||||
case ActionType.Editable:
|
case ActionType.Editable:
|
||||||
case ActionType.ReadOnly:
|
case ActionType.ReadOnly:
|
||||||
case ActionType.ReadPretty:
|
case ActionType.ReadPretty:
|
||||||
patternResult.push(getTempFieldState(conditionAnalyses(paramsToGetConditionResult), operator));
|
patternResult.push(getTempFieldState(conditionAnalyses(paramsToGetConditionResult, jsonLogic), operator));
|
||||||
field.stateOfLinkageRules = {
|
field.stateOfLinkageRules = {
|
||||||
...field.stateOfLinkageRules,
|
...field.stateOfLinkageRules,
|
||||||
pattern: patternResult,
|
pattern: patternResult,
|
||||||
@ -364,7 +368,7 @@ export const collectFieldStateOfLinkageRules = ({
|
|||||||
if (isConditionEmpty(condition)) {
|
if (isConditionEmpty(condition)) {
|
||||||
valueResult.push(getTempFieldState(true, getValue()));
|
valueResult.push(getTempFieldState(true, getValue()));
|
||||||
} else {
|
} else {
|
||||||
valueResult.push(getTempFieldState(conditionAnalyses(paramsToGetConditionResult), getValue()));
|
valueResult.push(getTempFieldState(conditionAnalyses(paramsToGetConditionResult, jsonLogic), getValue()));
|
||||||
}
|
}
|
||||||
field.stateOfLinkageRules = {
|
field.stateOfLinkageRules = {
|
||||||
...field.stateOfLinkageRules,
|
...field.stateOfLinkageRules,
|
||||||
|
@ -25,13 +25,13 @@ const getActionValue = (operator, value) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSatisfiedActions = async ({ rules, variables, localVariables }) => {
|
const getSatisfiedActions = async ({ rules, variables, localVariables }, jsonLogic) => {
|
||||||
const satisfiedRules = (
|
const satisfiedRules = (
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
rules
|
rules
|
||||||
.filter((k) => !k.disabled)
|
.filter((k) => !k.disabled)
|
||||||
.map(async (rule) => {
|
.map(async (rule) => {
|
||||||
if (await conditionAnalyses({ ruleGroup: rule.condition, variables, localVariables })) {
|
if (await conditionAnalyses({ ruleGroup: rule.condition, variables, localVariables }, jsonLogic)) {
|
||||||
return rule;
|
return rule;
|
||||||
} else return null;
|
} else return null;
|
||||||
}),
|
}),
|
||||||
@ -40,15 +40,15 @@ const getSatisfiedActions = async ({ rules, variables, localVariables }) => {
|
|||||||
return satisfiedRules.map((rule) => rule.actions).flat();
|
return satisfiedRules.map((rule) => rule.actions).flat();
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSatisfiedValues = async ({ rules, variables, localVariables }) => {
|
const getSatisfiedValues = async ({ rules, variables, localVariables }, jsonLogic) => {
|
||||||
return (await getSatisfiedActions({ rules, variables, localVariables })).map((action) => ({
|
return (await getSatisfiedActions({ rules, variables, localVariables }, jsonLogic)).map((action) => ({
|
||||||
...action,
|
...action,
|
||||||
value: getActionValue(action.operator, action.value),
|
value: getActionValue(action.operator, action.value),
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getSatisfiedValueMap = async ({ rules, variables, localVariables }) => {
|
export const getSatisfiedValueMap = async ({ rules, variables, localVariables }, jsonLogic) => {
|
||||||
const values = await getSatisfiedValues({ rules, variables, localVariables });
|
const values = await getSatisfiedValues({ rules, variables, localVariables }, jsonLogic);
|
||||||
const valueMap = values.reduce((a, v) => ({ ...a, [v.operator]: v.value }), {});
|
const valueMap = values.reduce((a, v) => ({ ...a, [v.operator]: v.value }), {});
|
||||||
return valueMap;
|
return valueMap;
|
||||||
};
|
};
|
||||||
|
@ -15,7 +15,7 @@ import React, { useCallback, useEffect, useState } from 'react';
|
|||||||
import { useLocalVariables, useVariables } from '../../variables';
|
import { useLocalVariables, useVariables } from '../../variables';
|
||||||
import { getSatisfiedValueMap } from './compute-rules';
|
import { getSatisfiedValueMap } from './compute-rules';
|
||||||
import { LinkageRuleCategory, LinkageRuleDataKeyMap } from './type';
|
import { LinkageRuleCategory, LinkageRuleDataKeyMap } from './type';
|
||||||
|
import { useApp } from '../../application';
|
||||||
export function useSatisfiedActionValues({
|
export function useSatisfiedActionValues({
|
||||||
formValues,
|
formValues,
|
||||||
category = 'default',
|
category = 'default',
|
||||||
@ -35,10 +35,11 @@ export function useSatisfiedActionValues({
|
|||||||
const localVariables = useLocalVariables({ currentForm: { values: formValues } as any });
|
const localVariables = useLocalVariables({ currentForm: { values: formValues } as any });
|
||||||
const localSchema = schema ?? fieldSchema;
|
const localSchema = schema ?? fieldSchema;
|
||||||
const styleRules = rules ?? localSchema[LinkageRuleDataKeyMap[category]];
|
const styleRules = rules ?? localSchema[LinkageRuleDataKeyMap[category]];
|
||||||
|
const app = useApp();
|
||||||
|
|
||||||
const compute = useCallback(() => {
|
const compute = useCallback(() => {
|
||||||
if (styleRules && formValues) {
|
if (styleRules && formValues) {
|
||||||
getSatisfiedValueMap({ rules: styleRules, variables, localVariables })
|
getSatisfiedValueMap({ rules: styleRules, variables, localVariables }, app.jsonLogic)
|
||||||
.then((valueMap) => {
|
.then((valueMap) => {
|
||||||
if (!isEmpty(valueMap)) {
|
if (!isEmpty(valueMap)) {
|
||||||
setValueMap(valueMap);
|
setValueMap(valueMap);
|
||||||
|
@ -19,7 +19,6 @@ import {
|
|||||||
useOpenModeContext,
|
useOpenModeContext,
|
||||||
useSchemaToolbar,
|
useSchemaToolbar,
|
||||||
SecondConFirm,
|
SecondConFirm,
|
||||||
WorkflowConfig,
|
|
||||||
AfterSuccess,
|
AfterSuccess,
|
||||||
RefreshDataBlockRequest,
|
RefreshDataBlockRequest,
|
||||||
} from '@nocobase/client';
|
} from '@nocobase/client';
|
||||||
@ -188,14 +187,6 @@ export const bulkEditFormSubmitActionSettings = new SchemaSettings({
|
|||||||
name: 'secondConfirmation',
|
name: 'secondConfirmation',
|
||||||
Component: SecondConFirm,
|
Component: SecondConFirm,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'workflowConfig',
|
|
||||||
Component: WorkflowConfig,
|
|
||||||
useVisible() {
|
|
||||||
const fieldSchema = useFieldSchema();
|
|
||||||
return isValid(fieldSchema?.['x-action-settings']?.triggerWorkflows);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'afterSuccessfulSubmission',
|
name: 'afterSuccessfulSubmission',
|
||||||
Component: AfterSuccess,
|
Component: AfterSuccess,
|
||||||
|
@ -25,7 +25,7 @@ test.describe('ReadPrettyFormActionInitializers & CalendarFormActionInitializers
|
|||||||
const nocoPage = await mockPage(oneCalenderWithViewAction).waitForInit();
|
const nocoPage = await mockPage(oneCalenderWithViewAction).waitForInit();
|
||||||
await mockRecord('general', { singleLineText: 'test' });
|
await mockRecord('general', { singleLineText: 'test' });
|
||||||
await nocoPage.goto();
|
await nocoPage.goto();
|
||||||
await page.getByTitle('test').click();
|
await page.getByLabel('block-item-CardItem-general-').getByLabel('event-title').click();
|
||||||
await page.getByLabel('schema-initializer-ActionBar-details:configureActions-general').hover();
|
await page.getByLabel('schema-initializer-ActionBar-details:configureActions-general').hover();
|
||||||
await page.getByRole('menuitem', { name: 'Print' }).click();
|
await page.getByRole('menuitem', { name: 'Print' }).click();
|
||||||
await page.getByLabel('action-Action-Print-print-general-form').click();
|
await page.getByLabel('action-Action-Print-print-general-form').click();
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { useFieldSchema } from '@formily/react';
|
import { useFieldSchema } from '@formily/react';
|
||||||
import { Action, Icon, useComponent, withDynamicSchemaProps } from '@nocobase/client';
|
import { Action, Icon, useCompile, useComponent, withDynamicSchemaProps } from '@nocobase/client';
|
||||||
import { Avatar } from 'antd';
|
import { Avatar } from 'antd';
|
||||||
import { createStyles } from 'antd-style';
|
import { createStyles } from 'antd-style';
|
||||||
import React, { useContext } from 'react';
|
import React, { useContext } from 'react';
|
||||||
@ -36,13 +36,15 @@ function Button() {
|
|||||||
const backgroundColor = fieldSchema['x-component-props']?.['iconColor'];
|
const backgroundColor = fieldSchema['x-component-props']?.['iconColor'];
|
||||||
const { layout } = useContext(WorkbenchBlockContext);
|
const { layout } = useContext(WorkbenchBlockContext);
|
||||||
const { styles, cx } = useStyles();
|
const { styles, cx } = useStyles();
|
||||||
|
const compile = useCompile();
|
||||||
|
const title = compile(fieldSchema.title);
|
||||||
return layout === WorkbenchLayout.Grid ? (
|
return layout === WorkbenchLayout.Grid ? (
|
||||||
<div title={fieldSchema.title} style={{ width: '100%', overflow: 'hidden' }} className="nb-action-panel-container">
|
<div title={title} style={{ width: '100%', overflow: 'hidden' }} className="nb-action-panel-container">
|
||||||
<Avatar style={{ backgroundColor }} size={54} icon={<Icon type={icon} />} />
|
<Avatar style={{ backgroundColor }} size={54} icon={<Icon type={icon} />} />
|
||||||
<div className={cx(styles.title)}>{fieldSchema.title}</div>
|
<div className={cx(styles.title)}>{title}</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<span>{fieldSchema.title}</span>
|
<span>{title}</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,10 +7,15 @@
|
|||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ButtonEditor, SchemaSettings, SchemaSettingsActionLinkItem, useSchemaInitializer } from '@nocobase/client';
|
import {
|
||||||
|
ButtonEditor,
|
||||||
|
SchemaSettings,
|
||||||
|
SchemaSettingsActionLinkItem,
|
||||||
|
useSchemaInitializer,
|
||||||
|
ModalActionSchemaInitializerItem,
|
||||||
|
} from '@nocobase/client';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { ModalActionSchemaInitializerItem } from './ModalActionSchemaInitializerItem';
|
|
||||||
export const workbenchActionSettingsCustomRequest = new SchemaSettings({
|
export const workbenchActionSettingsCustomRequest = new SchemaSettings({
|
||||||
name: 'workbench:actionSettings:customRequest',
|
name: 'workbench:actionSettings:customRequest',
|
||||||
items: [
|
items: [
|
||||||
|
@ -13,10 +13,10 @@ import {
|
|||||||
SchemaSettingsActionLinkItem,
|
SchemaSettingsActionLinkItem,
|
||||||
useSchemaInitializer,
|
useSchemaInitializer,
|
||||||
useSchemaInitializerItem,
|
useSchemaInitializerItem,
|
||||||
|
ModalActionSchemaInitializerItem,
|
||||||
} from '@nocobase/client';
|
} from '@nocobase/client';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { ModalActionSchemaInitializerItem } from './ModalActionSchemaInitializerItem';
|
|
||||||
|
|
||||||
export const workbenchActionSettingsLink = new SchemaSettings({
|
export const workbenchActionSettingsLink = new SchemaSettings({
|
||||||
name: 'workbench:actionSettings:link',
|
name: 'workbench:actionSettings:link',
|
||||||
|
@ -13,10 +13,10 @@ import {
|
|||||||
SchemaSettings,
|
SchemaSettings,
|
||||||
useSchemaInitializer,
|
useSchemaInitializer,
|
||||||
useOpenModeContext,
|
useOpenModeContext,
|
||||||
|
ModalActionSchemaInitializerItem,
|
||||||
} from '@nocobase/client';
|
} from '@nocobase/client';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { ModalActionSchemaInitializerItem } from './ModalActionSchemaInitializerItem';
|
|
||||||
|
|
||||||
export const workbenchActionSettingsPopup = new SchemaSettings({
|
export const workbenchActionSettingsPopup = new SchemaSettings({
|
||||||
name: 'workbench:actionSettings:popup',
|
name: 'workbench:actionSettings:popup',
|
||||||
|
@ -13,10 +13,10 @@ import {
|
|||||||
SchemaSettings,
|
SchemaSettings,
|
||||||
useSchemaInitializer,
|
useSchemaInitializer,
|
||||||
useSchemaInitializerItem,
|
useSchemaInitializerItem,
|
||||||
|
ModalActionSchemaInitializerItem,
|
||||||
} from '@nocobase/client';
|
} from '@nocobase/client';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { ModalActionSchemaInitializerItem } from './ModalActionSchemaInitializerItem';
|
|
||||||
|
|
||||||
export const workbenchActionSettingsScanQrCode = new SchemaSettings({
|
export const workbenchActionSettingsScanQrCode = new SchemaSettings({
|
||||||
name: 'workbench:actionSettings:scanQrCode',
|
name: 'workbench:actionSettings:scanQrCode',
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
import { expect, test } from '@nocobase/test/e2e';
|
import { expect, test } from '@nocobase/test/e2e';
|
||||||
import { backgroundColorFieldBasic } from './templates';
|
import { backgroundColorFieldBasic } from './templates';
|
||||||
|
|
||||||
test.describe('Background color field', () => {
|
test.describe('Color field', () => {
|
||||||
test('basic', async ({ mockPage, mockRecords, page }) => {
|
test('basic', async ({ mockPage, mockRecords, page }) => {
|
||||||
const nocoPage = await mockPage(backgroundColorFieldBasic).waitForInit();
|
const nocoPage = await mockPage(backgroundColorFieldBasic).waitForInit();
|
||||||
await mockRecords('calendar', 3);
|
await mockRecords('calendar', 3);
|
||||||
@ -19,18 +19,22 @@ test.describe('Background color field', () => {
|
|||||||
// 1. The default option is Not selected
|
// 1. The default option is Not selected
|
||||||
await page.getByLabel('block-item-CardItem-calendar-').hover();
|
await page.getByLabel('block-item-CardItem-calendar-').hover();
|
||||||
await page.getByLabel('designer-schema-settings-CardItem-blockSettings:calendar-calendar').hover();
|
await page.getByLabel('designer-schema-settings-CardItem-blockSettings:calendar-calendar').hover();
|
||||||
await page.getByRole('menuitem', { name: 'Background color field Not selected' }).click();
|
await page.getByRole('menuitem', { name: 'Color field Not selected' }).click();
|
||||||
|
|
||||||
// 2. Switch to the single select option
|
// 2. Switch to the single select option
|
||||||
await page.getByRole('option', { name: 'Single select' }).click();
|
await page.getByRole('option', { name: 'Single select' }).click();
|
||||||
await expect(page.getByRole('menuitem', { name: 'Background color field Single select' })).toBeVisible();
|
await page.getByLabel('block-item-CardItem-calendar-').hover();
|
||||||
|
await page.getByLabel('designer-schema-settings-CardItem-blockSettings:calendar-calendar').hover();
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Color field Single select' })).toBeVisible();
|
||||||
await page.mouse.move(-300, 0);
|
await page.mouse.move(-300, 0);
|
||||||
|
|
||||||
// 3. Switch to the radio group option
|
// 3. Switch to the radio group option
|
||||||
await page.getByLabel('block-item-CardItem-calendar-').hover();
|
await page.getByLabel('block-item-CardItem-calendar-').hover();
|
||||||
await page.getByLabel('designer-schema-settings-CardItem-blockSettings:calendar-calendar').hover();
|
await page.getByLabel('designer-schema-settings-CardItem-blockSettings:calendar-calendar').hover();
|
||||||
await page.getByRole('menuitem', { name: 'Background color field Single select' }).click();
|
await page.getByRole('menuitem', { name: 'Color field Single select' }).click();
|
||||||
await page.getByRole('option', { name: 'Radio group' }).click();
|
await page.getByRole('option', { name: 'Radio group' }).click();
|
||||||
await expect(page.getByRole('menuitem', { name: 'Background color field Radio group' })).toBeVisible();
|
await page.getByLabel('block-item-CardItem-calendar-').hover();
|
||||||
|
await page.getByLabel('designer-schema-settings-CardItem-blockSettings:calendar-calendar').hover();
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Color field Radio group' })).toBeVisible();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -30,10 +30,11 @@ import {
|
|||||||
useToken,
|
useToken,
|
||||||
withDynamicSchemaProps,
|
withDynamicSchemaProps,
|
||||||
withSkeletonComponent,
|
withSkeletonComponent,
|
||||||
|
useApp,
|
||||||
} from '@nocobase/client';
|
} from '@nocobase/client';
|
||||||
import type { Dayjs } from 'dayjs';
|
import type { Dayjs } from 'dayjs';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { cloneDeep, get } from 'lodash';
|
import { cloneDeep, get, omit } from 'lodash';
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { View } from 'react-big-calendar';
|
import { View } from 'react-big-calendar';
|
||||||
import { i18nt, useTranslation } from '../../locale';
|
import { i18nt, useTranslation } from '../../locale';
|
||||||
@ -116,6 +117,8 @@ const useEvents = (
|
|||||||
);
|
);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { fields } = useCollection();
|
const { fields } = useCollection();
|
||||||
|
const app = useApp();
|
||||||
|
const plugin = app.pm.get('calendar') as any;
|
||||||
const labelUiSchema = fields.find((v) => v.name === fieldNames?.title)?.uiSchema;
|
const labelUiSchema = fields.find((v) => v.name === fieldNames?.title)?.uiSchema;
|
||||||
const enumUiSchema = fields.find((v) => v.name === fieldNames?.colorFieldName);
|
const enumUiSchema = fields.find((v) => v.name === fieldNames?.colorFieldName);
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
@ -164,7 +167,10 @@ const useEvents = (
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (res) return out;
|
if (res) return out;
|
||||||
const title = getLabelFormatValue(labelUiSchema, get(item, fieldNames.title), true);
|
const targetTitleCollectionField = fields.find((v) => v.name === fieldNames.title);
|
||||||
|
const targetTitle = plugin.getTitleFieldInterface(targetTitleCollectionField.interface);
|
||||||
|
const title = getLabelFormatValue(labelUiSchema, get(item, fieldNames.title), true, targetTitle?.TitleRenderer);
|
||||||
|
|
||||||
const event: Event = {
|
const event: Event = {
|
||||||
id: get(item, fieldNames.id || 'id'),
|
id: get(item, fieldNames.id || 'id'),
|
||||||
colorFieldValue: item[fieldNames.colorFieldName],
|
colorFieldValue: item[fieldNames.colorFieldName],
|
||||||
@ -275,7 +281,7 @@ export const Calendar: any = withDynamicSchemaProps(
|
|||||||
}, [reactBigCalendar]);
|
}, [reactBigCalendar]);
|
||||||
|
|
||||||
// 新版 UISchema(1.0 之后)中已经废弃了 useProps,这里之所以继续保留是为了兼容旧版的 UISchema
|
// 新版 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 height = useCalenderHeight();
|
||||||
const [date, setDate] = useState<Date>(new Date());
|
const [date, setDate] = useState<Date>(new Date());
|
||||||
const [view, setView] = useState<View>(props.defaultView || 'month');
|
const [view, setView] = useState<View>(props.defaultView || 'month');
|
||||||
@ -285,7 +291,6 @@ export const Calendar: any = withDynamicSchemaProps(
|
|||||||
const parentRecordData = useCollectionParentRecordData();
|
const parentRecordData = useCollectionParentRecordData();
|
||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
const field = useField();
|
const field = useField();
|
||||||
const { token } = useToken();
|
|
||||||
//nint deal with slot select to show create popup
|
//nint deal with slot select to show create popup
|
||||||
const { parseAction } = useACLRoleContext();
|
const { parseAction } = useACLRoleContext();
|
||||||
const collection = useCollection();
|
const collection = useCollection();
|
||||||
@ -296,6 +301,8 @@ export const Calendar: any = withDynamicSchemaProps(
|
|||||||
const ctx = useActionContext();
|
const ctx = useActionContext();
|
||||||
const [visibleAddNewer, setVisibleAddNewer] = useState(false);
|
const [visibleAddNewer, setVisibleAddNewer] = useState(false);
|
||||||
const [currentSelectDate, setCurrentSelectDate] = useState(undefined);
|
const [currentSelectDate, setCurrentSelectDate] = useState(undefined);
|
||||||
|
const colorCollectionField = collection.getField(fieldNames.colorFieldName);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setView(props.defaultView);
|
setView(props.defaultView);
|
||||||
}, [props.defaultView]);
|
}, [props.defaultView]);
|
||||||
@ -339,10 +346,17 @@ export const Calendar: any = withDynamicSchemaProps(
|
|||||||
|
|
||||||
const eventPropGetter = (event: Event) => {
|
const eventPropGetter = (event: Event) => {
|
||||||
if (event.colorFieldValue) {
|
if (event.colorFieldValue) {
|
||||||
const fontColor = token[`${getColorString(event.colorFieldValue, enumList)}7`];
|
const fontColor = getFontColor?.(event.colorFieldValue);
|
||||||
const backgroundColor = token[`${getColorString(event.colorFieldValue, enumList)}1`];
|
const backgroundColor = getBackgroundColor?.(event.colorFieldValue);
|
||||||
|
const style = {};
|
||||||
|
if (fontColor) {
|
||||||
|
style['color'] = fontColor;
|
||||||
|
}
|
||||||
|
if (backgroundColor) {
|
||||||
|
style['backgroundColor'] = backgroundColor;
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
style: { color: fontColor, backgroundColor, border: 'none' },
|
style,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -435,7 +449,7 @@ export const Calendar: any = withDynamicSchemaProps(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
record.__event = {
|
record.__event = {
|
||||||
...event,
|
...omit(event, 'title'),
|
||||||
start: formatDate(dayjs(event.start)),
|
start: formatDate(dayjs(event.start)),
|
||||||
end: formatDate(dayjs(event.end)),
|
end: formatDate(dayjs(event.end)),
|
||||||
};
|
};
|
||||||
|
@ -24,6 +24,7 @@ import {
|
|||||||
useDesignable,
|
useDesignable,
|
||||||
useFormBlockContext,
|
useFormBlockContext,
|
||||||
usePopupSettings,
|
usePopupSettings,
|
||||||
|
useApp,
|
||||||
} from '@nocobase/client';
|
} from '@nocobase/client';
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { useTranslation } from '../../locale';
|
import { useTranslation } from '../../locale';
|
||||||
@ -73,14 +74,17 @@ export const calendarBlockSettings = new SchemaSettings({
|
|||||||
const fieldNames = fieldSchema?.['x-decorator-props']?.['fieldNames'] || {};
|
const fieldNames = fieldSchema?.['x-decorator-props']?.['fieldNames'] || {};
|
||||||
const { service } = useCalendarBlockContext();
|
const { service } = useCalendarBlockContext();
|
||||||
const { getCollectionFieldsOptions } = useCollectionManager_deprecated();
|
const { getCollectionFieldsOptions } = useCollectionManager_deprecated();
|
||||||
const { name, title } = useCollection();
|
const { name } = useCollection();
|
||||||
|
const app = useApp();
|
||||||
|
const plugin = app.pm.get('calendar') as any;
|
||||||
|
const { titleFieldInterfaces } = plugin;
|
||||||
|
|
||||||
const field = useField();
|
const field = useField();
|
||||||
const { dn } = useDesignable();
|
const { dn } = useDesignable();
|
||||||
return {
|
return {
|
||||||
title: t('Title field'),
|
title: t('Title field'),
|
||||||
value: fieldNames.title,
|
value: fieldNames.title,
|
||||||
options: getCollectionFieldsOptions(name, 'string'),
|
options: getCollectionFieldsOptions(name, null, Object.keys(titleFieldInterfaces)),
|
||||||
onChange: (title) => {
|
onChange: (title) => {
|
||||||
const fieldNames = field.decoratorProps.fieldNames || {};
|
const fieldNames = field.decoratorProps.fieldNames || {};
|
||||||
fieldNames['title'] = title;
|
fieldNames['title'] = title;
|
||||||
@ -112,13 +116,14 @@ export const calendarBlockSettings = new SchemaSettings({
|
|||||||
const { name } = useCollection();
|
const { name } = useCollection();
|
||||||
const field = useField();
|
const field = useField();
|
||||||
const { dn } = useDesignable();
|
const { dn } = useDesignable();
|
||||||
const fliedList = getCollectionFieldsOptions(name, 'string');
|
const app = useApp();
|
||||||
const filteredItems = [
|
const plugin = app.pm.get('calendar') as any;
|
||||||
{ label: t('Not selected'), value: '' },
|
const { colorFieldInterfaces } = plugin;
|
||||||
...fliedList.filter((item) => item.interface === 'radioGroup' || item.interface === 'select'),
|
const fliedList = getCollectionFieldsOptions(name, null, Object.keys(colorFieldInterfaces));
|
||||||
];
|
const filteredItems = [{ label: t('Not selected'), value: '' }, ...fliedList];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: t('Background color field'),
|
title: t('Color field'),
|
||||||
value: fieldNames.colorFieldName || '',
|
value: fieldNames.colorFieldName || '',
|
||||||
options: filteredItems,
|
options: filteredItems,
|
||||||
onChange: (colorFieldName: string) => {
|
onChange: (colorFieldName: string) => {
|
||||||
@ -230,10 +235,13 @@ export const calendarBlockSettings = new SchemaSettings({
|
|||||||
const { dn } = useDesignable();
|
const { dn } = useDesignable();
|
||||||
const { service } = useCalendarBlockContext();
|
const { service } = useCalendarBlockContext();
|
||||||
const { name } = useCollection();
|
const { name } = useCollection();
|
||||||
|
const app = useApp();
|
||||||
|
const plugin = app.pm.get('calendar') as any;
|
||||||
|
const { dateTimeFields } = plugin;
|
||||||
return {
|
return {
|
||||||
title: t('Start date field'),
|
title: t('Start date field'),
|
||||||
value: fieldNames.start,
|
value: fieldNames.start,
|
||||||
options: getCollectionFieldsOptions(name, ['date', 'datetime', 'dateOnly', 'datetimeNoTz', 'unixTimestamp'], {
|
options: getCollectionFieldsOptions(name, null, dateTimeFields, {
|
||||||
association: ['o2o', 'obo', 'oho', 'm2o'],
|
association: ['o2o', 'obo', 'oho', 'm2o'],
|
||||||
}),
|
}),
|
||||||
onChange: (start) => {
|
onChange: (start) => {
|
||||||
@ -265,10 +273,13 @@ export const calendarBlockSettings = new SchemaSettings({
|
|||||||
const { dn } = useDesignable();
|
const { dn } = useDesignable();
|
||||||
const { name } = useCollection();
|
const { name } = useCollection();
|
||||||
const fieldNames = fieldSchema?.['x-decorator-props']?.['fieldNames'] || {};
|
const fieldNames = fieldSchema?.['x-decorator-props']?.['fieldNames'] || {};
|
||||||
|
const app = useApp();
|
||||||
|
const plugin = app.pm.get('calendar') as any;
|
||||||
|
const { dateTimeFields } = plugin;
|
||||||
return {
|
return {
|
||||||
title: t('End date field'),
|
title: t('End date field'),
|
||||||
value: fieldNames.end,
|
value: fieldNames.end,
|
||||||
options: getCollectionFieldsOptions(name, ['date', 'datetime', 'dateOnly', 'datetimeNoTz', 'unixTimestamp'], {
|
options: getCollectionFieldsOptions(name, null, dateTimeFields, {
|
||||||
association: ['o2o', 'obo', 'oho', 'm2o'],
|
association: ['o2o', 'obo', 'oho', 'm2o'],
|
||||||
}),
|
}),
|
||||||
onChange: (end) => {
|
onChange: (end) => {
|
||||||
|
@ -6,8 +6,8 @@
|
|||||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
*/
|
*/
|
||||||
|
import React from 'react';
|
||||||
import { Plugin } from '@nocobase/client';
|
import { Plugin, useToken } from '@nocobase/client';
|
||||||
import { generateNTemplate } from '../locale';
|
import { generateNTemplate } from '../locale';
|
||||||
import { CalendarV2 } from './calendar';
|
import { CalendarV2 } from './calendar';
|
||||||
import { calendarBlockSettings } from './calendar/Calender.Settings';
|
import { calendarBlockSettings } from './calendar/Calender.Settings';
|
||||||
@ -27,7 +27,79 @@ import {
|
|||||||
useCreateCalendarBlock,
|
useCreateCalendarBlock,
|
||||||
} from './schema-initializer/items';
|
} from './schema-initializer/items';
|
||||||
|
|
||||||
|
const TitleRenderer = ({ value }) => {
|
||||||
|
return <span aria-label="event-title">{value || 'N/A'}</span>;
|
||||||
|
};
|
||||||
|
interface ColorFunctions {
|
||||||
|
loading: boolean;
|
||||||
|
getFontColor: (value: any) => string; // 返回字体颜色
|
||||||
|
getBackgroundColor: (value: any) => string; // 返回背景颜色
|
||||||
|
}
|
||||||
|
|
||||||
|
const useGetColor = (field) => {
|
||||||
|
const { token } = useToken();
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
getFontColor(value) {
|
||||||
|
const option = field.uiSchema.enum.find((item) => item.value === value);
|
||||||
|
if (option) {
|
||||||
|
return token[`${option.color}7`];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
getBackgroundColor(value) {
|
||||||
|
const option = field.uiSchema.enum.find((item) => item.value === value);
|
||||||
|
if (option) {
|
||||||
|
return token[`${option.color}1`];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type TitleRendererProps = { value: any };
|
||||||
|
|
||||||
export class PluginCalendarClient extends Plugin {
|
export class PluginCalendarClient extends Plugin {
|
||||||
|
titleFieldInterfaces: { [T: string]: { TitleRenderer: React.FC<TitleRendererProps> } } = {
|
||||||
|
input: { TitleRenderer },
|
||||||
|
select: { TitleRenderer },
|
||||||
|
phone: { TitleRenderer },
|
||||||
|
email: { TitleRenderer },
|
||||||
|
radioGroup: { TitleRenderer },
|
||||||
|
};
|
||||||
|
colorFieldInterfaces: {
|
||||||
|
[T: string]: { useGetColor: (field: any) => ColorFunctions };
|
||||||
|
} = {
|
||||||
|
select: { useGetColor },
|
||||||
|
radioGroup: { useGetColor },
|
||||||
|
};
|
||||||
|
|
||||||
|
dateTimeFieldInterfaces = ['date', 'datetime', 'dateOnly', 'datetimeNoTz', 'unixTimestamp', 'createdAt', 'updatedAt'];
|
||||||
|
|
||||||
|
registerTitleFieldInterface(key: string, options: { TitleRenderer: React.FC<TitleRendererProps> }) {
|
||||||
|
this.titleFieldInterfaces[key] = options;
|
||||||
|
}
|
||||||
|
getTitleFieldInterface(key: string) {
|
||||||
|
if (key) {
|
||||||
|
return this.titleFieldInterfaces[key];
|
||||||
|
} else {
|
||||||
|
return this.titleFieldInterfaces;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
registerDateTimeFieldInterface(data: string | string[]) {
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
const result = this.dateTimeFieldInterfaces.concat(data);
|
||||||
|
this.dateTimeFieldInterfaces = result;
|
||||||
|
} else {
|
||||||
|
this.dateTimeFieldInterfaces.push(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
registerColorFieldInterface(type, option: { useGetColor: (field: any) => ColorFunctions }) {
|
||||||
|
this.colorFieldInterfaces[type] = option;
|
||||||
|
}
|
||||||
|
getColorFieldInterface(type: string) {
|
||||||
|
return this.colorFieldInterfaces[type];
|
||||||
|
}
|
||||||
async load() {
|
async load() {
|
||||||
this.app.dataSourceManager.addCollectionTemplates([CalendarCollectionTemplate]);
|
this.app.dataSourceManager.addCollectionTemplates([CalendarCollectionTemplate]);
|
||||||
this.app.schemaInitializerManager.addItem('page:addBlock', 'dataBlocks.calendar', {
|
this.app.schemaInitializerManager.addItem('page:addBlock', 'dataBlocks.calendar', {
|
||||||
|
@ -9,8 +9,8 @@
|
|||||||
|
|
||||||
import { ArrayField } from '@formily/core';
|
import { ArrayField } from '@formily/core';
|
||||||
import { useField, useFieldSchema } from '@formily/react';
|
import { useField, useFieldSchema } from '@formily/react';
|
||||||
import { BlockProvider, useBlockRequestContext, withDynamicSchemaProps } from '@nocobase/client';
|
import { BlockProvider, useBlockRequestContext, withDynamicSchemaProps, useApp, useCollection } from '@nocobase/client';
|
||||||
import React, { createContext, useContext, useEffect } from 'react';
|
import React, { createContext, useContext, useEffect, useState, useMemo, useRef } from 'react';
|
||||||
import { useCalendarBlockParams } from '../hooks/useCalendarBlockParams';
|
import { useCalendarBlockParams } from '../hooks/useCalendarBlockParams';
|
||||||
|
|
||||||
export const CalendarBlockContext = createContext<any>({});
|
export const CalendarBlockContext = createContext<any>({});
|
||||||
@ -57,11 +57,12 @@ export const CalendarBlockProvider = withDynamicSchemaProps(
|
|||||||
if (parseVariableLoading) {
|
if (parseVariableLoading) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div key={props.fieldNames.colorFieldName}>
|
||||||
<BlockProvider name="calendar" {...props} params={params}>
|
<BlockProvider name="calendar" {...props} params={params}>
|
||||||
<InternalCalendarBlockProvider {...props} />
|
<InternalCalendarBlockProvider {...props} />
|
||||||
</BlockProvider>
|
</BlockProvider>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
{ displayName: 'CalendarBlockProvider' },
|
{ displayName: 'CalendarBlockProvider' },
|
||||||
@ -71,18 +72,40 @@ export const useCalendarBlockContext = () => {
|
|||||||
return useContext(CalendarBlockContext);
|
return useContext(CalendarBlockContext);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const useDefaultGetColor = () => {
|
||||||
|
return {
|
||||||
|
getFontColor(value) {
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
getBackgroundColor(value) {
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const useCalendarBlockProps = () => {
|
export const useCalendarBlockProps = () => {
|
||||||
const ctx = useCalendarBlockContext();
|
const ctx = useCalendarBlockContext();
|
||||||
const field = useField<ArrayField>();
|
const field = useField<ArrayField>();
|
||||||
|
const app = useApp();
|
||||||
|
const plugin = app.pm.get('calendar') as any;
|
||||||
|
const collection = useCollection();
|
||||||
|
const colorCollectionField = collection.getField(ctx.fieldNames.colorFieldName);
|
||||||
|
const pluginColorField = plugin.getColorFieldInterface(colorCollectionField?.interface) || {};
|
||||||
|
const useGetColor = pluginColorField.useGetColor || useDefaultGetColor;
|
||||||
|
const { getFontColor, getBackgroundColor } = useGetColor(colorCollectionField) || {};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!ctx?.service?.loading) {
|
if (!ctx?.service?.loading) {
|
||||||
field.componentProps.dataSource = ctx?.service?.data?.data;
|
field.componentProps.dataSource = ctx?.service?.data?.data;
|
||||||
}
|
}
|
||||||
}, [ctx?.service?.loading]);
|
}, [ctx?.service?.loading]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
fieldNames: ctx.fieldNames,
|
fieldNames: ctx.fieldNames,
|
||||||
showLunar: ctx.showLunar,
|
showLunar: ctx.showLunar,
|
||||||
defaultView: ctx.defaultView,
|
defaultView: ctx.defaultView,
|
||||||
fixedBlock: ctx.fixedBlock,
|
fixedBlock: ctx.fixedBlock,
|
||||||
|
getFontColor,
|
||||||
|
getBackgroundColor,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -21,6 +21,8 @@ import {
|
|||||||
useGlobalTheme,
|
useGlobalTheme,
|
||||||
useSchemaInitializer,
|
useSchemaInitializer,
|
||||||
useSchemaInitializerItem,
|
useSchemaInitializerItem,
|
||||||
|
useApp,
|
||||||
|
useCompile,
|
||||||
} from '@nocobase/client';
|
} from '@nocobase/client';
|
||||||
import React, { useContext } from 'react';
|
import React, { useContext } from 'react';
|
||||||
import { useTranslation } from '../../../locale';
|
import { useTranslation } from '../../../locale';
|
||||||
@ -67,17 +69,23 @@ export const useCreateCalendarBlock = () => {
|
|||||||
const { getCollectionField, getCollectionFieldsOptions } = useCollectionManager_deprecated();
|
const { getCollectionField, getCollectionFieldsOptions } = useCollectionManager_deprecated();
|
||||||
const options = useContext(SchemaOptionsContext);
|
const options = useContext(SchemaOptionsContext);
|
||||||
const { theme } = useGlobalTheme();
|
const { theme } = useGlobalTheme();
|
||||||
|
const app = useApp();
|
||||||
|
const plugin = app.pm.get('calendar') as any;
|
||||||
|
const { titleFieldInterfaces, dateTimeFieldInterfaces } = plugin;
|
||||||
|
|
||||||
const createCalendarBlock = async ({ item }) => {
|
const createCalendarBlock = async ({ item }) => {
|
||||||
const stringFieldsOptions = getCollectionFieldsOptions(item.name, 'string', { dataSource: item.dataSource });
|
const titleFieldsOptions = getCollectionFieldsOptions(
|
||||||
const dateFieldsOptions = getCollectionFieldsOptions(
|
|
||||||
item.name,
|
item.name,
|
||||||
['date', 'datetime', 'dateOnly', 'datetimeNoTz', 'unixTimestamp'],
|
null,
|
||||||
|
Object.keys(titleFieldInterfaces).map((v) => v || v),
|
||||||
{
|
{
|
||||||
association: ['o2o', 'obo', 'oho', 'm2o'],
|
|
||||||
dataSource: item.dataSource,
|
dataSource: item.dataSource,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
const dateFieldsOptions = getCollectionFieldsOptions(item.name, null, dateTimeFieldInterfaces, {
|
||||||
|
association: ['o2o', 'obo', 'oho', 'm2o'],
|
||||||
|
dataSource: item.dataSource,
|
||||||
|
});
|
||||||
|
|
||||||
const values = await FormDialog(
|
const values = await FormDialog(
|
||||||
t('Create calendar block'),
|
t('Create calendar block'),
|
||||||
@ -90,7 +98,7 @@ export const useCreateCalendarBlock = () => {
|
|||||||
properties: {
|
properties: {
|
||||||
title: {
|
title: {
|
||||||
title: t('Title field'),
|
title: t('Title field'),
|
||||||
enum: stringFieldsOptions,
|
enum: titleFieldsOptions,
|
||||||
required: true,
|
required: true,
|
||||||
'x-component': 'Select',
|
'x-component': 'Select',
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
|
@ -1,22 +1,4 @@
|
|||||||
/**
|
{
|
||||||
* This file is part of the NocoBase (R) project.
|
|
||||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
||||||
* Authors: NocoBase Team.
|
|
||||||
*
|
|
||||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This file is part of the NocoBase (R) project.
|
|
||||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
||||||
* Authors: NocoBase Team.
|
|
||||||
*
|
|
||||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export default {
|
|
||||||
"Configure calendar": "Configure calendar",
|
"Configure calendar": "Configure calendar",
|
||||||
"Title field": "Title field",
|
"Title field": "Title field",
|
||||||
"Custom title": "Custom title",
|
"Custom title": "Custom title",
|
||||||
@ -60,6 +42,6 @@ export default {
|
|||||||
"Monthly": "Monthly",
|
"Monthly": "Monthly",
|
||||||
"Yearly": "Yearly",
|
"Yearly": "Yearly",
|
||||||
"Repeats": "Repeats",
|
"Repeats": "Repeats",
|
||||||
"Background color field": "Background color field",
|
"Color field": "Color field",
|
||||||
"Not selected": "Not selected",
|
"Not selected": "Not selected"
|
||||||
};
|
}
|
@ -1,22 +1,4 @@
|
|||||||
/**
|
{
|
||||||
* This file is part of the NocoBase (R) project.
|
|
||||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
||||||
* Authors: NocoBase Team.
|
|
||||||
*
|
|
||||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This file is part of the NocoBase (R) project.
|
|
||||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
||||||
* Authors: NocoBase Team.
|
|
||||||
*
|
|
||||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export default {
|
|
||||||
"Configure calendar": "Configurar calendario",
|
"Configure calendar": "Configurar calendario",
|
||||||
"Title field": "Campo de título",
|
"Title field": "Campo de título",
|
||||||
"Custom title": "Título personalizado",
|
"Custom title": "Título personalizado",
|
||||||
@ -60,6 +42,6 @@ export default {
|
|||||||
"Monthly": "Mensual",
|
"Monthly": "Mensual",
|
||||||
"Yearly": "Anual",
|
"Yearly": "Anual",
|
||||||
"Repeats": "se repite",
|
"Repeats": "se repite",
|
||||||
"Background color field": "Campo de color de fondo",
|
"Color field": "Campo de color",
|
||||||
"Not selected": "No seleccionado",
|
"Not selected": "No seleccionado"
|
||||||
};
|
}
|
@ -1,13 +1,4 @@
|
|||||||
/**
|
{
|
||||||
* This file is part of the NocoBase (R) project.
|
|
||||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
||||||
* Authors: NocoBase Team.
|
|
||||||
*
|
|
||||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export default {
|
|
||||||
"Configure calendar": "Configurer le calendrier",
|
"Configure calendar": "Configurer le calendrier",
|
||||||
"Title field": "Champ de titre",
|
"Title field": "Champ de titre",
|
||||||
"Custom title": "Titre personnalisé",
|
"Custom title": "Titre personnalisé",
|
||||||
@ -51,4 +42,4 @@ export default {
|
|||||||
"Monthly": "Mensuel",
|
"Monthly": "Mensuel",
|
||||||
"Yearly": "Annuel",
|
"Yearly": "Annuel",
|
||||||
"Repeats": "Répétitions"
|
"Repeats": "Répétitions"
|
||||||
};
|
}
|
@ -1,22 +1,4 @@
|
|||||||
/**
|
{
|
||||||
* This file is part of the NocoBase (R) project.
|
|
||||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
||||||
* Authors: NocoBase Team.
|
|
||||||
*
|
|
||||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This file is part of the NocoBase (R) project.
|
|
||||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
||||||
* Authors: NocoBase Team.
|
|
||||||
*
|
|
||||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export default {
|
|
||||||
"Configure calendar": "カレンダーの設定",
|
"Configure calendar": "カレンダーの設定",
|
||||||
"Title field": "タイトルフィールド",
|
"Title field": "タイトルフィールド",
|
||||||
"Start date field": "開始日フィールド",
|
"Start date field": "開始日フィールド",
|
||||||
@ -61,5 +43,5 @@ export default {
|
|||||||
"Monthly": "毎月",
|
"Monthly": "毎月",
|
||||||
"Yearly": "毎年",
|
"Yearly": "毎年",
|
||||||
"Repeats": "繰り返し",
|
"Repeats": "繰り返し",
|
||||||
"Update record": "レコードを更新する",
|
"Update record": "レコードを更新する"
|
||||||
};
|
}
|
@ -1,13 +1,4 @@
|
|||||||
/**
|
{
|
||||||
* This file is part of the NocoBase (R) project.
|
|
||||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
||||||
* Authors: NocoBase Team.
|
|
||||||
*
|
|
||||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export default {
|
|
||||||
"Configure calendar": "캘린더 구성",
|
"Configure calendar": "캘린더 구성",
|
||||||
"Title field": "제목 필드",
|
"Title field": "제목 필드",
|
||||||
"Custom title": "사용자 정의 제목",
|
"Custom title": "사용자 정의 제목",
|
||||||
@ -52,4 +43,4 @@ export default {
|
|||||||
"Monthly": "매월",
|
"Monthly": "매월",
|
||||||
"Yearly": "매년",
|
"Yearly": "매년",
|
||||||
"Repeats": "반복"
|
"Repeats": "반복"
|
||||||
};
|
}
|
@ -1,13 +1,4 @@
|
|||||||
/**
|
{
|
||||||
* This file is part of the NocoBase (R) project.
|
|
||||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
||||||
* Authors: NocoBase Team.
|
|
||||||
*
|
|
||||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export default {
|
|
||||||
"Configure calendar": "Configurar calendário",
|
"Configure calendar": "Configurar calendário",
|
||||||
"Title field": "Campo de título",
|
"Title field": "Campo de título",
|
||||||
"Custom title": "Título personalizado",
|
"Custom title": "Título personalizado",
|
||||||
|
@ -1,13 +1,4 @@
|
|||||||
/**
|
{
|
||||||
* This file is part of the NocoBase (R) project.
|
|
||||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
||||||
* Authors: NocoBase Team.
|
|
||||||
*
|
|
||||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export default {
|
|
||||||
"Configure calendar": "Настроить календарь",
|
"Configure calendar": "Настроить календарь",
|
||||||
"Title field": "Поле заголовка",
|
"Title field": "Поле заголовка",
|
||||||
"Start date field": "Поле даты начала",
|
"Start date field": "Поле даты начала",
|
||||||
|
@ -1,13 +1,4 @@
|
|||||||
/**
|
{
|
||||||
* This file is part of the NocoBase (R) project.
|
|
||||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
||||||
* Authors: NocoBase Team.
|
|
||||||
*
|
|
||||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export default {
|
|
||||||
"Configure calendar": "Takvimi yapılandır",
|
"Configure calendar": "Takvimi yapılandır",
|
||||||
"Title field": "Başlık alanı",
|
"Title field": "Başlık alanı",
|
||||||
"Start date field": "Başlangıç tarihi alanı",
|
"Start date field": "Başlangıç tarihi alanı",
|
||||||
|
@ -1,13 +1,4 @@
|
|||||||
/**
|
{
|
||||||
* This file is part of the NocoBase (R) project.
|
|
||||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
||||||
* Authors: NocoBase Team.
|
|
||||||
*
|
|
||||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export default {
|
|
||||||
"Configure calendar": "Налаштувати календар",
|
"Configure calendar": "Налаштувати календар",
|
||||||
"Title field": "Поле заголовка",
|
"Title field": "Поле заголовка",
|
||||||
"Custom title": "Власний заголовок",
|
"Custom title": "Власний заголовок",
|
||||||
@ -51,4 +42,4 @@ export default {
|
|||||||
"Monthly": "Щомісяця",
|
"Monthly": "Щомісяця",
|
||||||
"Yearly": "Щороку",
|
"Yearly": "Щороку",
|
||||||
"Repeats": "Повторюється"
|
"Repeats": "Повторюється"
|
||||||
};
|
}
|
@ -47,7 +47,7 @@
|
|||||||
"Month": "月",
|
"Month": "月",
|
||||||
"Week": "周",
|
"Week": "周",
|
||||||
"{{count}} more items": "{{count}} 更多事项",
|
"{{count}} more items": "{{count}} 更多事项",
|
||||||
"Background color field": "背景颜色字段",
|
"Color field": "颜色字段",
|
||||||
"Not selected": "未选择",
|
"Not selected": "未选择",
|
||||||
"Default view": "默认视图",
|
"Default view": "默认视图",
|
||||||
"Event open mode": "事项打开方式"
|
"Event open mode": "事项打开方式"
|
||||||
|
@ -27,10 +27,6 @@ test.describe('create collection with preset fields', () => {
|
|||||||
//默认提交的数据符合预期
|
//默认提交的数据符合预期
|
||||||
expect(postData).toMatchObject({
|
expect(postData).toMatchObject({
|
||||||
autoGenId: false,
|
autoGenId: false,
|
||||||
createdAt: true,
|
|
||||||
createdBy: true,
|
|
||||||
updatedAt: true,
|
|
||||||
updatedBy: true,
|
|
||||||
fields: expect.arrayContaining([
|
fields: expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
name: 'id',
|
name: 'id',
|
||||||
@ -79,10 +75,6 @@ test.describe('create collection with preset fields', () => {
|
|||||||
//提交的数据符合预期
|
//提交的数据符合预期
|
||||||
expect(postData).toMatchObject({
|
expect(postData).toMatchObject({
|
||||||
autoGenId: false,
|
autoGenId: false,
|
||||||
createdAt: false,
|
|
||||||
createdBy: false,
|
|
||||||
updatedAt: false,
|
|
||||||
updatedBy: false,
|
|
||||||
fields: expect.arrayContaining([
|
fields: expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
name: 'id',
|
name: 'id',
|
||||||
@ -110,10 +102,6 @@ test.describe('create collection with preset fields', () => {
|
|||||||
//提交的数据符合预期
|
//提交的数据符合预期
|
||||||
expect(postData).toMatchObject({
|
expect(postData).toMatchObject({
|
||||||
autoGenId: false,
|
autoGenId: false,
|
||||||
createdAt: true,
|
|
||||||
createdBy: false,
|
|
||||||
updatedAt: false,
|
|
||||||
updatedBy: false,
|
|
||||||
fields: expect.arrayContaining([
|
fields: expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
name: 'createdAt',
|
name: 'createdAt',
|
||||||
@ -141,10 +129,6 @@ test.describe('create collection with preset fields', () => {
|
|||||||
//提交的数据符合预期
|
//提交的数据符合预期
|
||||||
expect(postData).toMatchObject({
|
expect(postData).toMatchObject({
|
||||||
autoGenId: false,
|
autoGenId: false,
|
||||||
createdAt: false,
|
|
||||||
createdBy: true,
|
|
||||||
updatedAt: false,
|
|
||||||
updatedBy: false,
|
|
||||||
fields: expect.arrayContaining([
|
fields: expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
name: 'createdBy',
|
name: 'createdBy',
|
||||||
@ -172,10 +156,6 @@ test.describe('create collection with preset fields', () => {
|
|||||||
//提交的数据符合预期
|
//提交的数据符合预期
|
||||||
expect(postData).toMatchObject({
|
expect(postData).toMatchObject({
|
||||||
autoGenId: false,
|
autoGenId: false,
|
||||||
createdAt: false,
|
|
||||||
createdBy: false,
|
|
||||||
updatedAt: false,
|
|
||||||
updatedBy: true,
|
|
||||||
fields: expect.arrayContaining([
|
fields: expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
name: 'updatedBy',
|
name: 'updatedBy',
|
||||||
@ -203,10 +183,6 @@ test.describe('create collection with preset fields', () => {
|
|||||||
//提交的数据符合预期
|
//提交的数据符合预期
|
||||||
expect(postData).toMatchObject({
|
expect(postData).toMatchObject({
|
||||||
autoGenId: false,
|
autoGenId: false,
|
||||||
createdAt: false,
|
|
||||||
createdBy: false,
|
|
||||||
updatedAt: true,
|
|
||||||
updatedBy: false,
|
|
||||||
fields: expect.arrayContaining([
|
fields: expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
name: 'updatedAt',
|
name: 'updatedAt',
|
||||||
@ -233,10 +209,6 @@ test.describe('create collection with preset fields', () => {
|
|||||||
//提交的数据符合预期
|
//提交的数据符合预期
|
||||||
expect(postData).toMatchObject({
|
expect(postData).toMatchObject({
|
||||||
autoGenId: false,
|
autoGenId: false,
|
||||||
createdAt: false,
|
|
||||||
createdBy: false,
|
|
||||||
updatedAt: false,
|
|
||||||
updatedBy: false,
|
|
||||||
fields: [],
|
fields: [],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -8,9 +8,131 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Plugin } from '@nocobase/client';
|
import { Plugin } from '@nocobase/client';
|
||||||
|
import { orderBy, reject } from 'lodash';
|
||||||
|
|
||||||
|
type PresetFieldConfig = {
|
||||||
|
order: number; // 定义字段的顺序。
|
||||||
|
description: string; // 字段描述
|
||||||
|
value: {
|
||||||
|
name: string;
|
||||||
|
interface: string;
|
||||||
|
type: string;
|
||||||
|
uiSchema: Record<string, any>;
|
||||||
|
field?: string;
|
||||||
|
[T: string]: any;
|
||||||
|
};
|
||||||
|
};
|
||||||
class PluginDataSourceMainClient extends Plugin {
|
class PluginDataSourceMainClient extends Plugin {
|
||||||
async load() {}
|
collectionPresetFields: { order: number; value: any }[] = [];
|
||||||
|
addCollectionPresetField(config: PresetFieldConfig) {
|
||||||
|
this.collectionPresetFields.push(config);
|
||||||
|
}
|
||||||
|
removeCollectionPresetField(fieldName: string) {
|
||||||
|
this.collectionPresetFields = reject(this.collectionPresetFields, (v) => v.value.name === fieldName);
|
||||||
|
}
|
||||||
|
getCollectionPresetFields() {
|
||||||
|
return orderBy(this.collectionPresetFields, ['order'], ['asc']);
|
||||||
|
}
|
||||||
|
async load() {
|
||||||
|
this.addCollectionPresetField({
|
||||||
|
order: 100,
|
||||||
|
description: '{{t("Primary key, unique identifier, self growth") }}',
|
||||||
|
value: {
|
||||||
|
name: 'id',
|
||||||
|
type: 'bigInt',
|
||||||
|
autoIncrement: true,
|
||||||
|
primaryKey: true,
|
||||||
|
allowNull: false,
|
||||||
|
uiSchema: {
|
||||||
|
type: 'number',
|
||||||
|
title: '{{t("ID")}}',
|
||||||
|
'x-component': 'InputNumber',
|
||||||
|
'x-read-pretty': true,
|
||||||
|
},
|
||||||
|
interface: 'integer',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.addCollectionPresetField({
|
||||||
|
order: 200,
|
||||||
|
description: '{{t("Store the creation time of each record")}}',
|
||||||
|
value: {
|
||||||
|
name: 'createdAt',
|
||||||
|
interface: 'createdAt',
|
||||||
|
type: 'date',
|
||||||
|
field: 'createdAt',
|
||||||
|
uiSchema: {
|
||||||
|
type: 'datetime',
|
||||||
|
title: '{{t("Created at")}}',
|
||||||
|
'x-component': 'DatePicker',
|
||||||
|
'x-component-props': {},
|
||||||
|
'x-read-pretty': true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.addCollectionPresetField({
|
||||||
|
order: 300,
|
||||||
|
description: '{{t("Store the creation user of each record") }}',
|
||||||
|
value: {
|
||||||
|
name: 'createdBy',
|
||||||
|
interface: 'createdBy',
|
||||||
|
type: 'belongsTo',
|
||||||
|
target: 'users',
|
||||||
|
foreignKey: 'createdById',
|
||||||
|
uiSchema: {
|
||||||
|
type: 'object',
|
||||||
|
title: '{{t("Created by")}}',
|
||||||
|
'x-component': 'AssociationField',
|
||||||
|
'x-component-props': {
|
||||||
|
fieldNames: {
|
||||||
|
value: 'id',
|
||||||
|
label: 'nickname',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'x-read-pretty': true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.addCollectionPresetField({
|
||||||
|
order: 400,
|
||||||
|
description: '{{t("Store the last update time of each record")}}',
|
||||||
|
value: {
|
||||||
|
type: 'date',
|
||||||
|
field: 'updatedAt',
|
||||||
|
name: 'updatedAt',
|
||||||
|
interface: 'updatedAt',
|
||||||
|
uiSchema: {
|
||||||
|
type: 'datetime',
|
||||||
|
title: '{{t("Last updated at")}}',
|
||||||
|
'x-component': 'DatePicker',
|
||||||
|
'x-component-props': {},
|
||||||
|
'x-read-pretty': true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.addCollectionPresetField({
|
||||||
|
order: 500,
|
||||||
|
description: '{{t("Store the last update user of each record")}}',
|
||||||
|
value: {
|
||||||
|
type: 'belongsTo',
|
||||||
|
target: 'users',
|
||||||
|
foreignKey: 'updatedById',
|
||||||
|
name: 'updatedBy',
|
||||||
|
interface: 'updatedBy',
|
||||||
|
uiSchema: {
|
||||||
|
type: 'object',
|
||||||
|
title: '{{t("Last updated by")}}',
|
||||||
|
'x-component': 'AssociationField',
|
||||||
|
'x-component-props': {
|
||||||
|
fieldNames: {
|
||||||
|
value: 'id',
|
||||||
|
label: 'nickname',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'x-read-pretty': true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default PluginDataSourceMainClient;
|
export default PluginDataSourceMainClient;
|
||||||
|
@ -20,6 +20,7 @@ describe('collections repository', () => {
|
|||||||
await agent.resource('collections').create({
|
await agent.resource('collections').create({
|
||||||
values: {
|
values: {
|
||||||
name: 'tags',
|
name: 'tags',
|
||||||
|
createdAt: true,
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
name: 'title',
|
name: 'title',
|
||||||
@ -31,6 +32,7 @@ describe('collections repository', () => {
|
|||||||
await agent.resource('collections').create({
|
await agent.resource('collections').create({
|
||||||
values: {
|
values: {
|
||||||
name: 'foos',
|
name: 'foos',
|
||||||
|
createdAt: true,
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
name: 'title',
|
name: 'title',
|
||||||
@ -49,6 +51,7 @@ describe('collections repository', () => {
|
|||||||
await agent.resource('collections').create({
|
await agent.resource('collections').create({
|
||||||
values: {
|
values: {
|
||||||
name: 'comments',
|
name: 'comments',
|
||||||
|
createdAt: true,
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
name: 'title',
|
name: 'title',
|
||||||
@ -60,6 +63,7 @@ describe('collections repository', () => {
|
|||||||
await agent.resource('collections').create({
|
await agent.resource('collections').create({
|
||||||
values: {
|
values: {
|
||||||
name: 'posts',
|
name: 'posts',
|
||||||
|
createdAt: true,
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
name: 'title',
|
name: 'title',
|
||||||
@ -329,16 +333,17 @@ describe('collections repository', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
const postId = response.body.data.id;
|
const postId = response.body.data.id;
|
||||||
const response1 = await agent.resource('posts.tags', postId).list({
|
// const response1 = await agent.resource('posts.tags', postId).list({
|
||||||
appends: ['foos'],
|
// appends: ['foos'],
|
||||||
page: 1,
|
// page: 1,
|
||||||
pageSize: 20,
|
// pageSize: 20,
|
||||||
sort: ['-createdAt', '-id'],
|
// sort: ['-createdAt', '-id'],
|
||||||
});
|
// });
|
||||||
|
|
||||||
console.log(JSON.stringify(response1.body.data));
|
console.log('postId', response.body);
|
||||||
|
|
||||||
expect(response1.body.data[0]['id']).toEqual(3);
|
// expect(res
|
||||||
|
// ponse1.body.data[0]['id']).toEqual(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('case 11', async () => {
|
it('case 11', async () => {
|
||||||
@ -486,6 +491,7 @@ describe('collections repository', () => {
|
|||||||
.create({
|
.create({
|
||||||
values: {
|
values: {
|
||||||
name: 'test',
|
name: 'test',
|
||||||
|
createdAt: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -518,6 +524,7 @@ describe('collections repository', () => {
|
|||||||
.create({
|
.create({
|
||||||
values: {
|
values: {
|
||||||
name: 'test',
|
name: 'test',
|
||||||
|
createdAt: true,
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
name: 'testField',
|
name: 'testField',
|
||||||
|
@ -377,7 +377,24 @@ export class PluginDataSourceMainServer extends Plugin {
|
|||||||
}
|
}
|
||||||
await next();
|
await next();
|
||||||
});
|
});
|
||||||
|
this.app.resourceManager.use(async function pushUISchemaWhenUpdateCollectionField(ctx, next) {
|
||||||
|
const { resourceName, actionName } = ctx.action;
|
||||||
|
if (resourceName === 'collections' && actionName === 'create') {
|
||||||
|
const { values } = ctx.action.params;
|
||||||
|
const keys = Object.keys(values);
|
||||||
|
const presetKeys = ['createdAt', 'createdBy', 'updatedAt', 'updatedBy'];
|
||||||
|
for (const presetKey of presetKeys) {
|
||||||
|
if (keys.includes(presetKey)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
values[presetKey] = !!values.fields?.find((v) => v.name === presetKey);
|
||||||
|
}
|
||||||
|
ctx.action.mergeParams({
|
||||||
|
values,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await next();
|
||||||
|
});
|
||||||
this.app.acl.allow('collections', 'list', 'loggedIn');
|
this.app.acl.allow('collections', 'list', 'loggedIn');
|
||||||
this.app.acl.allow('collections', 'listMeta', 'loggedIn');
|
this.app.acl.allow('collections', 'listMeta', 'loggedIn');
|
||||||
this.app.acl.allow('collectionCategories', 'list', 'loggedIn');
|
this.app.acl.allow('collectionCategories', 'list', 'loggedIn');
|
||||||
|
@ -8,14 +8,17 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useCollectionManager_deprecated, useCompile, Variable } from '@nocobase/client';
|
import { useCollectionManager_deprecated, useCompile, Variable, useApp } from '@nocobase/client';
|
||||||
|
|
||||||
export const Expression = (props) => {
|
export const Expression = (props) => {
|
||||||
const { value = '', supports = [], useCurrentFields, onChange } = props;
|
const { value = '', useCurrentFields, onChange } = props;
|
||||||
|
const app = useApp();
|
||||||
|
const plugin = app.pm.get('field-formula') as any;
|
||||||
|
const { expressionFields } = plugin;
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
const { interfaces } = useCollectionManager_deprecated();
|
const { interfaces } = useCollectionManager_deprecated();
|
||||||
|
|
||||||
const fields = (useCurrentFields?.() ?? []).filter((field) => supports.includes(field.interface));
|
const fields = (useCurrentFields?.() ?? []).filter((field) => expressionFields.includes(field.interface));
|
||||||
|
|
||||||
const options = fields.map((field) => ({
|
const options = fields.map((field) => ({
|
||||||
label: compile(field.uiSchema.title),
|
label: compile(field.uiSchema.title),
|
||||||
|
@ -9,11 +9,38 @@
|
|||||||
|
|
||||||
import { Plugin } from '@nocobase/client';
|
import { Plugin } from '@nocobase/client';
|
||||||
import { Formula } from './components';
|
import { Formula } from './components';
|
||||||
import { renderExpressionDescription } from './scopes';
|
|
||||||
import { FormulaFieldInterface } from './interfaces/formula';
|
|
||||||
import { FormulaComponentFieldSettings } from './FormulaComponentFieldSettings';
|
import { FormulaComponentFieldSettings } from './FormulaComponentFieldSettings';
|
||||||
|
import { FormulaFieldInterface } from './interfaces/formula';
|
||||||
|
import { renderExpressionDescription } from './scopes';
|
||||||
|
|
||||||
export class PluginFieldFormulaClient extends Plugin {
|
export class PluginFieldFormulaClient extends Plugin {
|
||||||
|
expressionFields = [
|
||||||
|
'checkbox',
|
||||||
|
'number',
|
||||||
|
'percent',
|
||||||
|
'integer',
|
||||||
|
'number',
|
||||||
|
'percent',
|
||||||
|
'input',
|
||||||
|
'textarea',
|
||||||
|
'email',
|
||||||
|
'phone',
|
||||||
|
'datetime',
|
||||||
|
'createdAt',
|
||||||
|
'updatedAt',
|
||||||
|
'radioGroup',
|
||||||
|
'checkboxGroup',
|
||||||
|
'select',
|
||||||
|
'multipleSelect',
|
||||||
|
];
|
||||||
|
registerExpressionFieldInterface(data: string | string[]) {
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
const result = this.expressionFields.concat(data);
|
||||||
|
this.expressionFields = result;
|
||||||
|
} else {
|
||||||
|
this.expressionFields.push(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
async load() {
|
async load() {
|
||||||
this.app.addComponents({
|
this.app.addComponents({
|
||||||
Formula,
|
Formula,
|
||||||
|
@ -150,31 +150,6 @@ export class FormulaFieldInterface extends CollectionFieldInterface {
|
|||||||
'x-component': 'Formula.Expression',
|
'x-component': 'Formula.Expression',
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
supports: [
|
|
||||||
'checkbox',
|
|
||||||
|
|
||||||
'number',
|
|
||||||
'percent',
|
|
||||||
'integer',
|
|
||||||
'number',
|
|
||||||
'percent',
|
|
||||||
|
|
||||||
'input',
|
|
||||||
'textarea',
|
|
||||||
'email',
|
|
||||||
'phone',
|
|
||||||
|
|
||||||
'datetime',
|
|
||||||
'createdAt',
|
|
||||||
'updatedAt',
|
|
||||||
|
|
||||||
'radioGroup',
|
|
||||||
'checkboxGroup',
|
|
||||||
'select',
|
|
||||||
'multipleSelect',
|
|
||||||
|
|
||||||
// 'json'
|
|
||||||
],
|
|
||||||
useCurrentFields: '{{ useCurrentFields }}',
|
useCurrentFields: '{{ useCurrentFields }}',
|
||||||
// evaluate(exp: string) {
|
// evaluate(exp: string) {
|
||||||
// const { values } = useForm();
|
// const { values } = useForm();
|
||||||
|
@ -121,9 +121,11 @@ const RuleTypes = {
|
|||||||
number: t('Number', { ns: NAMESPACE }),
|
number: t('Number', { ns: NAMESPACE }),
|
||||||
lowercase: t('Lowercase letters', { ns: NAMESPACE }),
|
lowercase: t('Lowercase letters', { ns: NAMESPACE }),
|
||||||
uppercase: t('Uppercase letters', { ns: NAMESPACE }),
|
uppercase: t('Uppercase letters', { ns: NAMESPACE }),
|
||||||
symbol: t('Symbols', { ns: NAMESPACE })
|
symbol: t('Symbols', { ns: NAMESPACE }),
|
||||||
};
|
};
|
||||||
return <code>{value?.map(charset => charsetLabels[charset]).join(', ') || t('Number', { ns: NAMESPACE })}</code>;
|
return (
|
||||||
|
<code>{value?.map((charset) => charsetLabels[charset]).join(', ') || t('Number', { ns: NAMESPACE })}</code>
|
||||||
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
fieldset: {
|
fieldset: {
|
||||||
@ -154,14 +156,14 @@ const RuleTypes = {
|
|||||||
{ value: 'number', label: `{{t("Number", { ns: "${NAMESPACE}" })}}` },
|
{ value: 'number', label: `{{t("Number", { ns: "${NAMESPACE}" })}}` },
|
||||||
{ value: 'lowercase', label: `{{t("Lowercase letters", { ns: "${NAMESPACE}" })}}` },
|
{ value: 'lowercase', label: `{{t("Lowercase letters", { ns: "${NAMESPACE}" })}}` },
|
||||||
{ value: 'uppercase', label: `{{t("Uppercase letters", { ns: "${NAMESPACE}" })}}` },
|
{ value: 'uppercase', label: `{{t("Uppercase letters", { ns: "${NAMESPACE}" })}}` },
|
||||||
{ value: 'symbol', label: `{{t("Symbols", { ns: "${NAMESPACE}" })}}` }
|
{ value: 'symbol', label: `{{t("Symbols", { ns: "${NAMESPACE}" })}}` },
|
||||||
],
|
],
|
||||||
required: true,
|
required: true,
|
||||||
default: ['number'],
|
default: ['number'],
|
||||||
'x-validator': {
|
'x-validator': {
|
||||||
minItems: 1,
|
minItems: 1,
|
||||||
message: `{{t("At least one character set should be selected", { ns: "${NAMESPACE}" })}}`
|
message: `{{t("At least one character set should be selected", { ns: "${NAMESPACE}" })}}`,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaults: {
|
defaults: {
|
||||||
|
@ -301,7 +301,7 @@ const CHAR_SETS = {
|
|||||||
lowercase: 'abcdefghijklmnopqrstuvwxyz',
|
lowercase: 'abcdefghijklmnopqrstuvwxyz',
|
||||||
uppercase: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
|
uppercase: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
|
||||||
// 符号只保留常用且安全的符号,有需要的可以自己加比如[]{}|;:,.<>放在链接或者文件名里容易出问题的字符
|
// 符号只保留常用且安全的符号,有需要的可以自己加比如[]{}|;:,.<>放在链接或者文件名里容易出问题的字符
|
||||||
symbol: '!@#$%^&*_-+'
|
symbol: '!@#$%^&*_-+',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
interface RandomCharOptions {
|
interface RandomCharOptions {
|
||||||
@ -317,21 +317,16 @@ sequencePatterns.register('randomChar', {
|
|||||||
if (!options?.charsets || options.charsets.length === 0) {
|
if (!options?.charsets || options.charsets.length === 0) {
|
||||||
return 'At least one character set should be selected';
|
return 'At least one character set should be selected';
|
||||||
}
|
}
|
||||||
if (options.charsets.some(charset => !CHAR_SETS[charset])) {
|
if (options.charsets.some((charset) => !CHAR_SETS[charset])) {
|
||||||
return 'Invalid charset selected';
|
return 'Invalid charset selected';
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
|
||||||
generate(instance: any, options: RandomCharOptions) {
|
generate(instance: any, options: RandomCharOptions) {
|
||||||
const {
|
const { length = 6, charsets = ['number'] } = options;
|
||||||
length = 6,
|
|
||||||
charsets = ['number']
|
|
||||||
} = options;
|
|
||||||
|
|
||||||
const chars = [...new Set(
|
const chars = [...new Set(charsets.reduce((acc, charset) => acc + CHAR_SETS[charset], ''))];
|
||||||
charsets.reduce((acc, charset) => acc + CHAR_SETS[charset], '')
|
|
||||||
)];
|
|
||||||
|
|
||||||
const getRandomChar = () => {
|
const getRandomChar = () => {
|
||||||
const randomIndex = Math.floor(Math.random() * chars.length);
|
const randomIndex = Math.floor(Math.random() * chars.length);
|
||||||
@ -352,20 +347,27 @@ sequencePatterns.register('randomChar', {
|
|||||||
},
|
},
|
||||||
|
|
||||||
getMatcher(options: RandomCharOptions) {
|
getMatcher(options: RandomCharOptions) {
|
||||||
const pattern = [...new Set(
|
const pattern = [
|
||||||
|
...new Set(
|
||||||
(options.charsets || ['number']).reduce((acc, charset) => {
|
(options.charsets || ['number']).reduce((acc, charset) => {
|
||||||
switch (charset) {
|
switch (charset) {
|
||||||
case 'number': return acc + '0-9';
|
case 'number':
|
||||||
case 'lowercase': return acc + 'a-z';
|
return acc + '0-9';
|
||||||
case 'uppercase': return acc + 'A-Z';
|
case 'lowercase':
|
||||||
case 'symbol': return acc + CHAR_SETS.symbol.replace('-', '').replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&') + '-';
|
return acc + 'a-z';
|
||||||
default: return acc;
|
case 'uppercase':
|
||||||
|
return acc + 'A-Z';
|
||||||
|
case 'symbol':
|
||||||
|
return acc + CHAR_SETS.symbol.replace('-', '').replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&') + '-';
|
||||||
|
default:
|
||||||
|
return acc;
|
||||||
}
|
}
|
||||||
}, '')
|
}, ''),
|
||||||
)].join('');
|
),
|
||||||
|
].join('');
|
||||||
|
|
||||||
return `[${pattern}]{${options.length || 6}}`;
|
return `[${pattern}]{${options.length || 6}}`;
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
interface PatternConfig {
|
interface PatternConfig {
|
||||||
|
@ -22,6 +22,7 @@ import {
|
|||||||
SchemaComponent,
|
SchemaComponent,
|
||||||
SchemaComponentOptions,
|
SchemaComponentOptions,
|
||||||
useAPIClient,
|
useAPIClient,
|
||||||
|
useApp,
|
||||||
useCollectionManager_deprecated,
|
useCollectionManager_deprecated,
|
||||||
useGlobalTheme,
|
useGlobalTheme,
|
||||||
useSchemaInitializer,
|
useSchemaInitializer,
|
||||||
@ -153,10 +154,13 @@ export const useCreateKanbanBlock = () => {
|
|||||||
const options = useContext(SchemaOptionsContext);
|
const options = useContext(SchemaOptionsContext);
|
||||||
const { theme } = useGlobalTheme();
|
const { theme } = useGlobalTheme();
|
||||||
const api = useAPIClient();
|
const api = useAPIClient();
|
||||||
|
const app = useApp();
|
||||||
|
const plugin = app.pm.get('kanban') as any;
|
||||||
|
const groupFieldInterfaces = plugin.getGroupFieldInterface() || [];
|
||||||
const createKanbanBlock = async ({ item }) => {
|
const createKanbanBlock = async ({ item }) => {
|
||||||
const collectionFields = getCollectionFields(item.name, item.dataSource);
|
const collectionFields = getCollectionFields(item.name, item.dataSource);
|
||||||
const fields = collectionFields
|
const fields = collectionFields
|
||||||
?.filter((field) => ['select', 'radioGroup'].includes(field.interface))
|
?.filter((field) => Object.keys(groupFieldInterfaces).find((v) => v === field.interface))
|
||||||
?.map((field) => {
|
?.map((field) => {
|
||||||
return {
|
return {
|
||||||
label: field?.uiSchema?.title,
|
label: field?.uiSchema?.title,
|
||||||
@ -218,13 +222,16 @@ export function useCreateAssociationKanbanBlock() {
|
|||||||
const { theme } = useGlobalTheme();
|
const { theme } = useGlobalTheme();
|
||||||
const { getCollectionFields } = useCollectionManager_deprecated();
|
const { getCollectionFields } = useCollectionManager_deprecated();
|
||||||
const api = useAPIClient();
|
const api = useAPIClient();
|
||||||
|
const app = useApp();
|
||||||
|
|
||||||
const createAssociationKanbanBlock = async ({ item }) => {
|
const createAssociationKanbanBlock = async ({ item }) => {
|
||||||
console.log(item);
|
|
||||||
const field = item.associationField;
|
const field = item.associationField;
|
||||||
const collectionFields = getCollectionFields(item.name, item.dataSource);
|
const collectionFields = getCollectionFields(item.name, item.dataSource);
|
||||||
|
const plugin = app.pm.get('kanban') as any;
|
||||||
|
const groupFieldInterfaces = plugin.getGroupFieldInterface() || [];
|
||||||
|
|
||||||
const fields = collectionFields
|
const fields = collectionFields
|
||||||
?.filter((field) => ['select', 'radioGroup'].includes(field.interface))
|
?.filter((field) => Object.keys(groupFieldInterfaces).find((v) => v === field.interface))
|
||||||
?.map((field) => {
|
?.map((field) => {
|
||||||
return {
|
return {
|
||||||
label: field?.uiSchema?.title,
|
label: field?.uiSchema?.title,
|
||||||
|
@ -12,14 +12,15 @@ import { useField, useFieldSchema } from '@formily/react';
|
|||||||
import {
|
import {
|
||||||
BlockProvider,
|
BlockProvider,
|
||||||
useACLRoleContext,
|
useACLRoleContext,
|
||||||
|
useAPIClient,
|
||||||
useBlockRequestContext,
|
useBlockRequestContext,
|
||||||
useCollection,
|
useCollection,
|
||||||
useCollection_deprecated,
|
useCollection_deprecated,
|
||||||
|
useApp,
|
||||||
} from '@nocobase/client';
|
} from '@nocobase/client';
|
||||||
import { Spin } from 'antd';
|
import { Spin } from 'antd';
|
||||||
import { isEqual } from 'lodash';
|
import { isEqual } from 'lodash';
|
||||||
import React, { createContext, useCallback, useContext, useEffect, useState } from 'react';
|
import React, { createContext, useCallback, useContext, useEffect, useState } from 'react';
|
||||||
import { toColumns } from './Kanban';
|
|
||||||
|
|
||||||
export const KanbanBlockContext = createContext<any>({});
|
export const KanbanBlockContext = createContext<any>({});
|
||||||
KanbanBlockContext.displayName = 'KanbanBlockContext';
|
KanbanBlockContext.displayName = 'KanbanBlockContext';
|
||||||
@ -93,20 +94,54 @@ const useDisableCardDrag = () => {
|
|||||||
return !result;
|
return !result;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const toColumns = (groupCollectionField: any, dataSource: Array<any> = [], primaryKey, options) => {
|
||||||
|
const columns = {
|
||||||
|
__unknown__: {
|
||||||
|
id: '__unknown__',
|
||||||
|
title: 'Unknown',
|
||||||
|
color: 'default',
|
||||||
|
cards: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
options?.forEach((item) => {
|
||||||
|
columns[item.value] = {
|
||||||
|
id: item.value,
|
||||||
|
title: item.label,
|
||||||
|
color: item.color,
|
||||||
|
cards: [],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
dataSource.forEach((ds) => {
|
||||||
|
const value = ds[groupCollectionField.name];
|
||||||
|
if (value && columns[value]) {
|
||||||
|
columns[value].cards.push({ ...ds, id: ds[primaryKey] });
|
||||||
|
} else {
|
||||||
|
columns.__unknown__.cards.push(ds);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (columns.__unknown__.cards.length === 0) {
|
||||||
|
delete columns.__unknown__;
|
||||||
|
}
|
||||||
|
return Object.values(columns);
|
||||||
|
};
|
||||||
|
|
||||||
export const useKanbanBlockProps = () => {
|
export const useKanbanBlockProps = () => {
|
||||||
const field = useField<ArrayField>();
|
const field = useField<ArrayField>();
|
||||||
const ctx = useKanbanBlockContext();
|
const ctx = useKanbanBlockContext();
|
||||||
const [dataSource, setDataSource] = useState([]);
|
const [dataSource, setDataSource] = useState([]);
|
||||||
const primaryKey = useCollection()?.getPrimaryKey();
|
const primaryKey = useCollection()?.getPrimaryKey();
|
||||||
|
const app = useApp();
|
||||||
|
const plugin = app.pm.get('kanban') as any;
|
||||||
|
const targetGroupField = plugin.getGroupFieldInterface(ctx.groupField.interface);
|
||||||
|
const { options } = targetGroupField?.useGetGroupOptions(ctx.groupField) || { options: [] };
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const data = toColumns(ctx.groupField, ctx?.service?.data?.data, primaryKey);
|
const data = toColumns(ctx.groupField, ctx?.service?.data?.data, primaryKey, options);
|
||||||
if (isEqual(field.value, data) && dataSource === field.value) {
|
if (isEqual(field.value, data) && dataSource === field.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
field.value = data;
|
field.value = data;
|
||||||
setDataSource(field.value);
|
setDataSource(field.value);
|
||||||
}, [ctx?.service?.loading]);
|
}, [ctx?.service?.loading, options]);
|
||||||
|
|
||||||
const disableCardDrag = useDisableCardDrag();
|
const disableCardDrag = useDisableCardDrag();
|
||||||
|
|
||||||
|
@ -28,6 +28,6 @@ export const useKanbanBlockHeight = () => {
|
|||||||
|
|
||||||
const blockTitleHeaderHeight = title ? token.fontSizeLG * token.lineHeightLG + token.padding * 2 - 1 : 0;
|
const blockTitleHeaderHeight = title ? token.fontSizeLG * token.lineHeightLG + token.padding * 2 - 1 : 0;
|
||||||
|
|
||||||
const footerheight = token.controlPaddingHorizontal + token.margin + token.paddingLG - token.marginXS;
|
const footerHeight = token.controlPaddingHorizontal + token.margin + token.paddingLG - token.marginXS;
|
||||||
return height - actionBarHeight - kanbanHeaderHeight - footerheight - blockTitleHeaderHeight;
|
return height - actionBarHeight - kanbanHeaderHeight - footerHeight - blockTitleHeaderHeight;
|
||||||
};
|
};
|
||||||
|
@ -43,7 +43,45 @@ const KanbanPluginProvider = React.memo((props) => {
|
|||||||
});
|
});
|
||||||
KanbanPluginProvider.displayName = 'KanbanPluginProvider';
|
KanbanPluginProvider.displayName = 'KanbanPluginProvider';
|
||||||
|
|
||||||
|
type GroupOption = {
|
||||||
|
value: string | number;
|
||||||
|
label: string;
|
||||||
|
color: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CollectionField = {
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
interface: string;
|
||||||
|
[key: string]: any; // 扩展字段
|
||||||
|
};
|
||||||
|
|
||||||
|
type Option = { color?: string; label: string; value: string };
|
||||||
|
type GroupOptions = { options: Option[]; loading?: boolean };
|
||||||
|
type GetGroupOptions = (collectionField: string) => GroupOptions;
|
||||||
|
|
||||||
|
type UseGetGroupOptions = (collectionField: CollectionField) => { options: GroupOption[] };
|
||||||
|
|
||||||
|
const useDefaultGroupFieldsOptions = (collectionField) => {
|
||||||
|
return { options: collectionField.uiSchema.enum };
|
||||||
|
};
|
||||||
class PluginKanbanClient extends Plugin {
|
class PluginKanbanClient extends Plugin {
|
||||||
|
groupFields: { [T: string]: { useGetGroupOptions: GetGroupOptions } } = {
|
||||||
|
select: { useGetGroupOptions: useDefaultGroupFieldsOptions },
|
||||||
|
radioGroup: { useGetGroupOptions: useDefaultGroupFieldsOptions },
|
||||||
|
};
|
||||||
|
|
||||||
|
registerGroupFieldInterface(interfaceName: string, options: { useGetGroupOptions: GetGroupOptions }) {
|
||||||
|
this.groupFields[interfaceName] = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
getGroupFieldInterface(key) {
|
||||||
|
if (key) {
|
||||||
|
return this.groupFields[key];
|
||||||
|
}
|
||||||
|
return this.groupFields;
|
||||||
|
}
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
this.app.use(KanbanPluginProvider);
|
this.app.use(KanbanPluginProvider);
|
||||||
this.app.schemaInitializerManager.add(kanbanCardInitializers_deprecated);
|
this.app.schemaInitializerManager.add(kanbanCardInitializers_deprecated);
|
||||||
|
@ -101,7 +101,7 @@ export const mapBlockSettings = new SchemaSettings({
|
|||||||
const { dn } = useDesignable();
|
const { dn } = useDesignable();
|
||||||
const { service } = useMapBlockContext();
|
const { service } = useMapBlockContext();
|
||||||
const { name } = useCollection();
|
const { name } = useCollection();
|
||||||
const mapFieldOptions = getCollectionFieldsOptions(name, ['point', 'lineString', 'polygon'], {
|
const mapFieldOptions = getCollectionFieldsOptions(name, ['point', 'lineString', 'polygon'], null, {
|
||||||
association: ['o2o', 'obo', 'oho', 'o2m', 'm2o', 'm2m'],
|
association: ['o2o', 'obo', 'oho', 'o2m', 'm2o', 'm2m'],
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
@ -164,7 +164,7 @@ export const mapBlockSettings = new SchemaSettings({
|
|||||||
const { getCollectionFieldsOptions } = useCollectionManager_deprecated();
|
const { getCollectionFieldsOptions } = useCollectionManager_deprecated();
|
||||||
const { name } = useCollection();
|
const { name } = useCollection();
|
||||||
const fieldNames = fieldSchema?.['x-decorator-props']?.['fieldNames'] || {};
|
const fieldNames = fieldSchema?.['x-decorator-props']?.['fieldNames'] || {};
|
||||||
const mapFieldOptions = getCollectionFieldsOptions(name, ['point', 'lineString', 'polygon'], {
|
const mapFieldOptions = getCollectionFieldsOptions(name, ['point', 'lineString', 'polygon'], null, {
|
||||||
association: ['o2o', 'obo', 'oho', 'o2m', 'm2o', 'm2m'],
|
association: ['o2o', 'obo', 'oho', 'o2m', 'm2o', 'm2m'],
|
||||||
});
|
});
|
||||||
const isPointField = findNestedOption(fieldNames.field, mapFieldOptions)?.type === 'point';
|
const isPointField = findNestedOption(fieldNames.field, mapFieldOptions)?.type === 'point';
|
||||||
|
@ -38,11 +38,11 @@ export const MapBlockInitializer = () => {
|
|||||||
componentType={`Map`}
|
componentType={`Map`}
|
||||||
icon={<TableOutlined />}
|
icon={<TableOutlined />}
|
||||||
onCreateBlockSchema={async ({ item }) => {
|
onCreateBlockSchema={async ({ item }) => {
|
||||||
const mapFieldOptions = getCollectionFieldsOptions(item.name, ['point', 'lineString', 'polygon'], {
|
const mapFieldOptions = getCollectionFieldsOptions(item.name, ['point', 'lineString', 'polygon'], null, {
|
||||||
association: ['o2o', 'obo', 'oho', 'o2m', 'm2o', 'm2m'],
|
association: ['o2o', 'obo', 'oho', 'o2m', 'm2o', 'm2m'],
|
||||||
dataSource: item.dataSource,
|
dataSource: item.dataSource,
|
||||||
});
|
});
|
||||||
const markerFieldOptions = getCollectionFieldsOptions(item.name, 'string', {
|
const markerFieldOptions = getCollectionFieldsOptions(item.name, 'string', null, {
|
||||||
dataSource: item.dataSource,
|
dataSource: item.dataSource,
|
||||||
});
|
});
|
||||||
const values = await FormDialog(
|
const values = await FormDialog(
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
import { get, pick } from 'lodash';
|
import { get, pick } from 'lodash';
|
||||||
import { BelongsTo, HasOne } from 'sequelize';
|
import { BelongsTo, HasOne } from 'sequelize';
|
||||||
import { Model, modelAssociationByKey } from '@nocobase/database';
|
import { Collection, Model, modelAssociationByKey } from '@nocobase/database';
|
||||||
import Application, { DefaultContext } from '@nocobase/server';
|
import Application, { DefaultContext } from '@nocobase/server';
|
||||||
import { Context as ActionContext, Next } from '@nocobase/actions';
|
import { Context as ActionContext, Next } from '@nocobase/actions';
|
||||||
|
|
||||||
@ -27,14 +27,10 @@ export default class extends Trigger {
|
|||||||
const self = this;
|
const self = this;
|
||||||
|
|
||||||
async function triggerWorkflowActionMiddleware(context: Context, next: Next) {
|
async function triggerWorkflowActionMiddleware(context: Context, next: Next) {
|
||||||
const { resourceName, actionName } = context.action;
|
|
||||||
|
|
||||||
if (resourceName === 'workflows' && actionName === 'trigger') {
|
|
||||||
return self.workflowTriggerAction(context, next);
|
|
||||||
}
|
|
||||||
|
|
||||||
await next();
|
await next();
|
||||||
|
|
||||||
|
const { actionName } = context.action;
|
||||||
|
|
||||||
if (!['create', 'update'].includes(actionName)) {
|
if (!['create', 'update'].includes(actionName)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -45,20 +41,17 @@ export default class extends Trigger {
|
|||||||
workflow.app.dataSourceManager.use(triggerWorkflowActionMiddleware);
|
workflow.app.dataSourceManager.use(triggerWorkflowActionMiddleware);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
getTargetCollection(collection: Collection, association: string) {
|
||||||
* @deprecated
|
if (!association) {
|
||||||
*/
|
return collection;
|
||||||
async workflowTriggerAction(context: Context, next: Next) {
|
|
||||||
const { triggerWorkflows } = context.action.params;
|
|
||||||
|
|
||||||
if (!triggerWorkflows) {
|
|
||||||
return context.throw(400);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
context.status = 202;
|
let targetCollection = collection;
|
||||||
await next();
|
for (const key of association.split('.')) {
|
||||||
|
targetCollection = collection.db.getCollection(targetCollection.getField(key).target);
|
||||||
|
}
|
||||||
|
|
||||||
return this.collectionTriggerAction(context);
|
return targetCollection;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async collectionTriggerAction(context: Context) {
|
private async collectionTriggerAction(context: Context) {
|
||||||
@ -76,7 +69,6 @@ export default class extends Trigger {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fullCollectionName = joinCollectionName(dataSourceHeader, collection.name);
|
|
||||||
const { currentUser, currentRole } = context.state;
|
const { currentUser, currentRole } = context.state;
|
||||||
const { model: UserModel } = this.workflow.db.getCollection('users');
|
const { model: UserModel } = this.workflow.db.getCollection('users');
|
||||||
const userInfo = {
|
const userInfo = {
|
||||||
@ -92,9 +84,8 @@ export default class extends Trigger {
|
|||||||
const globalWorkflows = new Map();
|
const globalWorkflows = new Map();
|
||||||
const localWorkflows = new Map();
|
const localWorkflows = new Map();
|
||||||
workflows.forEach((item) => {
|
workflows.forEach((item) => {
|
||||||
if (resourceName === 'workflows' && actionName === 'trigger') {
|
const targetCollection = this.getTargetCollection(collection, triggersKeysMap.get(item.key));
|
||||||
localWorkflows.set(item.key, item);
|
if (item.config.collection === joinCollectionName(dataSourceHeader, targetCollection.name)) {
|
||||||
} else if (item.config.collection === fullCollectionName) {
|
|
||||||
if (item.config.global) {
|
if (item.config.global) {
|
||||||
if (item.config.actions?.includes(actionName)) {
|
if (item.config.actions?.includes(actionName)) {
|
||||||
globalWorkflows.set(item.key, item);
|
globalWorkflows.set(item.key, item);
|
||||||
|
@ -207,6 +207,7 @@ describe('workflow > action-trigger', () => {
|
|||||||
type: 'action',
|
type: 'action',
|
||||||
config: {
|
config: {
|
||||||
collection: 'posts',
|
collection: 'posts',
|
||||||
|
appends: ['createdBy'],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -300,155 +301,21 @@ describe('workflow > action-trigger', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated
|
|
||||||
*/
|
|
||||||
describe('directly trigger', () => {
|
|
||||||
it('no collection configured should not be triggered', async () => {
|
|
||||||
const workflow = await WorkflowModel.create({
|
|
||||||
enabled: true,
|
|
||||||
type: 'action',
|
|
||||||
});
|
|
||||||
|
|
||||||
const res1 = await userAgents[0].resource('workflows').trigger({
|
|
||||||
values: { title: 't1' },
|
|
||||||
triggerWorkflows: `${workflow.key}`,
|
|
||||||
});
|
|
||||||
expect(res1.status).toBe(202);
|
|
||||||
|
|
||||||
await sleep(500);
|
|
||||||
|
|
||||||
const e1s = await workflow.getExecutions();
|
|
||||||
expect(e1s.length).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('trigger on form data', async () => {
|
|
||||||
const workflow = await WorkflowModel.create({
|
|
||||||
enabled: true,
|
|
||||||
type: 'action',
|
|
||||||
config: {
|
|
||||||
collection: 'posts',
|
|
||||||
appends: ['createdBy'],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const res1 = await userAgents[0].resource('workflows').trigger({
|
|
||||||
values: { title: 't1' },
|
|
||||||
triggerWorkflows: `${workflow.key}`,
|
|
||||||
});
|
|
||||||
expect(res1.status).toBe(202);
|
|
||||||
|
|
||||||
await sleep(500);
|
|
||||||
|
|
||||||
const [e1] = await workflow.getExecutions();
|
|
||||||
expect(e1.status).toBe(EXECUTION_STATUS.RESOLVED);
|
|
||||||
expect(e1.context.data).toMatchObject({ title: 't1' });
|
|
||||||
expect(e1.context.data.createdBy).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('trigger on record data', async () => {
|
|
||||||
const workflow = await WorkflowModel.create({
|
|
||||||
enabled: true,
|
|
||||||
type: 'action',
|
|
||||||
config: {
|
|
||||||
collection: 'posts',
|
|
||||||
appends: ['createdBy'],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const post = await PostRepo.create({
|
|
||||||
values: { title: 't1', createdBy: users[0].id },
|
|
||||||
});
|
|
||||||
|
|
||||||
const res1 = await userAgents[0].resource('workflows').trigger({
|
|
||||||
values: post.toJSON(),
|
|
||||||
triggerWorkflows: `${workflow.key}`,
|
|
||||||
});
|
|
||||||
expect(res1.status).toBe(202);
|
|
||||||
|
|
||||||
await sleep(500);
|
|
||||||
|
|
||||||
const [e1] = await workflow.getExecutions();
|
|
||||||
expect(e1.status).toBe(EXECUTION_STATUS.RESOLVED);
|
|
||||||
expect(e1.context.data).toMatchObject({ title: 't1' });
|
|
||||||
expect(e1.context.data).toHaveProperty('createdBy');
|
|
||||||
expect(e1.context.data.createdBy.id).toBe(users[0].id);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('multi trigger', async () => {
|
|
||||||
const w1 = await WorkflowModel.create({
|
|
||||||
enabled: true,
|
|
||||||
type: 'action',
|
|
||||||
config: {
|
|
||||||
collection: 'posts',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const w2 = await WorkflowModel.create({
|
|
||||||
enabled: true,
|
|
||||||
type: 'action',
|
|
||||||
config: {
|
|
||||||
collection: 'posts',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const res1 = await userAgents[0].resource('workflows').trigger({
|
|
||||||
values: { title: 't1' },
|
|
||||||
triggerWorkflows: `${w1.key},${w2.key}`,
|
|
||||||
});
|
|
||||||
expect(res1.status).toBe(202);
|
|
||||||
|
|
||||||
await sleep(500);
|
|
||||||
|
|
||||||
const [e1] = await w1.getExecutions();
|
|
||||||
expect(e1.status).toBe(EXECUTION_STATUS.RESOLVED);
|
|
||||||
expect(e1.context.data).toMatchObject({ title: 't1' });
|
|
||||||
|
|
||||||
const [e2] = await w2.getExecutions();
|
|
||||||
expect(e2.status).toBe(EXECUTION_STATUS.RESOLVED);
|
|
||||||
expect(e2.context.data).toMatchObject({ title: 't1' });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('user submitted form', async () => {
|
|
||||||
const workflow = await WorkflowModel.create({
|
|
||||||
enabled: true,
|
|
||||||
type: 'action',
|
|
||||||
config: {
|
|
||||||
collection: 'posts',
|
|
||||||
appends: ['createdBy'],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const res1 = await userAgents[0].resource('posts').create({
|
|
||||||
values: { title: 't1' },
|
|
||||||
triggerWorkflows: `${workflow.key}`,
|
|
||||||
});
|
|
||||||
expect(res1.status).toBe(200);
|
|
||||||
|
|
||||||
await sleep(500);
|
|
||||||
|
|
||||||
const [e1] = await workflow.getExecutions();
|
|
||||||
expect(e1.status).toBe(EXECUTION_STATUS.RESOLVED);
|
|
||||||
expect(e1.context.user).toBeDefined();
|
|
||||||
expect(e1.context.user.id).toBe(users[0].id);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('context data path', () => {
|
describe('context data path', () => {
|
||||||
it('level: 1', async () => {
|
it('level: 1', async () => {
|
||||||
const workflow = await WorkflowModel.create({
|
const workflow = await WorkflowModel.create({
|
||||||
enabled: true,
|
enabled: true,
|
||||||
type: 'action',
|
type: 'action',
|
||||||
config: {
|
config: {
|
||||||
collection: 'posts',
|
collection: 'categories',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const res1 = await userAgents[0].resource('workflows').trigger({
|
const res1 = await userAgents[0].resource('posts').create({
|
||||||
values: { title: 't1', category: { title: 'c1' } },
|
values: { title: 't1', category: { title: 'c1' } },
|
||||||
triggerWorkflows: `${workflow.key}!category`,
|
triggerWorkflows: `${workflow.key}!category`,
|
||||||
});
|
});
|
||||||
expect(res1.status).toBe(202);
|
expect(res1.status).toBe(200);
|
||||||
|
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
|
|
||||||
@ -462,15 +329,15 @@ describe('workflow > action-trigger', () => {
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
type: 'action',
|
type: 'action',
|
||||||
config: {
|
config: {
|
||||||
collection: 'posts',
|
collection: 'categories',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const res1 = await userAgents[0].resource('workflows').trigger({
|
const res1 = await userAgents[0].resource('comments').create({
|
||||||
values: { content: 'comment1', post: { category: { title: 'c1' } } },
|
values: { content: 'comment1', post: { category: { title: 'c1' } } },
|
||||||
triggerWorkflows: `${workflow.key}!post.category`,
|
triggerWorkflows: `${workflow.key}!post.category`,
|
||||||
});
|
});
|
||||||
expect(res1.status).toBe(202);
|
expect(res1.status).toBe(200);
|
||||||
|
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
|
|
||||||
@ -563,11 +430,11 @@ describe('workflow > action-trigger', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const res1 = await userAgents[0].resource('workflows').trigger({
|
const res1 = await userAgents[0].resource('posts').create({
|
||||||
values: { title: 't1' },
|
values: { title: 't1' },
|
||||||
triggerWorkflows: `${w1.key}`,
|
triggerWorkflows: `${w1.key}`,
|
||||||
});
|
});
|
||||||
expect(res1.status).toBe(202);
|
expect(res1.status).toBe(200);
|
||||||
|
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
|
|
||||||
@ -586,11 +453,11 @@ describe('workflow > action-trigger', () => {
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const res3 = await userAgents[0].resource('workflows').trigger({
|
const res3 = await userAgents[0].resource('posts').create({
|
||||||
values: { title: 't2' },
|
values: { title: 't2' },
|
||||||
triggerWorkflows: `${w1.key}`,
|
triggerWorkflows: `${w1.key}`,
|
||||||
});
|
});
|
||||||
expect(res3.status).toBe(202);
|
expect(res3.status).toBe(200);
|
||||||
|
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
|
|
||||||
@ -769,11 +636,11 @@ describe('workflow > action-trigger', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const res1 = await userAgents[0].resource('workflows').trigger({
|
const res1 = await userAgents[0].resource('posts').create({
|
||||||
values: { title: 't1' },
|
values: { title: 't1' },
|
||||||
triggerWorkflows: `${workflow.key}`,
|
triggerWorkflows: `${workflow.key}`,
|
||||||
});
|
});
|
||||||
expect(res1.status).toBe(202);
|
expect(res1.status).toBe(200);
|
||||||
|
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
|
|
||||||
|
@ -157,6 +157,7 @@ function ExecuteActionButton() {
|
|||||||
scope={{
|
scope={{
|
||||||
useCancelAction,
|
useCancelAction,
|
||||||
useExecuteConfirmAction,
|
useExecuteConfirmAction,
|
||||||
|
...trigger.scope,
|
||||||
}}
|
}}
|
||||||
schema={{
|
schema={{
|
||||||
name: `trigger-modal-${workflow.type}-${workflow.id}`,
|
name: `trigger-modal-${workflow.type}-${workflow.id}`,
|
||||||
|
@ -7,11 +7,7 @@
|
|||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import { PagePopups, Plugin, useCompile } from '@nocobase/client';
|
||||||
import { useFieldSchema } from '@formily/react';
|
|
||||||
import { isValid } from '@formily/shared';
|
|
||||||
|
|
||||||
import { PagePopups, Plugin, useCompile, WorkflowConfig } from '@nocobase/client';
|
|
||||||
import { Registry } from '@nocobase/utils/client';
|
import { Registry } from '@nocobase/utils/client';
|
||||||
|
|
||||||
// import { ExecutionPage } from './ExecutionPage';
|
// import { ExecutionPage } from './ExecutionPage';
|
||||||
@ -35,9 +31,13 @@ import UpdateInstruction from './nodes/update';
|
|||||||
import DestroyInstruction from './nodes/destroy';
|
import DestroyInstruction from './nodes/destroy';
|
||||||
import { getWorkflowDetailPath, getWorkflowExecutionsPath } from './utils';
|
import { getWorkflowDetailPath, getWorkflowExecutionsPath } from './utils';
|
||||||
import { lang, NAMESPACE } from './locale';
|
import { lang, NAMESPACE } from './locale';
|
||||||
import { customizeSubmitToWorkflowActionSettings } from './settings/customizeSubmitToWorkflowActionSettings';
|
|
||||||
import { VariableOption } from './variable';
|
import { VariableOption } from './variable';
|
||||||
import { WorkflowTasks, TasksProvider, TaskTypeOptions } from './WorkflowTasks';
|
import { WorkflowTasks, TasksProvider, TaskTypeOptions } from './WorkflowTasks';
|
||||||
|
import { BindWorkflowConfig } from './settings/BindWorkflowConfig';
|
||||||
|
|
||||||
|
const workflowConfigSettings = {
|
||||||
|
Component: BindWorkflowConfig,
|
||||||
|
};
|
||||||
|
|
||||||
type InstructionGroup = {
|
type InstructionGroup = {
|
||||||
key?: string;
|
key?: string;
|
||||||
@ -138,15 +138,13 @@ export default class PluginWorkflowClient extends Plugin {
|
|||||||
|
|
||||||
this.app.use(TasksProvider);
|
this.app.use(TasksProvider);
|
||||||
|
|
||||||
this.app.schemaSettingsManager.add(customizeSubmitToWorkflowActionSettings);
|
this.app.schemaSettingsManager.addItem('actionSettings:submit', 'workflowConfig', workflowConfigSettings);
|
||||||
|
this.app.schemaSettingsManager.addItem('actionSettings:createSubmit', 'workflowConfig', workflowConfigSettings);
|
||||||
this.app.schemaSettingsManager.addItem('actionSettings:delete', 'workflowConfig', {
|
this.app.schemaSettingsManager.addItem('actionSettings:updateSubmit', 'workflowConfig', workflowConfigSettings);
|
||||||
Component: WorkflowConfig,
|
this.app.schemaSettingsManager.addItem('actionSettings:saveRecord', 'workflowConfig', workflowConfigSettings);
|
||||||
useVisible() {
|
this.app.schemaSettingsManager.addItem('actionSettings:updateRecord', 'workflowConfig', workflowConfigSettings);
|
||||||
const fieldSchema = useFieldSchema();
|
this.app.schemaSettingsManager.addItem('actionSettings:delete', 'workflowConfig', workflowConfigSettings);
|
||||||
return isValid(fieldSchema?.['x-action-settings']?.triggerWorkflows);
|
this.app.schemaSettingsManager.addItem('actionSettings:bulkEditSubmit', 'workflowConfig', workflowConfigSettings);
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
this.registerInstructionGroup('control', { key: 'control', label: `{{t("Control", { ns: "${NAMESPACE}" })}}` });
|
this.registerInstructionGroup('control', { key: 'control', label: `{{t("Control", { ns: "${NAMESPACE}" })}}` });
|
||||||
this.registerInstructionGroup('calculation', {
|
this.registerInstructionGroup('calculation', {
|
||||||
@ -195,3 +193,4 @@ export * from './hooks';
|
|||||||
export { default as useStyles } from './style';
|
export { default as useStyles } from './style';
|
||||||
export * from './variable';
|
export * from './variable';
|
||||||
export * from './ExecutionContextProvider';
|
export * from './ExecutionContextProvider';
|
||||||
|
export * from './settings/BindWorkflowConfig';
|
||||||
|
@ -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
|
* @deprecated
|
||||||
* Keep for action trigger compatibility
|
* Keep for action trigger compatibility
|
||||||
*/
|
*/
|
||||||
export async function trigger(context: Context, next) {
|
// export async function trigger(context: Context, next) {
|
||||||
return next();
|
// return next();
|
||||||
}
|
// }
|
||||||
|
|
||||||
export async function execute(context: Context, next) {
|
export async function execute(context: Context, next) {
|
||||||
const plugin = context.app.pm.get(Plugin) as Plugin;
|
const plugin = context.app.pm.get(Plugin) as Plugin;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user