feat(variable): lazy load association fields (#2382)

* refactor: remove useless code

* feat: should always show user variable

* style: optimize style of variable input

* feat: change maxDepth of user varialbe to 3

* refactor: migrate getFieldDefaultValue to SchemaSettings.tsx

* feat: support to parse variables

* feat: support to parse array fields

* refactor: make testing more stable

* chore: fix CI

* feat: support to parse current user

* fix: fix tests

* feat: support to register variable

* feat: support to parse "$context"

* refactor: remove useless code

* fix: fix build

* fix: fix build

* test: make stable

* docs: add doc

* fix: fix T-1267

* fix: fix T-1268

* fix: fix T-1269

* fix: fix T-1271

* fix: fix T-1274

* refactor: improve code

* feat(variable): support local variable

* fix: fix T-1280

* refactor: close T-881

* refactor: remove useLazyLoadAssociation

* fix: fix T-1289,T-1290

* fix: fix T-1296

* refactor(AssignedField): replace user variable

* refactor: extract useBaseVariable

* refactor(AssignedField): replace variables

* feat(useVariables): support to use array as local variables

* refactor: improve variables (T-1295)

* feat: limit user choice (T-1297)

* fix: fix T-1305

* refactor: extract as SchemaSettings.DataScope

* refactor: improve linkage rules

* refactor: improve linkage rules of Action

* chore: fix build

* refactor: improve data scope of association fields

* chore: fix build

* feat: select limit

* refactor: use VariableInput to replace other

* fix: fix can not input

* fix(AssignedField): fix cannot close variables list

* test: fix errors

* refactor: migrate to new file

* fix: uniq

* fix: filter empty values

* fix: transform variables value

* fix: should sum numbers (T-1330)

* fix: fix Select (T-1337)

* refactor: use useMemo

* refactor: use hooks

* refactor: use hooks

* refactor: use hooks

* refactor: remove useless code

* refactor: rename `$form` to `$nForm` and `$record` to `$nRecord`

* fix: fix T-1334

* refactor(LinkageRuleActionGroup): use hooks

* refactor(FormButtonLinkageRuleAction): use hooks

* refactor: add type

* fix: avoid crashing

* refactor: rename `$date` to `$nDate`

* fix: fix T-1348

* perf: avoid duplicate requests

* test: fix error

* fix: fix T-1355

* fix: fix T-1355

* fix: fix T-1345

* fix: fix T-1346

* refactor(useVariableOptions): replace query with parameter

* fix: fix T-1335

* chore: fix build

* fix: fix T-1453

* refactor: remove useless code

* fix: fix T-1447

* fix: should update when seting default value

* fix: fix T-1454

* chore(eslint): ignore docker

* fix: fix T-1440

* refactor: use hooks

* fix: fix T-1332

* fix(defaultValue): should use Select mode

* fix(defaultValue): fix Checkbox

* refactor: rename and comment

* refactor: rename file name and hook name

* feat(subForm): support lazy load association fields

* fix: filter empty values

* fix(defaultValue): schema type of Checkbox should be void

* fix: fix T-1479

* fix: fix T-1491

* fix(ToOneNester): should use RecordProvider

* fix: fix T-1493

* chore: add comment

* fix: fix T-1495

* chore: fix build

* test: make pass

* fix: fix T-1503

* fix: fix T-1504

* fix: fix transformVariableValue

* fix: disable unsupport options

* refactor(useVariableOptions): add noDisabled params

* fix: data scope support to select to-many fields

* chore: add comment

* refactor: improve default value

* refactor: extract as useIsAllowToSetDefaultValue

* fix: should not support to set default value in edit mode

* fix: only the default values for sub-forms and sub-tables are valid

* fix: limit default value of association fields

* fix: fix '$nForm' variable

* fix: allow all inherit chain

* fix: optimize default value

* fix: optimize json field

* fix(currentObject): avoid error

* fix: should not show variable string

* fix: active fields

* fix: avoid errors

* feat: support special case

* fix: fix CI

* chore: fix build

* fix: json

* refactor: rename flag name

* fix: fix T-1559

* fix: fix T-1515

* fix: avoid crash

* fix: fix T-1520

* fix: fix T-1521

* fix: fix T-1523

* fix: fix T-1563

* fix: fix T-1565

* fix: fix T-1525

* fix: fix T-1524

* fix: fix T-1603

* fix: fix T-1332

* fix: fix T-1559

* fix: fix T-1602

* fix: fix T-1608

* fix: fix T-1612 T-1609

* fix: fix T-1681

* fix: fix T-1711

* fix: fix T-1713

* fix: fix T-1680

* chore: close T-1550

* fix: fix T-1507

* chore: avoid crashing

* fix: fix T-1557

* perf: close T-1726

* fix: fix T-1727

* fix: fix T-1741

* fix: fix T-1740

* chore(workflow): compact

* chore: should not to support to set default value in assign field values

* perf: compat and perf (T-1736)

* fix: fix T-1872

* fix: fix T-1877

* fix: fix quarter variables

* chore: add README

* chore: front-end should not depend on back-end code

* chore: should disable all variable options if no field is selected

* chore: should be a leaf if it has no children

* chore: fix build error

* fix: compat $form

* fix: compat $form

* chore: fix build

* chore: fix build

* fix: typeError: Cannot read properties of undefined (reading 'template')

* chore: fix test

* chore: add test

* fix: fix T-2072

* chore: optimize eslint

* perf: reduce number of requests

* fix: fix JSON

* test: fix error

* fix(e2e): avoid error

---------

Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
被雨水过滤的空气-Rain 2023-10-07 15:49:09 +08:00 committed by GitHub
parent c4f572fda8
commit 573cde0a21
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
164 changed files with 6669 additions and 2334 deletions

2
.vscode/launch.json vendored
View File

@ -30,7 +30,7 @@
"name": "Debug Client Tests",
"runtimeExecutable": "yarn",
"runtimeArgs": ["run", "test:client", "${file}"],
"skipFiles": ["<node_internals>/**"],
"skipFiles": ["<node_internals>/**", "**/node_modules/**", "**/dist/**", "**/lib/**", "**/es/**"],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"windows": {

View File

@ -1,14 +1,13 @@
import path from 'path';
import react from '@vitejs/plugin-react';
import fg from 'fast-glob';
import fs from 'fs-extra';
import { build as viteBuild } from 'vite';
import path from 'path';
import { build as tsupBuild } from 'tsup';
import { build as viteBuild } from 'vite';
import { libInjectCss } from 'vite-plugin-lib-inject-css';
import react from '@vitejs/plugin-react';
import { PkgLog, UserConfig } from './utils';
import { globExcludeFiles } from './constant';
import { PkgLog, UserConfig } from './utils';
export async function buildClient(cwd: string, userConfig: UserConfig, sourcemap: boolean = false, log: PkgLog) {
log('build client');
@ -19,7 +18,7 @@ export async function buildClient(cwd: string, userConfig: UserConfig, sourcemap
return false;
}
return true;
}
};
await buildEsm(cwd, userConfig, sourcemap, external, log);
await buildLib(cwd, userConfig, sourcemap, external, log);
await buildLocale(cwd, userConfig, log);
@ -31,10 +30,12 @@ export function buildEsm(cwd: string, userConfig: UserConfig, sourcemap: boolean
log('build client esm');
const entry = path.join(cwd, 'src/index.ts').replaceAll(/\\/g, '/');
const outDir = path.resolve(cwd, 'es');
return viteBuild(userConfig.modifyViteConfig({
return viteBuild(
userConfig.modifyViteConfig({
mode: 'production',
define: {
'process.env.NODE_ENV': JSON.stringify('production'),
'process.env.__TEST__': false,
},
build: {
minify: false,
@ -55,22 +56,30 @@ export function buildEsm(cwd: string, userConfig: UserConfig, sourcemap: boolean
},
},
plugins: [react(), libInjectCss()],
}));
}),
);
}
export async function buildLib(cwd: string, userConfig: UserConfig, sourcemap: boolean, external: External, log: PkgLog) {
export async function buildLib(
cwd: string,
userConfig: UserConfig,
sourcemap: boolean,
external: External,
log: PkgLog,
) {
log('build client lib');
const outDir = path.resolve(cwd, 'lib');
const esDir = path.resolve(cwd, 'es');
const entry = path.join(esDir, 'index.ts')
const entry = path.join(esDir, 'index.ts');
fs.removeSync(entry);
fs.linkSync(path.join(cwd, 'es/index.mjs'), entry);
await viteBuild(userConfig.modifyViteConfig({
await viteBuild(
userConfig.modifyViteConfig({
mode: 'production',
esbuild: {
format: 'cjs'
format: 'cjs',
},
build: {
outDir,
@ -85,7 +94,8 @@ export async function buildLib(cwd: string, userConfig: UserConfig, sourcemap: b
external,
},
},
}));
}),
);
fs.removeSync(entry);
@ -100,7 +110,8 @@ export function buildLocale(cwd: string, userConfig: UserConfig, log: PkgLog) {
const entry = fg.globSync(['src/locale/**', ...globExcludeFiles], { cwd, absolute: true });
const outDir = path.resolve(cwd, 'lib', 'locale');
return tsupBuild(userConfig.modifyTsupConfig({
return tsupBuild(
userConfig.modifyTsupConfig({
entry,
splitting: false,
clean: false,
@ -112,5 +123,6 @@ export function buildLocale(cwd: string, userConfig: UserConfig, log: PkgLog) {
outDir,
format: 'cjs',
skipNodeModulesBundle: true,
}));
}),
);
}

View File

@ -303,6 +303,7 @@ export async function buildPluginClient(cwd: string, userConfig: UserConfig, sou
mode: 'production',
define: {
'process.env.NODE_ENV': JSON.stringify('production'),
'process.env.__TEST__': false,
},
logLevel: 'warn',
build: {

View File

@ -0,0 +1,803 @@
import { test } from '@nocobase/test/client';
const t2072Config = {
collections: [
{
key: 'etlrv6xojl4',
name: 't_cyjvg3xxak7',
title: '人工>组织[普通表]',
inherit: false,
hidden: false,
description: null,
fields: [
{
key: 'e4jfv35cotm',
name: 'id',
type: 'bigInt',
interface: 'id',
description: null,
collectionName: 't_cyjvg3xxak7',
parentKey: null,
reverseKey: null,
autoIncrement: true,
primaryKey: true,
allowNull: false,
uiSchema: {
type: 'number',
title: '{{t("ID")}}',
'x-component': 'InputNumber',
'x-read-pretty': true,
},
},
{
key: '778zti7timg',
name: 'f_jnf1x0qxsnz',
type: 'bigInt',
interface: 'integer',
description: null,
collectionName: 't_cyjvg3xxak7',
parentKey: null,
reverseKey: null,
isForeignKey: true,
uiSchema: {
type: 'number',
title: 'f_jnf1x0qxsnz',
'x-component': 'InputNumber',
'x-read-pretty': true,
},
},
{
key: '2nuvtsxlzvh',
name: 'f_zx8j9897cv5',
type: 'bigInt',
interface: 'integer',
description: null,
collectionName: 't_cyjvg3xxak7',
parentKey: null,
reverseKey: null,
isForeignKey: true,
uiSchema: {
type: 'number',
title: 'f_zx8j9897cv5',
'x-component': 'InputNumber',
'x-read-pretty': true,
},
},
{
key: 'q491jgqygpg',
name: 'createdAt',
type: 'date',
interface: 'createdAt',
description: null,
collectionName: 't_cyjvg3xxak7',
parentKey: null,
reverseKey: null,
field: 'createdAt',
uiSchema: {
type: 'datetime',
title: '{{t("Created at")}}',
'x-component': 'DatePicker',
'x-component-props': {},
'x-read-pretty': true,
},
},
{
key: 'q2uungrabre',
name: 'createdBy',
type: 'belongsTo',
interface: 'createdBy',
description: null,
collectionName: 't_cyjvg3xxak7',
parentKey: null,
reverseKey: null,
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,
},
targetKey: 'id',
},
{
key: 'cizz53lon1q',
name: 'updatedAt',
type: 'date',
interface: 'updatedAt',
description: null,
collectionName: 't_cyjvg3xxak7',
parentKey: null,
reverseKey: null,
field: 'updatedAt',
uiSchema: {
type: 'string',
title: '{{t("Last updated at")}}',
'x-component': 'DatePicker',
'x-component-props': {},
'x-read-pretty': true,
},
},
{
key: '9dd8bohloh2',
name: 'updatedBy',
type: 'belongsTo',
interface: 'updatedBy',
description: null,
collectionName: 't_cyjvg3xxak7',
parentKey: null,
reverseKey: null,
target: 'users',
foreignKey: 'updatedById',
uiSchema: {
type: 'object',
title: '{{t("Last updated by")}}',
'x-component': 'AssociationField',
'x-component-props': {
fieldNames: {
value: 'id',
label: 'nickname',
},
},
'x-read-pretty': true,
},
targetKey: 'id',
},
{
key: 'p7649gl5tic',
name: 'f_wrlootg9xu9',
type: 'hasMany',
interface: 'o2m',
description: null,
collectionName: 't_cyjvg3xxak7',
parentKey: null,
reverseKey: null,
foreignKey: 'f_jnf1x0qxsnz',
onDelete: 'SET NULL',
uiSchema: {
'x-component': 'AssociationField',
'x-component-props': {
multiple: true,
fieldNames: {
label: 'id',
value: 'id',
},
},
title: '部门one to many',
},
target: 't_cyjvg3xxak7',
targetKey: 'id',
sourceKey: 'id',
},
{
key: '6etvnvsplrv',
name: 'f_4xulfnosvlx',
type: 'belongsTo',
interface: 'm2o',
description: null,
collectionName: 't_cyjvg3xxak7',
parentKey: null,
reverseKey: null,
foreignKey: 'f_zx8j9897cv5',
onDelete: 'SET NULL',
uiSchema: {
'x-component': 'AssociationField',
'x-component-props': {
multiple: false,
fieldNames: {
label: 'id',
value: 'id',
},
},
title: '负责人many to one',
},
target: 't_ymx99ljfetl',
targetKey: 'id',
},
],
category: [],
logging: true,
autoGenId: true,
createdBy: true,
updatedBy: true,
createdAt: true,
updatedAt: true,
sortable: true,
template: 'general',
view: false,
},
{
key: 'gmsav2kkbk3',
name: 't_ymx99ljfetl',
title: '人员',
inherit: false,
hidden: false,
description: null,
fields: [
{
key: 'dhonulr1xxc',
name: 'id',
type: 'bigInt',
interface: 'id',
description: null,
collectionName: 't_ymx99ljfetl',
parentKey: null,
reverseKey: null,
autoIncrement: true,
primaryKey: true,
allowNull: false,
uiSchema: {
type: 'number',
title: '{{t("ID")}}',
'x-component': 'InputNumber',
'x-read-pretty': true,
},
},
{
key: 'fh09uvcibsq',
name: 'f_qojsgjsrjie',
type: 'bigInt',
interface: 'integer',
description: null,
collectionName: 't_ymx99ljfetl',
parentKey: null,
reverseKey: null,
isForeignKey: true,
uiSchema: {
type: 'number',
title: 'f_qojsgjsrjie',
'x-component': 'InputNumber',
'x-read-pretty': true,
},
},
{
key: 'xzvwq5g75gi',
name: 'createdAt',
type: 'date',
interface: 'createdAt',
description: null,
collectionName: 't_ymx99ljfetl',
parentKey: null,
reverseKey: null,
field: 'createdAt',
uiSchema: {
type: 'datetime',
title: '{{t("Created at")}}',
'x-component': 'DatePicker',
'x-component-props': {},
'x-read-pretty': true,
},
},
{
key: 'oc580r95evm',
name: 'createdBy',
type: 'belongsTo',
interface: 'createdBy',
description: null,
collectionName: 't_ymx99ljfetl',
parentKey: null,
reverseKey: null,
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,
},
targetKey: 'id',
},
{
key: '9zi17bh97yl',
name: 'updatedAt',
type: 'date',
interface: 'updatedAt',
description: null,
collectionName: 't_ymx99ljfetl',
parentKey: null,
reverseKey: null,
field: 'updatedAt',
uiSchema: {
type: 'string',
title: '{{t("Last updated at")}}',
'x-component': 'DatePicker',
'x-component-props': {},
'x-read-pretty': true,
},
},
{
key: '2xbcmman6gz',
name: 'updatedBy',
type: 'belongsTo',
interface: 'updatedBy',
description: null,
collectionName: 't_ymx99ljfetl',
parentKey: null,
reverseKey: null,
target: 'users',
foreignKey: 'updatedById',
uiSchema: {
type: 'object',
title: '{{t("Last updated by")}}',
'x-component': 'AssociationField',
'x-component-props': {
fieldNames: {
value: 'id',
label: 'nickname',
},
},
'x-read-pretty': true,
},
targetKey: 'id',
},
],
category: [],
logging: true,
autoGenId: true,
createdBy: true,
updatedBy: true,
createdAt: true,
updatedAt: true,
sortable: true,
template: 'general',
view: false,
},
],
pageSchema: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Page',
properties: {
'3fsvxxy8klh': {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid',
'x-initializer': 'BlockInitializers',
properties: {
kwm0euv1w2z: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Row',
properties: {
'4wk59ijgdzx': {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Col',
properties: {
vsgx66mbgpm: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-decorator': 'TableBlockProvider',
'x-acl-action': 't_cyjvg3xxak7:list',
'x-decorator-props': {
collection: 't_cyjvg3xxak7',
resource: 't_cyjvg3xxak7',
action: 'list',
params: {
pageSize: 20,
},
rowKey: 'id',
showIndex: true,
dragSort: false,
disableTemplate: false,
},
'x-designer': 'TableBlockDesigner',
'x-component': 'CardItem',
'x-filter-targets': [],
properties: {
actions: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-initializer': 'TableActionInitializers',
'x-component': 'ActionBar',
'x-component-props': {
style: {
marginBottom: 'var(--nb-spacing)',
},
},
properties: {
mb64tcykcw3: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
title: '{{t("Add record")}}',
'x-designer': 'Action.Designer',
'x-component': 'Action',
'x-action': 'customize:create',
'x-component-props': {
openMode: 'drawer',
icon: 'PlusOutlined',
},
'x-align': 'right',
'x-decorator': 'ACLActionProvider',
'x-acl-action': 'create',
'x-acl-action-props': {
skipScopeCheck: true,
},
properties: {
drawer: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
title: '{{t("Add record")}}',
'x-component': 'Action.Container',
'x-component-props': {
className: 'nb-action-popup',
},
properties: {
tabs: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Tabs',
'x-component-props': {},
'x-initializer': 'TabPaneInitializersForCreateFormBlock',
properties: {
tab1: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
title: '{{t("Add record")}}',
'x-component': 'Tabs.TabPane',
'x-designer': 'Tabs.Designer',
'x-component-props': {},
properties: {
grid: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid',
'x-initializer': 'CusomeizeCreateFormBlockInitializers',
properties: {
'4sdqrhx1egh': {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Row',
properties: {
m1gl7ido4xg: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Col',
properties: {
f0srs8s7mq6: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-acl-action-props': {
skipScopeCheck: true,
},
'x-acl-action': 't_cyjvg3xxak7:create',
'x-decorator': 'FormBlockProvider',
'x-decorator-props': {
isCusomeizeCreate: true,
resource: 't_cyjvg3xxak7',
collection: 't_cyjvg3xxak7',
},
'x-designer': 'FormV2.Designer',
'x-component': 'CardItem',
'x-component-props': {},
properties: {
l3kfu43aq17: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'FormV2',
'x-component-props': {
useProps: '{{ useFormBlockProps }}',
},
properties: {
grid: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid',
'x-initializer': 'FormItemInitializers',
properties: {
j57m9wdhidh: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Row',
properties: {
'8l5ut3li3my': {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component': 'Grid.Col',
properties: {
f_wrlootg9xu9: {
'x-uid': 'enypaahwvzs',
_isJSONSchemaObject: true,
version: '2.0',
type: 'string',
'x-designer': 'FormItem.Designer',
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
'x-collection-field':
't_cyjvg3xxak7.f_wrlootg9xu9',
'x-component-props': {
mode: 'SubTable',
},
default: null,
properties: {
svaxqhd7v29: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-component':
'AssociationField.SubTable',
'x-initializer':
'TableColumnInitializers',
'x-initializer-props': {
action: false,
},
'x-index': 1,
properties: {
eevxwy50bcp: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-decorator':
'TableV2.Column.Decorator',
'x-designer':
'TableV2.Column.Designer',
'x-component': 'TableV2.Column',
properties: {
f_4xulfnosvlx: {
'x-uid': 'ho3gwsra9qk',
_isJSONSchemaObject: true,
version: '2.0',
'x-collection-field':
't_cyjvg3xxak7.f_4xulfnosvlx',
'x-component':
'CollectionField',
'x-component-props': {
ellipsis: true,
size: 'small',
},
'x-decorator': 'FormItem',
'x-decorator-props': {
labelStyle: {
display: 'none',
},
},
default:
'{{$context.f_4xulfnosvlx}}',
'x-async': false,
'x-index': 1,
},
},
'x-uid': '6ym1xpsxmyf',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'nmbnils3epp',
'x-async': false,
},
},
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'e69smhckndt',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'puxwfyhomzf',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'rziak1zqrgj',
'x-async': false,
'x-index': 1,
},
actions: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-initializer': 'FormActionInitializers',
'x-component': 'ActionBar',
'x-component-props': {
layout: 'one-column',
style: {
marginTop: 24,
},
},
'x-uid': 'ohkberml3rb',
'x-async': false,
'x-index': 2,
},
},
'x-uid': 't2zwt2anafy',
'x-async': false,
'x-index': 1,
},
},
'x-uid': '9nr7zawsp77',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'sy2eoiiguig',
'x-async': false,
'x-index': 1,
},
},
'x-uid': '3dpkedn8cdb',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'vkzbih8qgqz',
'x-async': false,
'x-index': 1,
},
},
'x-uid': '88pixfaj3wm',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'khgiclrgqhp',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'p5j5303aqdv',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'jcdnkjqfof4',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'wmcdk2bf0rk',
'x-async': false,
'x-index': 1,
},
ooh4p68w3wy: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'array',
'x-initializer': 'TableColumnInitializers',
'x-component': 'TableV2',
'x-component-props': {
rowKey: 'id',
rowSelection: {
type: 'checkbox',
},
useProps: '{{ useTableBlockProps }}',
},
properties: {
actions: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
title: '{{ t("Actions") }}',
'x-action-column': 'actions',
'x-decorator': 'TableV2.Column.ActionBar',
'x-component': 'TableV2.Column',
'x-designer': 'TableV2.ActionColumnDesigner',
'x-initializer': 'TableActionColumnInitializers',
properties: {
actions: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-decorator': 'DndContext',
'x-component': 'Space',
'x-component-props': {
split: '|',
},
'x-uid': 'zf80uv8uhol',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'lrb33ra71kx',
'x-async': false,
'x-index': 1,
},
bi9qnla6pey: {
_isJSONSchemaObject: true,
version: '2.0',
type: 'void',
'x-decorator': 'TableV2.Column.Decorator',
'x-designer': 'TableV2.Column.Designer',
'x-component': 'TableV2.Column',
properties: {
f_4xulfnosvlx: {
_isJSONSchemaObject: true,
version: '2.0',
'x-collection-field': 't_cyjvg3xxak7.f_4xulfnosvlx',
'x-component': 'CollectionField',
'x-component-props': {
ellipsis: true,
size: 'small',
},
'x-read-pretty': true,
'x-decorator': null,
'x-decorator-props': {
labelStyle: {
display: 'none',
},
},
'x-uid': '9a1xgqcn8g8',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'ipt9o4tqqkw',
'x-async': false,
'x-index': 2,
},
},
'x-uid': 'a89hczfm5nt',
'x-async': false,
'x-index': 2,
},
},
'x-uid': 'sghu92e6os9',
'x-async': false,
'x-index': 1,
},
},
'x-uid': '8bhcc7f8dfb',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'l4nrtmguqpv',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'jfcdfwu92mu',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'cfjrdgvlm68',
'x-async': true,
'x-index': 1,
},
};
test.describe.skip('variables', () => {
test('T-2072', async ({ page, mockPage }) => {
await mockPage(t2072Config).goto();
await page.getByTestId('table-index-1').hover();
await page.getByRole('cell', { name: '1' }).getByLabel('').check();
await page.getByTestId('table-index-2').hover();
await page.getByRole('cell', { name: '2' }).getByLabel('').check();
await page.getByTestId('table-index-3').hover();
await page.getByRole('cell', { name: '3' }).getByLabel('').check();
await page.getByTestId('customize:create-action').click();
});
});

View File

@ -1,5 +1,5 @@
import { css } from '@emotion/css';
import { Field } from '@formily/core';
import { Field, GeneralField } from '@formily/core';
import { RecursionField, useField, useFieldSchema } from '@formily/react';
import { useRequest } from 'ahooks';
import { Col, Row } from 'antd';
@ -24,7 +24,16 @@ import { useAssociationNames } from './hooks';
export const BlockResourceContext = createContext(null);
export const BlockAssociationContext = createContext(null);
export const BlockRequestContext = createContext<any>({});
export const BlockRequestContext = createContext<{
block?: string;
props?: any;
field?: GeneralField;
service?: any;
resource?: any;
allowedActions?: any;
__parent?: any;
updateAssociationValues?: any[];
}>({});
export const useBlockResource = () => {
return useContext(BlockResourceContext);
@ -56,7 +65,7 @@ const useResource = (props: UseResourceProps) => {
const isCreateAction = fieldSchema?.['x-action'] === 'create';
const association = useAssociation(props);
const sourceId = useSourceId?.();
const field = useField<Field>();
const field = useField();
const withoutTableFieldResource = useContext(WithoutTableFieldResource);
const __parent = useContext(BlockRequestContext);
if (block === 'TableField') {
@ -96,7 +105,7 @@ const useActionParams = (props) => {
return { ...props.params, ...params };
};
export const useResourceAction = (props, opts = {}) => {
const useResourceAction = (props, opts = {}) => {
/**
* fieldName: 来自 TableFieldProvider
*/
@ -168,7 +177,7 @@ export const MaybeCollectionProvider = (props) => {
};
export const BlockRequestProvider = (props) => {
const field = useField();
const field = useField<Field>();
const resource = useBlockResource();
const [allowedActions, setAllowedActions] = useState({});

View File

@ -8,6 +8,7 @@ import { RecordProvider, useRecord } from '../record-provider';
import { useActionContext, useDesignable } from '../schema-component';
import { Templates as DataTemplateSelect } from '../schema-component/antd/form-v2/Templates';
import { BlockProvider, useBlockRequestContext } from './BlockProvider';
import { FormActiveFieldsProvider } from './hooks';
export const FormBlockContext = createContext<any>({});
@ -19,7 +20,7 @@ const InternalFormBlockProvider = (props) => {
createForm({
readPretty,
}),
[],
[readPretty],
);
const { resource, service, updateAssociationValues } = useBlockRequestContext();
const formBlockRef = useRef();
@ -27,6 +28,7 @@ const InternalFormBlockProvider = (props) => {
if (service.loading && Object.keys(form?.initialValues)?.length === 0 && action) {
return <Spin />;
}
return (
<FormBlockContext.Provider
value={{
@ -83,7 +85,9 @@ export const FormBlockProvider = (props) => {
return (
(detailFlag || createFlag || isCusomeizeCreate) && (
<BlockProvider data-testid={props['data-testid'] || 'form-block'} {...props} block={'form'}>
<FormActiveFieldsProvider name="form">
<InternalFormBlockProvider {...props} />
</FormActiveFieldsProvider>
</BlockProvider>
)
);

View File

@ -91,7 +91,7 @@ export const useGanttBlockProps = () => {
const { getPrimaryKey, name, template, writableView } = useCollection();
const { parseAction } = useACLRoleContext();
const primaryKey = getPrimaryKey();
const checkPermassion = (record) => {
const checkPermission = (record) => {
const actionPath = `${name}:update`;
const schema = {};
const recordPkValue = record?.[primaryKey];
@ -106,13 +106,13 @@ export const useGanttBlockProps = () => {
ctx.field.data = tasksData;
};
const expandAndCollapseAll = (flag) => {
const data = formatData(ctx.service.data?.data, ctx.fieldNames, [], undefined, flag, checkPermassion);
const data = formatData(ctx.service.data?.data, ctx.fieldNames, [], undefined, flag, checkPermission);
setTasks(data);
ctx.field.data = data;
};
useEffect(() => {
if (!ctx?.service?.loading) {
const data = formatData(ctx.service.data?.data, ctx.fieldNames, [], undefined, false, checkPermassion);
const data = formatData(ctx.service.data?.data, ctx.fieldNames, [], undefined, false, checkPermission);
setTasks(data);
ctx.field.data = data;
}

View File

@ -28,6 +28,10 @@ interface Props {
rowKey?: string;
childrenColumnName: any;
fieldNames?: any;
/**
* Table collection name
*/
collection?: string;
}
const InternalTableBlockProvider = (props: Props) => {
@ -43,6 +47,7 @@ const InternalTableBlockProvider = (props: Props) => {
return keys || [];
}
}, [service?.loading]);
return (
<FixedBlockWrapper>
<TableBlockContext.Provider
@ -94,6 +99,7 @@ export const TableBlockProvider = (props) => {
}
}
const form = useMemo(() => createForm(), [treeTable]);
return (
<SchemaComponentOptions scope={{ treeTable }}>
<FormContext.Provider value={form}>
@ -142,7 +148,6 @@ export const useTableBlockProps = () => {
}
: false,
onRowSelectionChange(selectedRowKeys) {
console.log(selectedRowKeys);
ctx.field.data = ctx?.field?.data || {};
ctx.field.data.selectedRowKeys = selectedRowKeys;
ctx?.field?.onRowSelect?.(selectedRowKeys);

View File

@ -1,14 +1,14 @@
import { SchemaExpressionScopeContext, useField, useFieldSchema, useForm } from '@formily/react';
import { isURL, parse } from '@nocobase/utils/client';
import { App, message } from 'antd';
import { cloneDeep } from 'lodash';
import _, { cloneDeep } from 'lodash';
import get from 'lodash/get';
import omit from 'lodash/omit';
import { ChangeEvent, useContext, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { useReactToPrint } from 'react-to-print';
import { AssociationFilter, useFormBlockContext, useTableBlockContext } from '../..';
import { AssociationFilter, useFormActiveFields, useFormBlockContext, useTableBlockContext } from '../..';
import { useAPIClient, useRequest } from '../../api-client';
import { useCollection, useCollectionManager } from '../../collection-manager';
import { useFilterBlock } from '../../filter-provider/FilterProvider';
@ -17,11 +17,16 @@ import { useRecord } from '../../record-provider';
import { removeNullCondition, useActionContext, useCompile } from '../../schema-component';
import { BulkEditFormItemValueType } from '../../schema-initializer/components';
import { useCurrentUserContext } from '../../user';
import { useLocalVariables, useVariables } from '../../variables';
import { isVariable } from '../../variables/utils/isVariable';
import { transformVariableValue } from '../../variables/utils/transformVariableValue';
import { useBlockRequestContext, useFilterByTk, useParamsFromRecord } from '../BlockProvider';
import { useDetailsBlockContext } from '../DetailsBlockProvider';
import { mergeFilter } from '../SharedFilterProvider';
import { TableFieldResource } from '../TableFieldProvider';
export * from './useFormActiveFields';
export const usePickActionProps = () => {
const form = useForm();
return {
@ -61,13 +66,28 @@ const filterValue = (value) => {
return obj;
};
function getFormValues(filterByTk, field, form, fieldNames, getField, resource) {
function getFormValues({
filterByTk,
field,
form,
fieldNames,
getField,
resource,
actionFields,
}: {
filterByTk;
field;
form;
fieldNames;
getField;
resource;
actionFields: any[];
}) {
if (filterByTk) {
const actionFields = field?.data?.activeFields as Set<string>;
if (actionFields) {
const keys = Object.keys(form.values).filter((key) => {
const f = getField(key);
return !actionFields.has(key) && ['hasOne', 'hasMany', 'belongsTo', 'belongsToMany'].includes(f?.type);
return !actionFields.includes(key) && ['hasOne', 'hasMany', 'belongsTo', 'belongsToMany'].includes(f?.type);
});
return omit({ ...form.values }, keys);
}
@ -122,17 +142,17 @@ export const useCreateActionProps = () => {
const { field, resource, __parent } = useBlockRequestContext();
const { setVisible, fieldSchema } = useActionContext();
const navigate = useNavigate();
const { t } = useTranslation();
const actionSchema = useFieldSchema();
const actionField = useField();
const { fields, getField, getTreeParentField } = useCollection();
const { fields, getField, getTreeParentField, name } = useCollection();
const compile = useCompile();
const filterByTk = useFilterByTk();
const currentRecord = useRecord();
const currentUserContext = useCurrentUserContext();
const { modal } = App.useApp();
const variables = useVariables();
const localVariables = useLocalVariables({ currentForm: form });
const { getActiveFieldsName } = useFormActiveFields();
const currentUser = currentUserContext?.data?.data;
const action = actionField.componentProps.saveMode || 'create';
const filterKeys = actionField.componentProps.filterKeys?.checked || [];
return {
@ -146,11 +166,41 @@ export const useCreateActionProps = () => {
triggerWorkflows,
} = actionSchema?.['x-action-settings'] ?? {};
const addChild = fieldSchema?.['x-component-props']?.addChild;
const assignedValues = parse(originalAssignedValues)({ currentTime: new Date(), currentRecord, currentUser });
const assignedValues = {};
const waitList = Object.keys(originalAssignedValues).map(async (key) => {
const value = originalAssignedValues[key];
const collectionField = getField(key);
if (process.env.NODE_ENV !== 'production') {
if (!collectionField) {
throw new Error(`useCreateActionProps: field "${key}" not found in collection "${name}"`);
}
}
if (isVariable(value)) {
const result = await variables?.parseVariable(value, localVariables);
if (result) {
assignedValues[key] = transformVariableValue(result, { targetCollectionField: collectionField });
}
} else if (value != null && value !== '') {
assignedValues[key] = value;
}
});
await Promise.all(waitList);
if (!skipValidator) {
await form.submit();
}
const values = getFormValues(filterByTk, field, form, fieldNames, getField, resource);
const values = getFormValues({
filterByTk,
field,
form,
fieldNames,
getField,
resource,
actionFields: getActiveFieldsName('form'),
});
// const values = omitBy(formValues, (value) => isEqual(JSON.stringify(value), '[{}]'));
if (addChild) {
const treeParentField = getTreeParentField();
@ -209,12 +259,14 @@ export const useAssociationCreateActionProps = () => {
const { setVisible, fieldSchema } = useActionContext();
const actionSchema = useFieldSchema();
const actionField = useField();
const { fields, getField, getTreeParentField } = useCollection();
const { fields, getField, getTreeParentField, name } = useCollection();
const compile = useCompile();
const filterByTk = useFilterByTk();
const currentRecord = useRecord();
const currentUserContext = useCurrentUserContext();
const currentUser = currentUserContext?.data?.data;
const variables = useVariables();
const localVariables = useLocalVariables({ currentForm: form });
const { getActiveFieldsName } = useFormActiveFields();
const action = actionField.componentProps.saveMode || 'create';
const filterKeys = actionField.componentProps.filterKeys?.checked || [];
return {
@ -228,11 +280,41 @@ export const useAssociationCreateActionProps = () => {
triggerWorkflows,
} = actionSchema?.['x-action-settings'] ?? {};
const addChild = fieldSchema?.['x-component-props']?.addChild;
const assignedValues = parse(originalAssignedValues)({ currentTime: new Date(), currentRecord, currentUser });
const assignedValues = {};
const waitList = Object.keys(originalAssignedValues).map(async (key) => {
const value = originalAssignedValues[key];
const collectionField = getField(key);
if (process.env.NODE_ENV !== 'production') {
if (!collectionField) {
throw new Error(`useAssociationCreateActionProps: field "${key}" not found in collection "${name}"`);
}
}
if (isVariable(value)) {
const result = await variables?.parseVariable(value, localVariables);
if (result) {
assignedValues[key] = transformVariableValue(result, { targetCollectionField: collectionField });
}
} else if (value != null && value !== '') {
assignedValues[key] = value;
}
});
await Promise.all(waitList);
if (!skipValidator) {
await form.submit();
}
const values = getFormValues(filterByTk, field, form, fieldNames, getField, resource);
const values = getFormValues({
filterByTk,
field,
form,
fieldNames,
getField,
resource,
actionFields: getActiveFieldsName('form'),
});
if (addChild) {
const treeParentField = getTreeParentField();
values[treeParentField?.name ?? 'parent'] = currentRecord;
@ -405,13 +487,13 @@ export const useCustomizeUpdateActionProps = () => {
const { resource, __parent, service } = useBlockRequestContext();
const filterByTk = useFilterByTk();
const actionSchema = useFieldSchema();
const currentRecord = useRecord();
const currentUserContext = useCurrentUserContext();
const currentUser = currentUserContext?.data?.data;
const navigate = useNavigate();
const compile = useCompile();
const form = useForm();
const { modal } = App.useApp();
const variables = useVariables();
const localVariables = useLocalVariables({ currentForm: form });
const { name, getField } = useCollection();
return {
async onClick() {
@ -420,7 +502,29 @@ export const useCustomizeUpdateActionProps = () => {
onSuccess,
skipValidator,
} = actionSchema?.['x-action-settings'] ?? {};
const assignedValues = parse(originalAssignedValues)({ currentTime: new Date(), currentRecord, currentUser });
const assignedValues = {};
const waitList = Object.keys(originalAssignedValues).map(async (key) => {
const value = originalAssignedValues[key];
const collectionField = getField(key);
if (process.env.NODE_ENV !== 'production') {
if (!collectionField) {
throw new Error(`useCustomizeUpdateActionProps: field "${key}" not found in collection "${name}"`);
}
}
if (isVariable(value)) {
const result = await variables?.parseVariable(value, localVariables);
if (result) {
assignedValues[key] = transformVariableValue(result, { targetCollectionField: collectionField });
}
} else if (value != null && value !== '') {
assignedValues[key] = value;
}
});
await Promise.all(waitList);
if (skipValidator === false) {
await form.submit();
}
@ -463,13 +567,14 @@ export const useCustomizeBulkUpdateActionProps = () => {
const { rowKey } = tableBlockContext;
const selectedRecordKeys =
tableBlockContext.field?.data?.selectedRowKeys ?? expressionScope?.selectedRecordKeys ?? {};
const currentUserContext = useCurrentUserContext();
const currentUser = currentUserContext?.data?.data;
const navigate = useNavigate();
const compile = useCompile();
const { t } = useTranslation();
const actionField = useField();
const { modal } = App.useApp();
const variables = useVariables();
const localVariables = useLocalVariables();
const { name, getField } = useCollection();
return {
async onClick() {
@ -480,7 +585,29 @@ export const useCustomizeBulkUpdateActionProps = () => {
} = actionSchema?.['x-action-settings'] ?? {};
actionField.data = field.data || {};
actionField.data.loading = true;
const assignedValues = parse(originalAssignedValues)({ currentTime: new Date(), currentUser });
const assignedValues = {};
const waitList = Object.keys(originalAssignedValues).map(async (key) => {
const value = originalAssignedValues[key];
const collectionField = getField(key);
if (process.env.NODE_ENV !== 'production') {
if (!collectionField) {
throw new Error(`useCustomizeBulkUpdateActionProps: field "${key}" not found in collection "${name}"`);
}
}
if (isVariable(value)) {
const result = await variables?.parseVariable(value, localVariables);
if (result) {
assignedValues[key] = transformVariableValue(result, { targetCollectionField: collectionField });
}
} else if (value != null && value !== '') {
assignedValues[key] = value;
}
});
await Promise.all(waitList);
modal.confirm({
title: t('Bulk update'),
content: updateMode === 'selected' ? t('Update selected data?') : t('Update all data?'),
@ -644,6 +771,7 @@ export const useCustomizeRequestActionProps = () => {
const actionField = useField();
const { setVisible } = useActionContext();
const { modal } = App.useApp();
const { getActiveFieldsName } = useFormActiveFields();
return {
async onClick() {
@ -662,7 +790,15 @@ export const useCustomizeRequestActionProps = () => {
const methods = ['POST', 'PUT', 'PATCH'];
if (xAction === 'customize:form:request' && methods.includes(requestSettings['method'])) {
const fieldNames = fields.map((field) => field.name);
const values = getFormValues(filterByTk, field, form, fieldNames, getField, resource);
const values = getFormValues({
filterByTk,
field,
form,
fieldNames,
getField,
resource,
actionFields: getActiveFieldsName('form'),
});
Object.assign(data, values);
}
const requestBody = {
@ -715,20 +851,19 @@ export const useCustomizeRequestActionProps = () => {
export const useUpdateActionProps = () => {
const form = useForm();
const filterByTk = useFilterByTk();
const { field, resource, __parent, service } = useBlockRequestContext();
const { field, resource, __parent } = useBlockRequestContext();
const { setVisible } = useActionContext();
const actionSchema = useFieldSchema();
const navigate = useNavigate();
const { fields, getField } = useCollection();
const { fields, getField, name } = useCollection();
const compile = useCompile();
const actionField = useField();
const { updateAssociationValues } = useFormBlockContext();
const currentRecord = useRecord();
const currentUserContext = useCurrentUserContext();
const { modal } = App.useApp();
const currentUser = currentUserContext?.data?.data;
const data = useParamsFromRecord();
const variables = useVariables();
const localVariables = useLocalVariables({ currentForm: form });
const { getActiveFieldsName } = useFormActiveFields();
return {
async onClick() {
@ -739,12 +874,42 @@ export const useUpdateActionProps = () => {
skipValidator,
triggerWorkflows,
} = actionSchema?.['x-action-settings'] ?? {};
const assignedValues = parse(originalAssignedValues)({ currentTime: new Date(), currentRecord, currentUser });
const assignedValues = {};
const waitList = Object.keys(originalAssignedValues).map(async (key) => {
const value = originalAssignedValues[key];
const collectionField = getField(key);
if (process.env.NODE_ENV !== 'production') {
if (!collectionField) {
throw new Error(`useUpdateActionProps: field "${key}" not found in collection "${name}"`);
}
}
if (isVariable(value)) {
const result = await variables?.parseVariable(value, localVariables);
if (result) {
assignedValues[key] = transformVariableValue(result, { targetCollectionField: collectionField });
}
} else if (value != null && value !== '') {
assignedValues[key] = value;
}
});
await Promise.all(waitList);
if (!skipValidator) {
await form.submit();
}
const fieldNames = fields.map((field) => field.name);
const values = getFormValues(filterByTk, field, form, fieldNames, getField, resource);
const values = getFormValues({
filterByTk,
field,
form,
fieldNames,
getField,
resource,
actionFields: getActiveFieldsName('form'),
});
actionField.data = field.data || {};
actionField.data.loading = true;
try {
@ -1125,6 +1290,25 @@ export const useAssociationNames = () => {
const isAssociationSubfield = s.name.includes('.');
const isAssociationField =
collectionfield && ['hasOne', 'hasMany', 'belongsTo', 'belongsToMany'].includes(collectionfield.type);
// 根据联动规则中条件的字段获取一些 appends
if (s['x-linkage-rules']) {
const rules = s['x-linkage-rules'];
rules.forEach(({ condition }) => {
const type = Object.keys(condition)[0] || '$and';
const list = condition[type];
list.forEach((item) => {
const fieldNames = getTargetField(item);
// 只应该收集关系字段,只有大于 1 的时候才是关系字段
if (fieldNames.length > 1) {
appends.add(fieldNames.join('.'));
}
});
});
}
const isTreeCollection = isAssociationField && getCollection(collectionfield.target)?.template === 'tree';
if (collectionfield && (isAssociationField || isAssociationSubfield) && s['x-component'] !== 'TableField') {
const fieldPath = !isAssociationField && isAssociationSubfield ? getAssociationPath(s.name) : s.name;
@ -1161,3 +1345,30 @@ export const useAssociationNames = () => {
getAssociationAppends(fieldSchema, '');
return { appends: [...appends], updateAssociationValues: [...updateAssociationValues] };
};
function getTargetField(obj) {
function getAllKeys(obj) {
const keys = [];
function traverse(o) {
Object.keys(o)
.sort()
.forEach(function (key) {
keys.push(key);
if (o[key] && typeof o[key] === 'object') {
traverse(o[key]);
}
});
}
traverse(obj);
return keys;
}
const keys = getAllKeys(obj);
const index = _.findIndex(keys, (key: string, index: number) => {
if (key.includes('$') && index > 0) {
return true;
}
});
const result = keys.slice(0, index);
return result;
}

View File

@ -0,0 +1,61 @@
import React, { createContext, useContext, useMemo, useRef } from 'react';
interface FormActiveFieldsProviderValue {
getActiveFieldsName: (name: string) => string[];
addActiveFieldName: (fieldName: string) => void;
removeActiveFieldName: (fieldName: string) => void;
name: string;
parent: FormActiveFieldsProviderValue;
}
const FormActiveFieldsContext = createContext<FormActiveFieldsProviderValue>(null);
interface ProviderProps extends Partial<FormActiveFieldsProviderValue> {
children: React.ReactNode;
}
export const FormActiveFieldsProvider = ({
children,
name,
getActiveFieldsName,
addActiveFieldName,
removeActiveFieldName,
}: ProviderProps) => {
const activeFieldsNameRef = useRef<Set<string>>(new Set());
const upLevelContext = useContext(FormActiveFieldsContext);
const value = useMemo(() => {
return {
getActiveFieldsName:
getActiveFieldsName ||
((_name: string) => {
if (!_name || _name === name) {
return Array.from(activeFieldsNameRef.current);
}
return upLevelContext?.getActiveFieldsName(_name);
}),
addActiveFieldName:
addActiveFieldName ||
((field: string) => {
activeFieldsNameRef.current.add(field);
}),
removeActiveFieldName:
removeActiveFieldName ||
((field: string) => {
activeFieldsNameRef.current.delete(field);
}),
name,
parent: upLevelContext,
};
}, [addActiveFieldName, getActiveFieldsName, name, removeActiveFieldName, upLevelContext]);
return <FormActiveFieldsContext.Provider value={value}>{children}</FormActiveFieldsContext.Provider>;
};
/**
*
* @returns
*/
export const useFormActiveFields = () => {
return useContext(FormActiveFieldsContext);
};

View File

@ -6,7 +6,7 @@ export const CollectionManagerContext = createContext<CollectionManagerOptions>(
interfaces: {},
});
export const CollectionContext = createContext<CollectionOptions>({});
export const CollectionContext = createContext<CollectionOptions>({} as CollectionOptions);
export const CollectionFieldContext = createContext<CollectionFieldOptions>({});

View File

@ -4,11 +4,12 @@ import _, { reduce, unionBy, uniq, uniqBy } from 'lodash';
import { useContext } from 'react';
import { useCompile } from '../../schema-component';
import { CollectionManagerContext } from '../context';
import { CollectionFieldOptions } from '../types';
import { CollectionFieldOptions, CollectionOptions } from '../types';
export const useCollectionManager = () => {
const { refreshCM, updateCollection, service, interfaces, collections, templates } =
useContext(CollectionManagerContext);
const { refreshCM, updateCollection, service, interfaces, collections, templates } = useContext(
CollectionManagerContext,
);
const compile = useCompile();
const getInheritedFields = (name) => {
const inheritKeys = getInheritCollections(name);
@ -192,7 +193,7 @@ export const useCollectionManager = () => {
return options;
};
const getCollection = (name: any) => {
const getCollection = (name: any): CollectionOptions => {
if (typeof name !== 'string') {
return name;
}
@ -290,7 +291,7 @@ export const useCollectionManager = () => {
getCollectionFieldsOptions,
getCurrentCollectionFields,
getCollection,
getCollectionJoinField(name: string) {
getCollectionJoinField(name: string): CollectionFieldOptions {
if (!name) {
return;
}

View File

@ -1,7 +1,7 @@
import { FormItem, FormLayout } from '@formily/antd-v5';
import { registerValidateRules } from '@formily/core';
import React from 'react';
import { defaultProps } from './properties';
import { defaultProps, operators } from './properties';
import { IField } from './types';
registerValidateRules({
@ -59,5 +59,7 @@ export const json: IField = {
'x-disabled': `{{ disabledJSONB }}`,
},
},
filterable: {},
filterable: {
operators: operators.string,
},
};

View File

@ -138,5 +138,4 @@ export const linkTo: IField = {
// },
],
},
invariable: true,
};

View File

@ -1,10 +1,6 @@
import { ISchema } from '@formily/react';
import { uid } from '@formily/shared';
import {
defaultProps,
relationshipType,
reverseFieldProperties
} from './properties';
import { defaultProps, relationshipType, reverseFieldProperties } from './properties';
import { IField } from './types';
export const m2m: IField = {
@ -246,5 +242,4 @@ export const m2m: IField = {
// },
],
},
invariable: true,
};

View File

@ -1,9 +1,5 @@
import { ISchema } from '@formily/react';
import {
constraintsProps,
relationshipType,
reverseFieldProperties
} from './properties';
import { constraintsProps, relationshipType, reverseFieldProperties } from './properties';
import { IField } from './types';
export const m2o: IField = {
@ -178,5 +174,4 @@ export const m2o: IField = {
// },
],
},
invariable: true,
};

View File

@ -1,6 +1,6 @@
import { ISchema } from '@formily/react';
import { i18n } from '../../i18n';
import { defaultProps } from './properties';
import { defaultProps, operators } from './properties';
import { IField } from './types';
export const markdown: IField = {
@ -69,4 +69,7 @@ export const markdown: IField = {
},
};
},
filterable: {
operators: operators.string,
},
};

View File

@ -198,5 +198,4 @@ export const o2m: IField = {
// },
],
},
invariable: true,
};

View File

@ -178,7 +178,6 @@ export const o2o: IField = {
// },
],
},
invariable: true,
};
export const oho: IField = {
@ -349,7 +348,6 @@ export const oho: IField = {
// },
],
},
invariable: true,
};
export const obo: IField = {
@ -524,5 +522,4 @@ export const obo: IField = {
// },
],
},
invariable: true,
};

View File

@ -45,6 +45,11 @@ export const array = [
{ label: '{{t("is not empty")}}', value: '$notEmpty', noValue: true },
];
export const object = [
{ label: '{{t("is")}}', value: '$eq', selected: true },
{ label: '{{t("is not")}}', value: '$ne' },
];
export const datetime = [
{ label: "{{ t('is') }}", value: '$dateOn', selected: true },
{ label: "{{ t('is not') }}", value: '$dateNotOn' },

View File

@ -1,7 +1,7 @@
import type { ISchema } from '@formily/react';
import { defaultProps } from './properties';
import { IField } from './types';
import { i18n } from '../../i18n';
import { defaultProps, operators } from './properties';
import { IField } from './types';
export const richText: IField = {
name: 'richText',
@ -70,4 +70,7 @@ export const richText: IField = {
},
};
},
filterable: {
operators: operators.string,
},
};

View File

@ -1,7 +1,7 @@
import { ISchema } from '@formily/react';
import { defaultProps } from './properties';
import { IField } from './types';
import { i18n } from '../../i18n';
import { defaultProps, operators } from './properties';
import { IField } from './types';
export const textarea: IField = {
name: 'textarea',
@ -70,4 +70,7 @@ export const textarea: IField = {
},
};
},
filterable: {
operators: operators.string,
},
};

View File

@ -9,9 +9,21 @@ interface IDefault {
export interface IField extends ISchema {
default?: IDefault;
operators?: any[];
filterable?: any;
/** 不支持使用变量的值进行设置 */
invariable?: boolean;
/**
* - Filter
* -
*/
filterable?: {
/**
* Filter `数据范围`
*/
operators?: any[];
/**
* Filter `数据范围`
*/
children?: any[];
[key: string]: any;
};
// NOTE: set to `true` means field could be used as a title field
titleUsable?: boolean;
[key: string]: any;

View File

@ -1,5 +1,5 @@
import { ISchema } from '@formily/react';
import { defaultProps } from './properties';
import { defaultProps, operators } from './properties';
import { IField } from './types';
export const url: IField = {
@ -20,4 +20,7 @@ export const url: IField = {
...defaultProps,
},
titleUsable: true,
filterable: {
operators: operators.string,
},
};

View File

@ -40,7 +40,7 @@ const PreviewCom = (props) => {
sourceCollections.forEach((item) => {
const collection = getCollection(item);
const inherits = getInheritCollections(item);
const result = inherits.map((v) => {
const result: any[] = inherits.map((v) => {
const fields = getParentCollectionFields(v, item);
return {
type: 'group',
@ -59,7 +59,7 @@ const PreviewCom = (props) => {
const children = collection.fields
.filter((v) => !['hasOne', 'hasMany', 'belongsToMany'].includes(v?.type))
?.map((v) => {
return { value: v.name, label: t(v.uiSchema?.title) };
return { value: v.name, key: v.name, label: t(v.uiSchema?.title) };
})
.concat(result);
data.push({

View File

@ -1,36 +1,66 @@
import { ISchema } from '@formily/react';
import { ReactNode } from 'react';
type dumpable = 'required' | 'optional' | 'skip';
type CollectionSortable = string | boolean | { name?: string; scopeKey?: string };
export interface CollectionOptions {
name: string;
title?: string;
namespace?: string;
/**
* Used for @nocobase/plugin-duplicator
* @see packages/core/database/src/collection-group-manager.tss
*
* @prop {'required' | 'optional' | 'skip'} dumpable - Determine whether the collection is dumped
* @prop {string[] | string} [with] - Collections dumped with this collection
* @prop {any} [delayRestore] - A function to execute after all collections are restored
*/
duplicator?:
| dumpable
| {
dumpable: dumpable;
with?: string[] | string;
delayRestore?: any;
};
tableName?: string;
inherits?: string[] | string;
viewName?: string;
writableView?: boolean;
filterTargetKey?: string;
fields?: FieldOptions[];
model?: any;
repository?: any;
sortable?: CollectionSortable;
/**
* @default true
*/
autoGenId?: boolean;
/**
* @default 'options'
*/
magicAttribute?: string;
tree?: string;
template?: string;
[key: string]: any;
}
export interface CollectionManagerOptions {
service?: any;
interfaces?: any;
collections?: any[];
collections?: CollectionOptions[];
templates?: any;
refreshCM?: () => Promise<void>;
children?: ReactNode;
updateCollection?: (collection: any) => void;
}
export interface FieldOptions {
type: string;
interface?: string;
uiSchema?: ISchema;
[key: string]: any;
}
export interface CollectionOptions {
name?: string;
title?: string;
filterTargetKey?: string;
targetKey?: string;
sortable?: any;
fields?: FieldOptions[];
inherits?: string[];
tree?: string;
template?: string;
writableView?: boolean;
}
export type FieldOptions = any;
export interface ICollectionProviderProps {
name?: string;

View File

@ -3,11 +3,11 @@ import { uniqBy } from 'lodash';
import React, { createContext, useEffect, useRef } from 'react';
import { useBlockRequestContext } from '../block-provider/BlockProvider';
import { SharedFilter, mergeFilter } from '../block-provider/SharedFilterProvider';
import { CollectionFieldOptions, FieldOptions, useCollection } from '../collection-manager';
import { CollectionFieldOptions, useCollection } from '../collection-manager';
import { removeNullCondition } from '../schema-component';
import { useAssociatedFields } from './utils';
export interface ForeignKeyField extends FieldOptions {
export interface ForeignKeyField {
/** 外键字段所在的数据表的名称 */
collectionName: string;
isForeignKey: boolean;
@ -15,6 +15,8 @@ export interface ForeignKeyField extends FieldOptions {
name: string;
parentKey: null | string;
reverseKey: null | string;
[key: string]: any;
}
type Collection = ReturnType<typeof useCollection>;

View File

@ -0,0 +1,31 @@
import React, { useMemo } from 'react';
export interface FlagProviderProps {
/**
* `字段赋值`
*/
isInAssignFieldValues?: boolean;
/**
* `设置默认值`
*/
isInSetDefaultValueDialog?: boolean;
/**
* `表单数据模板`
*/
isInFormDataTemplate?: boolean;
children: any;
}
export const FlagContext = React.createContext<Omit<FlagProviderProps, 'children'>>(null);
export const FlagProvider = (props: FlagProviderProps) => {
const value = useMemo(() => {
return {
isInAssignFieldValues: props.isInAssignFieldValues,
isInSetDefaultValueDialog: props.isInSetDefaultValueDialog,
isInFormDataTemplate: props.isInFormDataTemplate,
};
}, [props.isInAssignFieldValues, props.isInFormDataTemplate, props.isInSetDefaultValueDialog]);
return <FlagContext.Provider value={value}>{props.children}</FlagContext.Provider>;
};

View File

@ -0,0 +1,16 @@
import React from 'react';
import { renderHook } from 'testUtils';
import { FlagProvider } from '../FlagProvider';
import { useFlag } from '../hooks/useFlag';
describe('FlagProvider', () => {
it('should render', () => {
const { result } = renderHook(() => useFlag(), {
wrapper: ({ children }) => {
return <FlagProvider isInAssignFieldValues={true}>{children}</FlagProvider>;
},
});
expect(result.current.isInAssignFieldValues).toBe(true);
});
});

View File

@ -0,0 +1,6 @@
import { useContext } from 'react';
import { FlagContext } from '../FlagProvider';
export const useFlag = () => {
return useContext(FlagContext);
};

View File

@ -0,0 +1,3 @@
export * from './FlagProvider';
export * from './hooks/useFlag';

View File

@ -25,6 +25,7 @@ import {
import { Plugin } from '../../../application/Plugin';
import { useAppSpin } from '../../../application/hooks/useAppSpin';
import { useCollectionManager } from '../../../collection-manager';
import { VariablesProvider } from '../../../variables';
const filterByACL = (schema, options) => {
const { allowAll, allowMenuItemIds = [] } = options;
@ -355,7 +356,9 @@ export const AdminProvider = (props) => {
<NavigateIfNotSignIn>
<RemoteSchemaTemplateManagerProvider>
<RemoteCollectionManagerProvider>
<VariablesProvider>
<ACLRolesCheckProvider>{props.children}</ACLRolesCheckProvider>
</VariablesProvider>
</RemoteCollectionManagerProvider>
</RemoteSchemaTemplateManagerProvider>
</NavigateIfNotSignIn>

View File

@ -7,12 +7,14 @@ import { cloneDeep } from 'lodash';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { RemoteSelect, useCompile, useDesignable } from '../..';
import { useCollection, useCollectionManager } from '../../../collection-manager';
import { CollectionOptions, useCollection, useCollectionManager } from '../../../collection-manager';
import { FlagProvider } from '../../../flag-provider';
import { useRecord } from '../../../record-provider';
import { OpenModeSchemaItems } from '../../../schema-items';
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
import { useCollectionState } from '../../../schema-settings/DataTemplates/hooks/useCollectionState';
import { useSyncFromForm } from '../../../schema-settings/DataTemplates/utils';
import { DefaultValueProvider } from '../../../schema-settings/hooks/useIsAllowToSetDefaultValue';
import { useLinkageAction } from './hooks';
import { requestSettingsSchema } from './utils';
@ -481,15 +483,8 @@ function AssignedFieldValues() {
),
};
const actionType = fieldSchema['x-action'] ?? '';
return (
<SchemaSettings.ActionModalItem
title={t('Assign field values')}
initialSchema={initialSchema}
initialValues={fieldSchema?.['x-action-settings']?.assignedValues}
modalTip={tips[actionType]}
uid={fieldSchema?.['x-action-settings']?.schemaUid}
onSubmit={(assignedValues) => {
const onSubmit = useCallback(
(assignedValues) => {
fieldSchema['x-action-settings']['assignedValues'] = assignedValues;
dn.emit('patch', {
schema: {
@ -497,9 +492,23 @@ function AssignedFieldValues() {
'x-action-settings': fieldSchema['x-action-settings'],
},
});
dn.refresh();
}}
},
[dn, fieldSchema],
);
return (
<FlagProvider isInAssignFieldValues={true}>
<DefaultValueProvider isAllowToSetDefaultValue={() => false}>
<SchemaSettings.ActionModalItem
title={t('Assign field values')}
initialSchema={initialSchema}
initialValues={fieldSchema?.['x-action-settings']?.assignedValues}
modalTip={tips[actionType]}
uid={fieldSchema?.['x-action-settings']?.schemaUid}
onSubmit={onSubmit}
/>
</DefaultValueProvider>
</FlagProvider>
);
}
@ -653,7 +662,7 @@ function FormWorkflowSelect(props) {
const [workflowCollection, setWorkflowCollection] = useState(baseCollection.name);
useFormEffects(() => {
onFieldValueChange(`group[${index}].context`, (field) => {
let collection = baseCollection;
let collection: CollectionOptions = baseCollection;
if (field.value) {
const paths = field.value.split('.');
for (let i = 0; i < paths.length && collection; i++) {

View File

@ -1,13 +1,15 @@
import { observer, RecursionField, useField, useFieldSchema, useForm } from '@formily/react';
import { isPortalInBody } from '@nocobase/utils/client';
import { App, Button, Popover } from 'antd';
import classnames from 'classnames';
import lodash from 'lodash';
import React, { useEffect, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useActionContext } from '../..';
import { useDesignable } from '../../';
import { Icon } from '../../../icon';
import { useRecord } from '../../../record-provider';
import { useLocalVariables, useVariables } from '../../../variables';
import { SortableItem } from '../../common';
import { useCompile, useComponent, useDesigner } from '../../hooks';
import { useProps } from '../../hooks/useProps';
@ -51,44 +53,44 @@ export const Action: ComposedAction = observer(
const values = useRecord();
const designerProps = fieldSchema['x-designer-props'];
const openMode = fieldSchema?.['x-component-props']?.['openMode'];
const disabled = form.disabled || field.disabled || props.disabled;
const disabled = form.disabled || field.disabled || field.data?.disabled || props.disabled;
const openSize = fieldSchema?.['x-component-props']?.['openSize'];
const linkageRules = fieldSchema?.['x-linkage-rules'] || [];
const { designable } = useDesignable();
const tarComponent = useComponent(component) || component;
const { modal } = App.useApp();
const variables = useVariables();
const localVariables = useLocalVariables({ currentForm: { values } as any });
let actionTitle = title || compile(fieldSchema.title);
actionTitle = lodash.isString(actionTitle) ? t(actionTitle) : actionTitle;
useEffect(() => {
field.linkageProperty = {};
linkageRules
.filter((k) => !k.disabled)
.forEach((v) => {
return v.actions?.map((h) => {
linkageAction(h.operator, field, v.condition, form.initialValues);
v.actions?.forEach((h) => {
linkageAction({
operator: h.operator,
field,
condition: v.condition,
values,
variables,
localVariables,
});
});
}, [linkageRules, values, designable]);
});
}, [JSON.stringify(linkageRules), values, designable, field]);
const renderButton = () => {
if (!designable && field?.data?.hidden) {
return null;
const handleButtonClick = useCallback(
(e: React.MouseEvent) => {
if (isPortalInBody(e.target as Element)) {
return;
}
return (
<SortableItem
data-testid={`${fieldSchema['x-action'] || fieldSchema.name}-action`}
{...others}
loading={field?.data?.loading}
icon={icon ? <Icon type={icon} /> : null}
disabled={disabled}
style={{
...others.style,
opacity: designable && field?.data?.hidden && 0.1,
}}
onClick={(e: React.MouseEvent) => {
if (!disabled) {
e.preventDefault();
e.stopPropagation();
if (!disabled) {
const onOk = () => {
onClick?.(e);
setVisible(true);
@ -103,9 +105,33 @@ export const Action: ComposedAction = observer(
onOk();
}
}
}}
},
[confirm, disabled, modal, onClick, run],
);
const buttonStyle = useMemo(() => {
return {
...others.style,
opacity: designable && field?.data?.hidden && 0.1,
};
}, [designable, field?.data?.hidden, others.style]);
const renderButton = () => {
if (!designable && field?.data?.hidden) {
return null;
}
return (
<SortableItem
data-testid={`${fieldSchema['x-action'] || fieldSchema.name}-action`}
{...others}
loading={field?.data?.loading}
icon={icon ? <Icon type={icon} /> : null}
disabled={disabled}
style={buttonStyle}
onClick={handleButtonClick}
component={tarComponent || Button}
className={classnames(componentCls, hashId, className)}
className={classnames(componentCls, hashId, className, 'nb-action')}
type={props.type === 'danger' ? undefined : props.type}
>
{actionTitle}

View File

@ -1,7 +1,7 @@
import type { ISchema } from '@formily/react';
import { last } from 'lodash';
import { conditionAnalyse } from '../../common/utils/uitls';
import { ActionType } from '../../../schema-settings/LinkageRules/type';
import { VariableOption, VariablesContextType } from '../../../variables/types';
import { conditionAnalyses } from '../../common/utils/uitls';
const validateJSON = {
validator: `{{(value, rule)=> {
if (!value) {
@ -69,52 +69,69 @@ export const requestSettingsSchema: ISchema = {
},
};
export const linkageAction = (operator, field, condition, values) => {
const disableResult = field?.linkageProperty?.disabled || [false];
const displayResult = field?.linkageProperty?.display || ['visible'];
export const linkageAction = async ({
operator,
field,
condition,
values,
variables,
localVariables,
}: {
operator;
field;
condition;
values;
variables: VariablesContextType;
localVariables: VariableOption[];
}) => {
field.data = field.data || {};
switch (operator) {
case ActionType.Visible:
if (conditionAnalyse(condition, values)) {
displayResult.push(operator);
field.data = field.data || {};
field.data.hidden = false;
reset(field);
if (await conditionAnalyses({ rules: condition, formValues: values, variables, localVariables })) {
field._display = '_display' in field ? field._display : field.display;
field.display = operator;
} else {
field.display = field._display;
}
field.linkageProperty = {
...field.linkageProperty,
display: displayResult,
};
field.display = last(displayResult);
break;
case ActionType.Hidden:
if (conditionAnalyse(condition, values)) {
field.data = field.data || {};
reset(field);
if (await conditionAnalyses({ rules: condition, formValues: values, variables, localVariables })) {
field.data.hidden = true;
} else {
field.data = field.data || {};
field.data.hidden = false;
}
break;
case ActionType.Disabled:
if (conditionAnalyse(condition, values)) {
disableResult.push(true);
reset(field);
if (await conditionAnalyses({ rules: condition, formValues: values, variables, localVariables })) {
field.data.disabled = true;
field.disabled = true;
} else {
field.data.disabled = false;
field.disabled = false;
}
field.linkageProperty = {
...field.linkageProperty,
disabled: disableResult,
};
field.disabled = last(disableResult);
break;
case ActionType.Active:
if (conditionAnalyse(condition, values)) {
disableResult.push(false);
reset(field);
if (await conditionAnalyses({ rules: condition, formValues: values, variables, localVariables })) {
field.data.disabled = false;
field.disabled = false;
} else {
field.data.disabled = true;
field.disabled = true;
}
field.linkageProperty = {
...field.linkageProperty,
disabled: disableResult,
};
field.disabled = last(disableResult);
break;
default:
return null;
}
};
function reset(field: any) {
field.display = field._display;
field.data.hidden = false;
field.data.disabled = false;
field.disabled = false;
}

View File

@ -5,7 +5,7 @@ import { isFunction } from 'mathjs';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { RecordProvider, useAPIClient } from '../../../';
import { isVariable } from '../../common/utils/uitls';
import { isVariable } from '../../../variables/utils/isVariable';
import { RemoteSelect, RemoteSelectProps } from '../remote-select';
import useServiceOptions, { useAssociationFieldContext } from './hooks';
@ -110,3 +110,26 @@ interface AssociationSelectInterface {
}
export const AssociationSelect = InternalAssociationSelect as unknown as AssociationSelectInterface;
export const AssociationSelectReadPretty = connect(
(props: any) => {
const service = useServiceOptions(props);
if (props.fieldNames) {
return <RemoteSelect.ReadPretty {...props} service={service}></RemoteSelect.ReadPretty>;
}
return null;
},
mapProps(
{
dataSource: 'options',
loading: true,
},
(props, field) => {
return {
...props,
fieldNames: props.fieldNames && { ...props.fieldNames, ...field.componentProps.fieldNames },
suffixIcon: field?.['loading'] || field?.['validating'] ? <LoadingOutlined /> : props.suffixIcon,
};
},
),
);

View File

@ -167,7 +167,7 @@ const InternalFileManager = (props) => {
}}
>
<RecordPickerProvider {...pickerProps}>
<CollectionProvider name={collectionField.target}>
<CollectionProvider name={collectionField?.target}>
<FormProvider>
<TableSelectorParamsProvider params={{ filter: getFilter() }}>
<SchemaComponentOptions scope={{ usePickActionProps, useTableSelectorProps }}>

View File

@ -1,14 +1,14 @@
import { observer, useField, connect, createSchemaField, FormProvider, useFieldSchema, Field } from '@formily/react';
import { createForm, onFormValuesChange } from '@formily/core';
import { uid } from '@formily/shared';
import { Space, Tag, Spin, Select as AntdSelect, Input } from 'antd';
import { ArrayItems, FormItem } from '@formily/antd-v5';
import React, { useState, useCallback, useEffect, useMemo } from 'react';
import { createForm, onFormValuesChange } from '@formily/core';
import { FormProvider, connect, createSchemaField, observer, useField, useFieldSchema } from '@formily/react';
import { uid } from '@formily/shared';
import { Select as AntdSelect, Input, Space, Spin, Tag } from 'antd';
import dayjs from 'dayjs';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useCompile, SchemaComponent } from '../../../schema-component';
import { useAPIClient, useCollectionManager } from '../../../';
import { mergeFilter } from '../../../block-provider/SharedFilterProvider';
import { SchemaComponent, useCompile } from '../../../schema-component';
import useServiceOptions, { useAssociationFieldContext } from './hooks';
const EMPTY = 'N/A';
@ -73,7 +73,7 @@ const CascadeSelect = connect((props) => {
if (Array.isArray(label)) {
label = label
.map((item, index) => {
const option = targetField.uiSchema.enum.find((i) => i.value === item);
const option = (targetField.uiSchema.enum as any).find?.((i) => i.value === item);
if (option) {
return (
<Tag key={index} color={option.color} style={{ marginRight: 3 }}>
@ -86,7 +86,7 @@ const CascadeSelect = connect((props) => {
})
.reverse();
} else {
const item = targetField.uiSchema.enum.find((i) => i.value === label);
const item = (targetField.uiSchema.enum as any).find?.((i) => i.value === label);
if (item) {
label = <Tag color={item.color}>{item.label}</Tag>;
}

View File

@ -4,7 +4,7 @@ import { RecursionField, SchemaOptionsContext, observer, useField, useFieldSchem
import React, { useEffect } from 'react';
import { ACLCollectionProvider, useACLActionParamsContext } from '../../../acl';
import { CollectionProvider } from '../../../collection-manager';
import { useSchemaOptionsContext } from '../../../schema-component';
import { FormItem, useSchemaOptionsContext } from '../../../schema-component';
import Select from '../select/Select';
import { useAssociationFieldContext, useInsertSchema } from './hooks';
import schema from './schema';
@ -24,8 +24,9 @@ export const InternalSubTable = observer(
const option = useSchemaOptionsContext();
const components = {
...option.components,
'Radio.Group': (props) => <Select data-testid="antd-select" {...props} />,
'Checkbox.Group': (props) => <Select data-testid="antd-select" multiple={true} mode="multiple" {...props} />,
FormItem,
'Radio.Group': Select,
'Checkbox.Group': (props) => <Select multiple={true} mode="multiple" {...props} />,
};
return (
<CollectionProvider name={options.target}>

View File

@ -57,7 +57,7 @@ export const ReadPrettyInternalViewer: React.FC = observer(
);
const text = getLabelFormatValue(compile(labelUiSchema), val, true);
return (
<Fragment key={`${record.id}_${index}`}>
<Fragment key={`${record?.id}_${index}`}>
<span>
{snapshot ? (
text

View File

@ -2,15 +2,23 @@ import { CloseOutlined, PlusOutlined } from '@ant-design/icons';
import { css } from '@emotion/css';
import { ArrayField } from '@formily/core';
import { spliceArrayState } from '@formily/core/esm/shared/internals';
import { observer, RecursionField, useFieldSchema } from '@formily/react';
import { RecursionField, observer, useFieldSchema } from '@formily/react';
import { action } from '@formily/reactive';
import { each } from '@formily/shared';
import { Button, Card, Divider, Tooltip } from 'antd';
import React, { useContext } from 'react';
import _ from 'lodash';
import React, { useCallback, useContext } from 'react';
import { useTranslation } from 'react-i18next';
import { FormActiveFieldsProvider } from '../../../block-provider';
import { RecordIndexProvider, RecordProvider, useRecord } from '../../../record-provider';
import { isPatternDisabled, isSystemField } from '../../../schema-settings';
import {
DefaultValueProvider,
interfacesOfUnsupportedDefaultValue,
} from '../../../schema-settings/hooks/useIsAllowToSetDefaultValue';
import { LocalVariablesProvider } from '../../../variables/hooks/useLocalVariables';
import { AssociationFieldContext } from './context';
import { useAssociationFieldContext } from './hooks';
import { RecordProvider, useRecord } from '../../../record-provider';
export const Nester = (props) => {
const { options } = useContext(AssociationFieldContext);
@ -24,7 +32,46 @@ export const Nester = (props) => {
};
const ToOneNester = (props) => {
return <Card bordered={true}>{props.children}</Card>;
const { field } = useAssociationFieldContext<ArrayField>();
const record = useRecord();
// 保存原始数据,用于判断是否允许设置默认值
const recordRef = React.useRef(_.omit(record, '__parent'));
const isAllowToSetDefaultValue = useCallback(({ form, fieldSchema, collectionField, getInterface }) => {
// 当 Field component 不是下列组件时,不允许设置默认值
if (
collectionField.target &&
fieldSchema['x-component-props']?.mode &&
!['Picker', 'Select'].includes(fieldSchema['x-component-props'].mode)
) {
return false;
}
// hasOne 和 belongsTo 类型的字段只能有一个值,不会新增值,所以在编辑状态下不允许设置默认值
if (!_.isEmpty(recordRef.current)) {
return false;
}
return (
!form?.readPretty &&
!isPatternDisabled(fieldSchema) &&
!interfacesOfUnsupportedDefaultValue.includes(collectionField?.interface) &&
!isSystemField(collectionField, getInterface)
);
}, []);
return (
<FormActiveFieldsProvider name="nester">
<RecordProvider record={field.value}>
<LocalVariablesProvider iterationCtx={field.value}>
<DefaultValueProvider isAllowToSetDefaultValue={isAllowToSetDefaultValue}>
<Card bordered={true}>{props.children}</Card>
</DefaultValueProvider>
</LocalVariablesProvider>
</RecordProvider>
</FormActiveFieldsProvider>
);
};
const ToManyNester = observer(
@ -32,7 +79,30 @@ const ToManyNester = observer(
const fieldSchema = useFieldSchema();
const { options, field, allowMultiple, allowDissociate } = useAssociationFieldContext<ArrayField>();
const { t } = useTranslation();
return (field.value || []).length > 0 ? (
if (!Array.isArray(field.value)) {
field.value = [];
}
const isAllowToSetDefaultValue = useCallback(({ form, fieldSchema, collectionField, getInterface }) => {
// 当 Field component 不是下列组件时,不允许设置默认值
if (
collectionField.target &&
fieldSchema['x-component-props']?.mode &&
!['Picker', 'Select'].includes(fieldSchema['x-component-props'].mode)
) {
return false;
}
return (
!form?.readPretty &&
!isPatternDisabled(fieldSchema) &&
!interfacesOfUnsupportedDefaultValue.includes(collectionField?.interface) &&
!isSystemField(collectionField, getInterface)
);
}, []);
return field.value.length > 0 ? (
<Card
bordered={true}
style={{ position: 'relative' }}
@ -42,7 +112,7 @@ const ToManyNester = observer(
}
`}
>
{(field.value || []).map((value, index) => {
{field.value.map((value, index) => {
let allowed = allowDissociate;
if (!allowDissociate) {
allowed = !value?.[options.targetKey];
@ -55,7 +125,7 @@ const ToManyNester = observer(
<PlusOutlined
style={{ zIndex: 1000, marginRight: '10px', color: '#a8a3a3' }}
onClick={() => {
action(() => {
void action(() => {
if (!Array.isArray(field.value)) {
field.value = [];
}
@ -80,7 +150,7 @@ const ToManyNester = observer(
<CloseOutlined
style={{ zIndex: 1000, color: '#a8a3a3' }}
onClick={() => {
action(() => {
void action(() => {
spliceArrayState(field as any, {
startIndex: index,
deleteCount: 1,
@ -93,9 +163,21 @@ const ToManyNester = observer(
</Tooltip>
)}
</div>
<FormActiveFieldsProvider name="nester">
<RecordProvider record={value}>
<RecursionField onlyRenderProperties basePath={field.address.concat(index)} schema={fieldSchema} />
<LocalVariablesProvider iterationCtx={value}>
<RecordIndexProvider index={index}>
<DefaultValueProvider isAllowToSetDefaultValue={isAllowToSetDefaultValue}>
<RecursionField
onlyRenderProperties
basePath={field.address.concat(index)}
schema={fieldSchema}
/>
</DefaultValueProvider>
</RecordIndexProvider>
</LocalVariablesProvider>
</RecordProvider>
</FormActiveFieldsProvider>
<Divider />
</React.Fragment>
);

View File

@ -7,12 +7,15 @@ import { action } from '@formily/reactive';
import { isArr } from '@formily/shared';
import { Button } from 'antd';
import React from 'react';
import { FormActiveFieldsProvider } from '../../../block-provider';
import { useSubTableSpecialCase } from '../form-item/hooks/useSpecialCase';
import { Table } from '../table-v2/Table';
import { useAssociationFieldContext } from './hooks';
export const SubTable: any = observer(
(props: any) => {
const { field } = useAssociationFieldContext<ArrayField>();
useSubTableSpecialCase({ field });
const move = (fromIndex: number, toIndex: number) => {
if (toIndex === undefined) return;
if (!isArr(field.value)) return;
@ -59,6 +62,7 @@ export const SubTable: any = observer(
}
`}
>
<FormActiveFieldsProvider name="nester">
<Table
className={css`
.ant-formily-item.ant-formily-item-feedback-layout-loose {
@ -87,7 +91,7 @@ export const SubTable: any = observer(
onClick={() => {
field.value = field.value || [];
field.value.push({});
field.onInput(field.value);
void field.onInput(field.value);
}}
icon={<PlusOutlined />}
>
@ -97,6 +101,7 @@ export const SubTable: any = observer(
}
isSubTable={true}
/>
</FormActiveFieldsProvider>
</div>
);
},

View File

@ -1,18 +1,22 @@
import { GeneralField } from '@formily/core';
import { useField, useFieldSchema, useForm } from '@formily/react';
import flat from 'flat';
import { isString } from 'lodash';
import { useField, useFieldSchema } from '@formily/react';
import { reaction } from '@formily/reactive';
import { flatten } from '@nocobase/utils/client';
import _, { isString } from 'lodash';
import cloneDeep from 'lodash/cloneDeep';
import { useCallback, useContext, useMemo } from 'react';
import { useBlockRequestContext } from '../../../block-provider/BlockProvider';
import { useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { useFormBlockContext } from '../../../block-provider';
import { mergeFilter } from '../../../block-provider/SharedFilterProvider';
import { useCollection, useCollectionManager } from '../../../collection-manager';
import { isInFilterFormBlock } from '../../../filter-provider';
import { useRecord } from '../../../record-provider';
import { getInnermostKeyAndValue } from '../../common/utils/uitls';
import { useParseDataScopeFilter } from '../../../schema-settings';
import { DEBOUNCE_WAIT } from '../../../variables';
import { getPath } from '../../../variables/utils/getPath';
import { isVariable } from '../../../variables/utils/isVariable';
import { useDesignable } from '../../hooks';
import { removeNullCondition } from '../filter';
import { AssociationFieldContext } from './context';
import { extractFilterfield, extractValuesByPattern, generatePattern, parseVariables } from './util';
export const useInsertSchema = (component) => {
const fieldSchema = useFieldSchema();
@ -49,74 +53,47 @@ export default function useServiceOptions(props) {
const params = service?.params || {};
const fieldSchema = useFieldSchema();
const field = useField();
const form = useForm();
const ctx = useBlockRequestContext();
const { getField } = useCollection();
const { getCollectionFields, getCollectionJoinField } = useCollectionManager();
const { getCollectionJoinField } = useCollectionManager();
const record = useRecord();
const parseFilter = useCallback(
(rules) => {
if (!rules) {
return undefined;
}
if (typeof rules === 'string') {
return rules;
}
const type = Object.keys(rules)[0] || '$and';
const conditions = rules[type];
const results = [];
conditions?.forEach((c) => {
const jsonlogic = getInnermostKeyAndValue(c);
const regex = /{{(.*?)}}/;
const matches = jsonlogic.value?.match?.(regex);
if (!matches || (!matches[1].includes('$form') && !matches[1].includes('$iteration'))) {
results.push(c);
return;
}
const associationfield = extractFilterfield(matches[1]);
const filterCollectionField = getCollectionJoinField(`${ctx.props.collection}.${associationfield}`);
if (['o2m', 'm2m'].includes(filterCollectionField?.interface)) {
// 对多子表单
const pattern = generatePattern(matches?.[1], associationfield);
const parseValue: any = extractValuesByPattern(flat(form.values), pattern);
const filters = parseValue.map((v) => {
return JSON.parse(JSON.stringify(c).replace(jsonlogic.value, v));
});
results.push({ $or: filters });
} else {
const variablesCtx = { $form: form.values, $iteration: form.values };
let str = matches?.[1];
if (str.includes('$iteration')) {
const path = field.path.segments.concat([]);
path.pop();
str = str.replace('$iteration.', `$iteration.${path.join('.')}.`);
}
const parseValue = parseVariables(str, variablesCtx);
if (Array.isArray(parseValue)) {
const filters = parseValue.map((v) => {
return JSON.parse(JSON.stringify(c).replace(jsonlogic.value, v));
});
results.push({ $or: filters });
} else {
const filterObj = JSON.parse(
JSON.stringify(c).replace(jsonlogic.value, str.endsWith('id') ? parseValue ?? 0 : parseValue),
);
results.push(filterObj);
}
}
});
return { [type]: results };
},
[ctx.props?.collection, field.path.segments, form.values, getCollectionJoinField],
);
const { parseFilter } = useParseDataScopeFilter();
const [fieldServiceFilter, setFieldServiceFilter] = useState(null);
const { form } = useFormBlockContext();
const fieldServiceFilter = useMemo(() => {
useEffect(() => {
const filterFromSchema = isString(fieldSchema?.['x-component-props']?.service?.params?.filter)
? field.componentProps?.service?.params?.filter
: fieldSchema?.['x-component-props']?.service?.params?.filter;
return mergeFilter([parseFilter(filterFromSchema) || service?.params?.filter]);
}, [field.componentProps?.service?.params?.filter, fieldSchema, parseFilter, service?.params?.filter]);
const _run = async () => {
const result = await parseFilter(mergeFilter([filterFromSchema || service?.params?.filter]));
setFieldServiceFilter(removeNullCondition(result));
};
const run = _.debounce(_run, DEBOUNCE_WAIT);
_run();
reaction(() => {
// 这一步主要是为了使 reaction 能够收集到依赖
const flat = flatten(filterFromSchema, {
breakOn({ key }) {
return key.startsWith('$') && key !== '$and' && key !== '$or';
},
transformValue(value) {
if (!isVariable(value)) {
return value;
}
const result = _.get({ $nForm: form?.values }, getPath(value));
return result;
},
});
return flat;
}, run);
}, [
field.componentProps?.service?.params?.filter,
fieldSchema?.['x-component-props']?.service?.params?.filter,
service?.params?.filter,
]);
const normalizeValues = useCallback(
(obj) => {
@ -132,16 +109,21 @@ export default function useServiceOptions(props) {
if (props.value === undefined || props.value === null) {
return;
}
let result: any[] = null;
if (Array.isArray(props.value)) {
return props.value.map(normalizeValues);
result = props.value.map(normalizeValues);
} else {
return [normalizeValues(props.value)];
result = [normalizeValues(props.value)];
}
return result.filter(Boolean);
}, [props.value, normalizeValues]);
const collectionField = useMemo(() => {
return getField(fieldSchema.name) || getCollectionJoinField(fieldSchema?.['x-collection-field']);
}, [fieldSchema.name]);
}, [fieldSchema]);
const sourceValue = record?.[collectionField?.sourceKey];
const filter = useMemo(() => {
@ -169,7 +151,7 @@ export default function useServiceOptions(props) {
},
}
: null,
params?.filter && value
params?.filter && value?.length
? {
[fieldNames?.value]: {
['$in']: value,
@ -179,7 +161,16 @@ export default function useServiceOptions(props) {
],
'$or',
);
}, [params?.filter, getCollectionFields, collectionField, sourceValue, value, fieldNames?.value]);
}, [
collectionField?.interface,
collectionField.foreignKey,
fieldSchema,
fieldServiceFilter,
sourceValue,
params?.filter,
value,
fieldNames?.value,
]);
return useMemo(() => {
return {

View File

@ -1,8 +1,7 @@
import { ISchema } from '@formily/react';
import { ISchema, Schema } from '@formily/react';
import { isArr } from '@formily/shared';
import { getDefaultFormat, str2moment } from '@nocobase/utils/client';
import { Tag } from 'antd';
import { get, isFunction } from 'lodash';
import dayjs from 'dayjs';
import React from 'react';
import { CollectionFieldOptions, useCollectionManager } from '../../../collection-manager';
@ -88,43 +87,9 @@ export function isShowFilePicker(labelUiSchema) {
return labelUiSchema?.['x-component'] === 'Preview';
}
export const toValue = (value, placeholder) => {
if (value === null || value === undefined) {
return placeholder;
}
return value;
};
export const parseVariables = (str: string, ctx) => {
if (str) {
const result = get(ctx, str);
return isFunction(result) ? result() : result;
} else {
return str;
}
};
export function extractFilterfield(str) {
const match = str.match(/^\$form\.([^.[\]]+)/);
if (match) {
return match[1];
}
return null;
}
export function extractValuesByPattern(obj, pattern) {
const regexPattern = new RegExp(pattern.replace(/\*/g, '\\d+'));
const result = [];
for (const key in obj) {
if (regexPattern.test(key)) {
const value = obj[key];
result.push(value);
}
}
return result;
}
export function generatePattern(str, fieldName) {
const result = str.replace(`$form.${fieldName}.`, `${fieldName}.*.`);
return result;
/**
* `子表格` `子表单`
*/
export function isSubMode(fieldSchema: Schema) {
return ['Nester', 'SubTable', 'PopoverNester'].includes(fieldSchema['x-component-props']?.mode);
}

View File

@ -2,26 +2,21 @@ import { ISchema, useField, useFieldSchema } from '@formily/react';
import _ from 'lodash';
import React from 'react';
import { useTranslation } from 'react-i18next';
import {
useCollection,
useCollectionFilterOptions,
useCollectionManager,
useSortFields,
} from '../../../collection-manager';
import { useFormBlockContext } from '../../../block-provider';
import { useCollection, useCollectionManager, useSortFields } from '../../../collection-manager';
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
import { useCompile, useDesignable } from '../../hooks';
export const AssociationFilterItemDesigner = (props) => {
const fieldSchema = useFieldSchema();
const { form } = useFormBlockContext();
const field = useField();
const { t } = useTranslation();
const { getCollectionJoinField } = useCollectionManager();
const { getField } = useCollection();
const collectionField = getField(fieldSchema['name']!) || getCollectionJoinField(fieldSchema['x-collection-field']);
const filterEnum = collectionField?.target ? useCollectionFilterOptions(collectionField?.target) : [];
const defaultFilter = fieldSchema?.['x-component-props']?.params?.filter || {};
const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']);
const { getCollectionFields } = useCollectionManager();
const compile = useCompile();
@ -30,9 +25,6 @@ export const AssociationFilterItemDesigner = (props) => {
const targetFields = collectionField?.target ? getCollectionFields(collectionField?.target) : [];
const options = targetFields
// .filter(
// (field) => field?.interface && ['id', 'input', 'phone', 'email', 'integer', 'number'].includes(field?.interface),
// )
.filter((field) => !field?.target && field.type !== 'boolean')
.map((field) => ({
value: field?.name,
@ -117,22 +109,10 @@ export const AssociationFilterItemDesigner = (props) => {
dn.refresh();
}}
/>
<SchemaSettings.ModalItem
title={t('Set the data scope')}
schema={
{
type: 'object',
title: t('Set the data scope'),
properties: {
filter: {
default: defaultFilter,
enum: filterEnum,
'x-component': 'Filter',
'x-component-props': {},
},
},
} as ISchema
}
<SchemaSettings.DataScope
collectionName={collectionField?.target}
defaultFilter={fieldSchema?.['x-component-props']?.params?.filter || {}}
form={form}
onSubmit={({ filter }) => {
_.set(field.componentProps, 'params', {
...field.componentProps?.params,

View File

@ -7,20 +7,15 @@ import _ from 'lodash';
import React, { useCallback, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useFilterByTk, useFormBlockContext } from '../../../block-provider';
import {
useCollection,
useCollectionFilterOptions,
useCollectionManager,
useSortFields,
} from '../../../collection-manager';
import { useCollection, useCollectionManager, useSortFields } from '../../../collection-manager';
import { GeneralSchemaItems } from '../../../schema-items';
import { GeneralSchemaDesigner, SchemaSettings, isPatternDisabled, isShowDefaultValue } from '../../../schema-settings';
import { GeneralSchemaDesigner, SchemaSettings, isPatternDisabled } from '../../../schema-settings';
import useIsAllowToSetDefaultValue from '../../../schema-settings/hooks/useIsAllowToSetDefaultValue';
import { useIsShowMultipleSwitch } from '../../../schema-settings/hooks/useIsShowMultipleSwitch';
import { useCompile, useDesignable, useFieldComponentOptions, useFieldTitle } from '../../hooks';
import { removeNullCondition } from '../filter';
import { RemoteSelect, RemoteSelectProps } from '../remote-select';
import { defaultFieldNames } from '../select';
import { FilterDynamicComponent } from '../table-v2/FilterDynamicComponent';
import { ReadPretty } from './ReadPretty';
import useServiceOptions from './useServiceOptions';
@ -105,6 +100,7 @@ AssociationSelect.Designer = function Designer() {
const { dn, refresh, insertAdjacent } = useDesignable();
const compile = useCompile();
const IsShowMultipleSwitch = useIsShowMultipleSwitch();
const { isAllowToSetDefaultValue } = useIsAllowToSetDefaultValue();
const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']);
const fieldComponentOptions = useFieldComponentOptions();
@ -119,8 +115,6 @@ AssociationSelect.Designer = function Designer() {
const sortFields = useSortFields(collectionField?.target);
const defaultSort = field.componentProps?.service?.params?.sort || [];
const defaultFilter = field.componentProps?.service?.params?.filter || {};
const dataSource = useCollectionFilterOptions(collectionField?.target);
// TODO: 这里 fieldSchema['x-read-pretty'] 的值为 true但是 field.readPretty 的值却为 false不知道什么原因
useEffect(() => {
@ -287,10 +281,7 @@ AssociationSelect.Designer = function Designer() {
}}
/>
)}
{form &&
!form?.readPretty &&
isShowDefaultValue(collectionField, getInterface) &&
!isPatternDisabled(fieldSchema) && (
{isAllowToSetDefaultValue() && (
<SchemaSettings.ModalItem
title={t('Set default value')}
components={{ ArrayCollapse, FormLayout }}
@ -409,25 +400,10 @@ AssociationSelect.Designer = function Designer() {
}}
/>
) : null}
<SchemaSettings.ModalItem
title={t('Set the data scope')}
schema={
{
type: 'object',
title: t('Set the data scope'),
properties: {
filter: {
default: defaultFilter,
// title: '数据范围',
enum: dataSource,
'x-component': 'Filter',
'x-component-props': {
dynamicComponent: (props) => FilterDynamicComponent({ ...props }),
},
},
},
} as ISchema
}
<SchemaSettings.DataScope
collectionName={collectionField?.target}
defaultFilter={field.componentProps?.service?.params?.filter || {}}
form={form}
onSubmit={({ filter }) => {
filter = removeNullCondition(filter);
_.set(field.componentProps, 'service.params.filter', filter);
@ -646,8 +622,6 @@ AssociationSelect.FilterDesigner = function FilterDesigner() {
const sortFields = useSortFields(collectionField?.target);
const defaultSort = field.componentProps?.service?.params?.sort || [];
const defaultFilter = field.componentProps?.service?.params?.filter || {};
const dataSource = useCollectionFilterOptions(collectionField?.target);
const sort = defaultSort?.map((item: string) => {
return item.startsWith('-')
@ -839,25 +813,10 @@ AssociationSelect.FilterDesigner = function FilterDesigner() {
}}
/>
)}
<SchemaSettings.ModalItem
title={t('Set the data scope')}
schema={
{
type: 'object',
title: t('Set the data scope'),
properties: {
filter: {
default: defaultFilter,
// title: '数据范围',
enum: dataSource,
'x-component': 'Filter',
'x-component-props': {
dynamicComponent: (props) => FilterDynamicComponent({ ...props }),
},
},
},
} as ISchema
}
<SchemaSettings.DataScope
collectionName={collectionField?.target}
defaultFilter={field.componentProps?.service?.params?.filter || {}}
form={form}
onSubmit={({ filter }) => {
filter = removeNullCondition(filter);
_.set(field.componentProps, 'service.params.filter', filter);

View File

@ -1,25 +1,22 @@
import { ISchema, useField, useFieldSchema } from '@formily/react';
import { useField, useFieldSchema } from '@formily/react';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { FixedBlockDesignerItem, removeNullCondition, useDesignable } from '../..';
import { useCalendarBlockContext } from '../../../block-provider';
import { useCalendarBlockContext, useFormBlockContext } from '../../../block-provider';
import { useCollection, useCollectionManager } from '../../../collection-manager';
import { useCollectionFilterOptions } from '../../../collection-manager/action-hooks';
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
import { useSchemaTemplate } from '../../../schema-templates';
import { FilterDynamicComponent } from '../table-v2/FilterDynamicComponent';
export const CalendarDesigner = () => {
const field = useField();
const fieldSchema = useFieldSchema();
const { form } = useFormBlockContext();
const { name, title } = useCollection();
const { getCollectionFieldsOptions } = useCollectionManager();
const dataSource = useCollectionFilterOptions(name);
const { service } = useCalendarBlockContext();
const { dn } = useDesignable();
const { t } = useTranslation();
const template = useSchemaTemplate();
const defaultFilter = fieldSchema?.['x-decorator-props']?.params?.filter || {};
const fieldNames = fieldSchema?.['x-decorator-props']?.['fieldNames'] || {};
const defaultResource = fieldSchema?.['x-decorator-props']?.resource;
@ -105,30 +102,10 @@ export const CalendarDesigner = () => {
dn.refresh();
}}
/>
<SchemaSettings.ModalItem
title={t('Set the data scope')}
schema={
{
type: 'object',
title: t('Set the data scope'),
properties: {
filter: {
default: defaultFilter,
enum: dataSource,
'x-component': 'Filter',
'x-component-props': {
dynamicComponent: (props) => FilterDynamicComponent({ ...props }),
},
},
},
} as ISchema
}
initialValues={
{
// title: field.title,
// icon: field.componentProps.icon,
}
}
<SchemaSettings.DataScope
collectionName={name}
defaultFilter={fieldSchema?.['x-decorator-props']?.params?.filter || {}}
form={form}
onSubmit={({ filter }) => {
filter = removeNullCondition(filter);
const params = field.decoratorProps.params || {};

View File

@ -56,17 +56,20 @@ export const Cascader = connect(
if (selectedOptions[index]) {
return <span key={label}>{label}</span>;
}
const item = toArr(value).find((item) => item[fieldNames.value] === label);
const item = toArr(value)
.filter(Boolean)
.find((item) => item[fieldNames.value] === label);
return <span key={label}>{item?.[fieldNames.label] || label}</span>;
})}
</Space>
);
};
const handelDropDownVisible = (value) => {
if (value && !field.dataSource.length) {
if (value && !field.dataSource?.length) {
run();
}
};
return (
<AntdCascader
loading={loading}

View File

@ -27,7 +27,7 @@ function useOptions({ filter, isTableOid }: CollectionSelectProps) {
: collections;
const filtered =
typeof filter === 'function'
? (inheritCollections || currentCollections).filter(filter)
? ((inheritCollections || currentCollections) as any[]).filter(filter)
: inheritCollections || currentCollections;
return filtered
.filter((item) => !item.hidden)

View File

@ -37,8 +37,9 @@ const InternalRangePicker = connect(
export const DatePicker = (props) => {
const { utc = true } = useDatePickerContext();
const value = Array.isArray(props.value) ? props.value[0] : props.value;
props = { utc, ...props };
return <InternalDatePicker {...props} />;
return <InternalDatePicker {...props} value={value} />;
};
DatePicker.ReadPretty = ReadPretty.DatePicker;

View File

@ -10,6 +10,18 @@ describe('getDateRanges', () => {
expect(end.toISOString()).toBe(dayjs().endOf('day').toISOString());
});
test('yesterday', () => {
const [start, end] = dateRanges.yesterday();
expect(dayjs(start).isSame(dayjs().subtract(1, 'day'), 'day')).toBe(true);
expect(dayjs(end).isSame(dayjs().subtract(1, 'day'), 'day')).toBe(true);
});
test('tomorrow', () => {
const [start, end] = dateRanges.tomorrow();
expect(dayjs(start).isSame(dayjs().add(1, 'day'), 'day')).toBe(true);
expect(dayjs(end).isSame(dayjs().add(1, 'day'), 'day')).toBe(true);
});
it('lastWeek', () => {
const [start, end] = dateRanges.lastWeek();
expect(start.toISOString()).toBe(dayjs().add(-1, 'week').startOf('isoWeek').toISOString());

View File

@ -6,7 +6,9 @@ const toStringByPicker = (value, picker, timezone: 'gmt' | 'local') => {
if (!dayjs.isDayjs(value)) return value;
if (timezone === 'local') {
const offset = new Date().getTimezoneOffset();
return dayjs(toStringByPicker(value, picker, 'gmt')).add(offset, 'minutes').toISOString();
return dayjs(toStringByPicker(value, picker, 'gmt'))
.add(offset, 'minutes')
.toISOString();
}
if (picker === 'year') {
@ -136,18 +138,24 @@ const getEnd = (offset: any, unit: any) => {
export const getDateRanges = () => {
return {
now: () => dayjs(),
today: () => [getStart(0, 'day'), getEnd(0, 'day')],
lastWeek: () => [getStart(-1, 'isoWeek'), getEnd(-1, 'isoWeek')],
yesterday: () => [getStart(-1, 'day'), getEnd(-1, 'day')],
tomorrow: () => [getStart(1, 'day'), getEnd(1, 'day')],
thisWeek: () => [getStart(0, 'isoWeek'), getEnd(0, 'isoWeek')],
lastWeek: () => [getStart(-1, 'isoWeek'), getEnd(-1, 'isoWeek')],
nextWeek: () => [getStart(1, 'isoWeek'), getEnd(1, 'isoWeek')],
lastMonth: () => [getStart(-1, 'month'), getEnd(-1, 'month')],
thisIsoWeek: () => [getStart(0, 'isoWeek'), getEnd(0, 'isoWeek')],
lastIsoWeek: () => [getStart(-1, 'isoWeek'), getEnd(-1, 'isoWeek')],
nextIsoWeek: () => [getStart(1, 'isoWeek'), getEnd(1, 'isoWeek')],
thisMonth: () => [getStart(0, 'month'), getEnd(0, 'month')],
lastMonth: () => [getStart(-1, 'month'), getEnd(-1, 'month')],
nextMonth: () => [getStart(1, 'month'), getEnd(1, 'month')],
lastQuarter: () => [getStart(-1, 'quarter'), getEnd(-1, 'quarter')],
thisQuarter: () => [getStart(0, 'quarter'), getEnd(0, 'quarter')],
lastQuarter: () => [getStart(-1, 'quarter'), getEnd(-1, 'quarter')],
nextQuarter: () => [getStart(1, 'quarter'), getEnd(1, 'quarter')],
lastYear: () => [getStart(-1, 'year'), getEnd(-1, 'year')],
thisYear: () => [getStart(0, 'year'), getEnd(0, 'year')],
lastYear: () => [getStart(-1, 'year'), getEnd(-1, 'year')],
nextYear: () => [getStart(1, 'year'), getEnd(1, 'year')],
last7Days: () => [getStart(-6, 'days'), getEnd(0, 'days')],
next7Days: () => [getStart(1, 'day'), getEnd(7, 'days')],

View File

@ -1,17 +1,30 @@
import { createForm, onFieldValueChange } from '@formily/core';
import { FieldContext, FormContext } from '@formily/react';
import { merge } from '@formily/shared';
import React, { useContext, useMemo } from 'react';
import React, { useCallback, useContext, useMemo } from 'react';
import { CollectionFieldOptions } from '../../../collection-manager';
import { SchemaComponent } from '../../core';
import { useComponent } from '../../hooks';
import { FilterContext } from './context';
const isDateComponent = {
'DatePicker.RangePicker': true,
DatePicker: true,
};
export interface DynamicComponentProps {
value: any;
/**
* `Filter`
*/
collectionField: CollectionFieldOptions;
onChange: (value: any) => void;
renderSchemaComponent: () => React.JSX.Element;
}
export const DynamicComponent = (props) => {
interface Props {
schema: any;
value: any;
collectionField: CollectionFieldOptions;
onChange: (value: any) => void;
}
export const DynamicComponent = (props: Props) => {
const { dynamicComponent, disabled } = useContext(FilterContext);
const component = useComponent(dynamicComponent);
const form = useMemo(() => {
@ -26,8 +39,8 @@ export const DynamicComponent = (props) => {
});
},
});
}, [JSON.stringify(props.schema), JSON.stringify(props.value)]);
const renderSchemaComponent = () => {
}, [JSON.stringify(props.value)]);
const renderSchemaComponent = useCallback(() => {
return (
<FieldContext.Provider value={null}>
<SchemaComponent
@ -47,13 +60,14 @@ export const DynamicComponent = (props) => {
/>
</FieldContext.Provider>
);
};
}, [props.schema]);
return (
<FormContext.Provider value={form}>
<div data-testid="dynamic-component-filter-item">
{component
? React.createElement(component, {
? React.createElement<DynamicComponentProps>(component, {
value: props.value,
collectionField: props.collectionField,
onChange: props?.onChange,
renderSchemaComponent,
})

View File

@ -3,12 +3,11 @@ import { observer, useField, useFieldSchema } from '@formily/react';
import React, { useEffect } from 'react';
import { useRequest } from '../../../api-client';
import { useProps } from '../../hooks/useProps';
import { DatePickerProvider } from '../date-picker';
import { FilterContext } from './context';
import { FilterActionDesigner } from './Filter.Action.Designer';
import { FilterAction } from './FilterAction';
import { FilterGroup } from './FilterGroup';
import { SaveDefaultValue } from './SaveDefaultValue';
import { FilterContext } from './context';
const useDef = (options) => {
const field = useField<ObjectFieldModel>();
@ -26,12 +25,12 @@ export const Filter: any = observer(
field.dataSource = data?.data || [];
},
});
useEffect(() => {
field.initialValue = fieldSchema.defaultValue;
}, []);
return (
<div className={className}>
<DatePickerProvider value={{ utc: false }}>
<FilterContext.Provider
value={{
field,
@ -43,9 +42,7 @@ export const Filter: any = observer(
}}
>
<FilterGroup {...props} bordered={false} />
{/* <pre>{JSON.stringify(field.value, null, 2)}</pre> */}
</FilterContext.Provider>
</DatePickerProvider>
</div>
);
},

View File

@ -2,7 +2,7 @@ import { CloseCircleOutlined } from '@ant-design/icons';
import { css } from '@emotion/css';
import { observer } from '@formily/react';
import { Cascader, Select, Space } from 'antd';
import React, { useContext } from 'react';
import React, { useCallback, useContext, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useCompile } from '../..';
import { DynamicComponent } from './DynamicComponent';
@ -14,27 +14,56 @@ export const FilterItem = observer(
const { t } = useTranslation();
const compile = useCompile();
const remove = useContext(RemoveConditionContext);
const { schema, fields, operators, dataIndex, operator, setDataIndex, setOperator, value, setValue } = useValues();
const {
schema,
fields,
operators,
dataIndex,
operator,
setDataIndex,
setOperator,
value,
setValue,
collectionField,
} = useValues();
const style = useMemo(() => ({ marginBottom: 8 }), []);
const fieldNames = useMemo(
() => ({
label: 'title',
value: 'name',
children: 'children',
}),
[],
);
const onChange = useCallback(
(value) => {
setDataIndex(value);
},
[setDataIndex],
);
const onOperatorsChange = useCallback(
(value) => {
setOperator(value);
},
[setOperator],
);
const removeStyle = useMemo(() => ({ color: '#bfbfbf' }), []);
return (
// 添加 nc-filter-item 类名是为了帮助编写测试时更容易选中该元素
<div style={{ marginBottom: 8 }} className="nc-filter-item">
<div style={style} className="nc-filter-item">
<Space>
<Cascader
data-testid="antd-cascader"
className={css`
width: 160px;
`}
fieldNames={{
label: 'title',
value: 'name',
children: 'children',
}}
fieldNames={fieldNames}
changeOnSelect={false}
value={dataIndex}
options={compile(fields)}
onChange={(value) => {
setDataIndex(value);
}}
onChange={onChange}
placeholder={t('Select field')}
/>
<Select
@ -45,15 +74,15 @@ export const FilterItem = observer(
popupMatchSelectWidth={false}
value={operator?.value}
options={compile(operators)}
onChange={(value) => {
setOperator(value);
}}
onChange={onOperatorsChange}
placeholder={t('Comparision')}
/>
{!operator?.noValue ? <DynamicComponent value={value} schema={schema} onChange={setValue} /> : null}
{!operator?.noValue ? (
<DynamicComponent value={value} schema={schema} collectionField={collectionField} onChange={setValue} />
) : null}
{!props.disabled && (
<a data-testid="close-icon-button">
<CloseCircleOutlined onClick={() => remove()} style={{ color: '#bfbfbf' }} />
<CloseCircleOutlined onClick={remove} style={removeStyle} />
</a>
)}
</Space>

View File

@ -1,9 +1,9 @@
import { ArrayField as ArrayFieldModel } from '@formily/core';
import { ObjectField, observer, useField } from '@formily/react';
import React from 'react';
import { RemoveConditionContext } from './context';
import { FilterGroup } from './FilterGroup';
import { FilterItem } from './FilterItem';
import { RemoveConditionContext } from './context';
export const FilterItems = observer(
(props) => {

View File

@ -3,7 +3,7 @@ import { Schema } from '@formily/react';
import { createContext } from 'react';
export interface FilterContextProps {
field?: ObjectField;
field?: ObjectField & { collectionName?: string };
fieldSchema?: Schema;
dynamicComponent?: any;
options?: any[];

View File

@ -46,6 +46,7 @@ export const useGetFilterFieldOptions = () => {
target: field.target,
title: field?.uiSchema?.title || field.name,
schema: field?.uiSchema,
interface: field.interface,
operators:
operators?.filter?.((operator) => {
return !operator?.visible || operator.visible(field);

View File

@ -3,9 +3,22 @@ import { merge } from '@formily/shared';
import flat, { unflatten } from 'flat';
import cloneDeep from 'lodash/cloneDeep';
import get from 'lodash/get';
import { useContext, useEffect } from 'react';
import { FilterContext } from './context';
import { useCallback, useContext, useEffect, useMemo } from 'react';
import { useCollection, useCollectionManager } from '../../../collection-manager';
import { FilterContext } from './context';
interface UseValuesReturn {
fields: any[];
collectionField: any;
dataIndex: string[];
operators: any[];
operator: any;
schema: any;
value: any;
setDataIndex: (dataIndex: string[]) => void;
setOperator: (operatorValue: string) => void;
setValue: (value: any) => void;
}
// import { useValues } from './useValues';
const findOption = (dataIndex = [], options) => {
@ -21,21 +34,27 @@ const findOption = (dataIndex = [], options) => {
return option;
};
export const useValues = () => {
const field = useField<any>();
export const useValues = (): UseValuesReturn => {
const { name } = useCollection();
const { getCollectionJoinField } = useCollectionManager();
const ctx: any = useContext(FilterContext) || {};
const { options } = ctx;
const data2value = () => {
const field = useField<any>();
const { options, collectionName, field: ctxField } = useContext(FilterContext) || {};
const values: object = flat(field.value || {});
const path = Object.keys(values).shift() || '';
const collectionField = useMemo(() => {
const [fieldPath = ''] = path.split('.$');
return getCollectionJoinField(`${collectionName || name}.${fieldPath}`);
}, [name, path]);
const data2value = useCallback(() => {
field.value = flat.unflatten({
[`${field.data.dataIndex?.join('.')}.${field.data?.operator?.value}`]: field.data?.value,
});
};
}, [field]);
const value2data = () => {
field.data = field.data || {};
const values: object = flat(field.value || {});
const path = Object.keys(values).shift() || '';
if (!path || !options) {
return;
}
@ -50,20 +69,20 @@ export const useValues = () => {
const fieldNames = dataIndex.concat();
fieldNames.pop();
const targetField = getCollectionJoinField(`${name}.${fieldNames.join('.')}`);
ctx.field.collectionName = targetField?.target;
ctxField.collectionName = targetField?.target;
} else {
ctx.field.collectionName = null;
ctxField.collectionName = null;
}
field.data.operators = operators;
field.data.operator = operator;
field.data.schema = merge(option?.schema, operator?.schema);
field.data.value = get(unflatten(field.value), `${fieldPath}.$${operatorValue}`);
};
useEffect(value2data, [field.path]);
return {
fields: options,
...(field?.data || {}),
setDataIndex(dataIndex) {
const setDataIndex = useCallback(
(dataIndex) => {
const option = findOption(dataIndex, options);
const operator = option?.operators?.[0];
field.data = field.data || {};
@ -77,14 +96,18 @@ export const useValues = () => {
const fieldNames = dataIndex.concat();
fieldNames.pop();
const targetField = getCollectionJoinField(`${name}.${fieldNames.join('.')}`);
ctx.field.collectionName = targetField?.target;
ctxField.collectionName = targetField?.target;
} else {
ctx.field.collectionName = null;
ctxField.collectionName = null;
}
field.data.value = operator?.noValue ? operator.default || true : undefined;
data2value();
},
setOperator(operatorValue) {
[data2value, field, options],
);
const setOperator = useCallback(
(operatorValue) => {
const operator = field.data?.operators?.find?.((item) => item.value === operatorValue);
field.data.operator = operator;
const option = findOption(field.data.dataIndex, options);
@ -94,9 +117,23 @@ export const useValues = () => {
field.data.value = operator.noValue ? operator.default || true : undefined;
data2value();
},
setValue(value) {
[data2value, field.data, options],
);
const setValue = useCallback(
(value) => {
field.data.value = value;
data2value();
},
[data2value, field.data],
);
return {
fields: options,
...(field?.data || {}),
collectionField,
setDataIndex,
setOperator,
setValue,
};
};

View File

@ -3,130 +3,70 @@ import { ArrayCollapse, FormLayout, FormItem as Item } from '@formily/antd-v5';
import { Field } from '@formily/core';
import { ISchema, observer, useField, useFieldSchema } from '@formily/react';
import { Select } from 'antd';
import dayjs from 'dayjs';
import _ from 'lodash';
import React, { useEffect } from 'react';
import React, { useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { ACLCollectionFieldProvider } from '../../../acl/ACLProvider';
import { useBlockRequestContext } from '../../../block-provider/BlockProvider';
import { useFormActiveFields } from '../../../block-provider';
import { useFormBlockContext } from '../../../block-provider/FormBlockProvider';
import {
Collection,
CollectionFieldOptions,
useCollection,
useCollectionFilterOptions,
useCollectionManager,
} from '../../../collection-manager';
import { Collection, useCollection, useCollectionManager } from '../../../collection-manager';
import { useRecord } from '../../../record-provider';
import { GeneralSchemaItems } from '../../../schema-items/GeneralSchemaItems';
import { GeneralSchemaDesigner, SchemaSettings, isPatternDisabled, isShowDefaultValue } from '../../../schema-settings';
import { GeneralSchemaDesigner, SchemaSettings, isPatternDisabled } from '../../../schema-settings';
import { VariableInput, getShouldChange } from '../../../schema-settings/VariableInput/VariableInput';
import useIsAllowToSetDefaultValue from '../../../schema-settings/hooks/useIsAllowToSetDefaultValue';
import { useIsShowMultipleSwitch } from '../../../schema-settings/hooks/useIsShowMultipleSwitch';
import { isVariable, parseVariables, useVariablesCtx } from '../../common/utils/uitls';
import { useLocalVariables, useVariables } from '../../../variables';
import useContextVariable from '../../../variables/hooks/useContextVariable';
import { useCompile, useDesignable, useFieldModeOptions } from '../../hooks';
import { isSubMode } from '../association-field/util';
import { BlockItem } from '../block-item';
import { removeNullCondition } from '../filter';
import { DynamicComponentProps } from '../filter/DynamicComponent';
import { HTMLEncode } from '../input/shared';
import { FilterDynamicComponent } from '../table-v2/FilterDynamicComponent';
import { useColorFields } from '../table-v2/Table.Column.Designer';
import { FilterFormDesigner } from './FormItem.FilterFormDesigner';
import { useEnsureOperatorsValid } from './SchemaSettingOptions';
import useLazyLoadAssociationFieldOfForm from './hooks/useLazyLoadAssociationFieldOfForm';
import useLazyLoadAssociationFieldOfSubForm from './hooks/useLazyLoadAssociationFieldOfSubForm';
import useParseDefaultValue from './hooks/useParseDefaultValue';
export const findColumnFieldSchema = (fieldSchema, getCollectionJoinField) => {
const childsSchema = new Set();
const getAssociationAppends = (schema) => {
schema.reduceProperties((_, s) => {
const collectionfield = s['x-collection-field'] && getCollectionJoinField(s['x-collection-field']);
const isAssociationField = collectionfield && ['belongsTo'].includes(collectionfield.type);
if (collectionfield && isAssociationField && s.default?.includes?.('$context')) {
childsSchema.add(JSON.stringify({ name: s.name, default: s.default }));
} else {
getAssociationAppends(s);
}
}, []);
};
getAssociationAppends(fieldSchema);
return [...childsSchema];
};
function transformData(inputData) {
const transformedData = [];
const keys = Object.keys(inputData) || [];
const values: any[] = Object.values(inputData) || [];
for (let i = 0; i < values[0]?.length; i++) {
const newObj = {};
keys.forEach((key, index) => {
newObj[key] = values[index][i];
});
transformedData.push(newObj);
}
return transformedData;
}
export const FormItem: any = observer(
(props: any) => {
useEnsureOperatorsValid();
const field = useField<Field>();
const ctx = useBlockRequestContext();
const schema = useFieldSchema();
const variablesCtx = useVariablesCtx();
const { getCollectionJoinField } = useCollectionManager();
const collectionField = getCollectionJoinField(schema['x-collection-field']);
const contextVariable = useContextVariable();
const variables = useVariables();
const { addActiveFieldName } = useFormActiveFields() || {};
useEffect(() => {
if (ctx?.block === 'form') {
ctx.field.data = ctx.field.data || {};
ctx.field.data.activeFields = ctx.field.data.activeFields || new Set();
ctx.field.data.activeFields.add(schema.name);
// 如果默认值是一个变量,则需要解析之后再显示出来
if (isVariable(schema?.default) && !schema?.default.includes('$context')) {
field.setInitialValue?.(parseVariables(schema.default, variablesCtx));
} else if (
isVariable(schema?.default) &&
schema?.default?.includes('$context') &&
collectionField?.interface === 'm2m'
) {
// 直接对多
const contextData = parseVariables('{{$context}}', variablesCtx);
let iniValues = [];
contextData?.map((v) => {
const data = parseVariables(schema.default, { $context: v });
iniValues = iniValues.concat(data).map((v) => {
delete v[collectionField.through];
return v;
});
});
const data = _.uniqBy(iniValues, 'id');
field.setInitialValue?.(data.length > 0 ? data : [{}]);
} else if (
collectionField?.interface === 'o2m' &&
['SubTable', 'Nester'].includes(schema?.['x-component-props']?.['mode']) // 间接对多
) {
const childrenFieldWithDefault = findColumnFieldSchema(schema, getCollectionJoinField);
// 子表格/子表单中找出所有belongsTo字段的上下文默认值
if (childrenFieldWithDefault.length > 0) {
const tableData = parseVariables('{{$context}}', variablesCtx);
const contextData = {};
// 将数据拍平
childrenFieldWithDefault?.forEach((s: any) => {
const child = JSON.parse(s);
tableData?.map((v) => {
contextData[child.name] = _.uniqBy(
(contextData[child.name] || []).concat(parseVariables(child.default, { $context: v })),
'id',
);
});
});
const initValues = transformData(contextData);
field.setInitialValue?.(initValues);
}
}
}
}, []);
variables?.registerVariable(contextVariable);
}, [contextVariable]);
// 需要放在注冊完变量之后
useParseDefaultValue();
useLazyLoadAssociationFieldOfForm();
useLazyLoadAssociationFieldOfSubForm();
useEffect(() => {
addActiveFieldName?.(schema.name as string);
}, [addActiveFieldName, schema.name]);
const showTitle = schema['x-decorator-props']?.showTitle ?? true;
return (
<ACLCollectionFieldProvider>
<BlockItem className={'nb-form-item'}>
<Item
className={cx(
const extra = useMemo(() => {
return typeof field.description === 'string' ? (
<div
dangerouslySetInnerHTML={{
__html: HTMLEncode(field.description).split('\n').join('<br/>'),
}}
/>
) : (
field.description
);
}, [field.description]);
const className = useMemo(() => {
return cx(
css`
& .ant-space {
flex-wrap: wrap;
@ -139,20 +79,13 @@ export const FormItem: any = observer(
}
`]: showTitle === false,
},
)}
{...props}
extra={
typeof field.description === 'string' ? (
<div
dangerouslySetInnerHTML={{
__html: HTMLEncode(field.description).split('\n').join('<br/>'),
}}
/>
) : (
field.description
)
}
/>
);
}, [showTitle]);
return (
<ACLCollectionFieldProvider>
<BlockItem className={'nb-form-item'}>
<Item className={className} {...props} extra={extra} />
</BlockItem>
</ACLCollectionFieldProvider>
);
@ -161,12 +94,17 @@ export const FormItem: any = observer(
);
FormItem.Designer = function Designer() {
let targetField;
const { getCollectionFields, getInterface, getCollectionJoinField, getCollection, isTitleField } =
useCollectionManager();
const {
getCollectionFields,
getInterface,
getCollectionJoinField,
getCollection,
isTitleField,
getAllCollectionsInheritChain,
} = useCollectionManager();
const { getField } = useCollection();
const { form } = useFormBlockContext();
const ctx = useBlockRequestContext();
const record = useRecord();
const field = useField<Field>();
const fieldSchema = useFieldSchema();
const { t } = useTranslation();
@ -174,11 +112,10 @@ FormItem.Designer = function Designer() {
const compile = useCompile();
const IsShowMultipleSwitch = useIsShowMultipleSwitch();
const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']);
if (collectionField?.target) {
targetField = getCollectionJoinField(
`${collectionField.target}.${fieldSchema['x-component-props']?.fieldNames?.label || 'id'}`,
);
}
const variables = useVariables();
const localVariables = useLocalVariables();
const { isAllowToSetDefaultValue } = useIsAllowToSetDefaultValue();
const isAddNewForm = useIsAddNewForm();
const targetCollection = getCollection(collectionField?.target);
const interfaceConfig = getInterface(collectionField?.interface);
@ -190,7 +127,7 @@ FormItem.Designer = function Designer() {
const fieldModeOptions = useFieldModeOptions();
const isAssociationField = ['obo', 'oho', 'o2o', 'o2m', 'm2m', 'm2o'].includes(collectionField?.interface);
const isTableField = fieldSchema['x-component'] === 'TableField';
const isFileField = isFileCollection(targetCollection);
const isFileField = isFileCollection(targetCollection as any);
const initialValue = {
title: field.title === originalTitle ? undefined : field.title,
};
@ -214,8 +151,6 @@ FormItem.Designer = function Designer() {
if (fieldSchema['x-read-pretty'] === true) {
readOnlyMode = 'read-pretty';
}
const dataSource = useCollectionFilterOptions(collectionField?.target);
const defaultFilter = fieldSchema?.['x-component-props']?.service?.params?.filter || {};
const fieldMode = field?.componentProps?.['mode'] || (isFileField ? 'FileManager' : 'Select');
const isSelectFieldMode = isAssociationField && fieldMode === 'Select';
const isSubFormMode = fieldSchema['x-component-props']?.mode === 'Nester';
@ -365,10 +300,6 @@ FormItem.Designer = function Designer() {
const schema = {
['x-uid']: fieldSchema['x-uid'],
};
// return;
// if (['number'].includes(collectionField?.interface) && collectionField?.uiSchema?.['x-component-props']?.['stringMode'] === true) {
// rules['numberStringMode'] = true;
// }
if (['percent'].includes(collectionField?.interface)) {
for (const rule of rules) {
if (!!rule.maxValue || !!rule.minValue) {
@ -391,41 +322,33 @@ FormItem.Designer = function Designer() {
}}
/>
)}
{form &&
!form?.readPretty &&
isShowDefaultValue(collectionField, getInterface) &&
!isPatternDisabled(fieldSchema) && <SchemaSettings.DefaultValue />}
{isAllowToSetDefaultValue() && <SchemaSettings.DefaultValue />}
{isSelectFieldMode && !field.readPretty && (
<SchemaSettings.ModalItem
title={t('Set the data scope')}
schema={
{
type: 'object',
title: t('Set the data scope'),
properties: {
filter: {
defaultValue: defaultFilter,
enum: dataSource,
'x-component': 'Filter',
'x-component-props': {
collectionName: collectionField?.target,
dynamicComponent: (props) =>
FilterDynamicComponent({
...props,
form,
collectionField,
rootCollection: ctx.props.collection || ctx.props.resource,
}),
},
},
},
} as ISchema
}
<SchemaSettings.DataScope
collectionName={collectionField?.target}
defaultFilter={fieldSchema?.['x-component-props']?.service?.params?.filter || {}}
form={form}
dynamicComponent={(props: DynamicComponentProps) => {
return (
<VariableInput
{...props}
form={form}
collectionField={props.collectionField}
record={record}
shouldChange={getShouldChange({
collectionField: props.collectionField,
variables,
localVariables,
getAllCollectionsInheritChain,
})}
/>
);
}}
onSubmit={({ filter }) => {
filter = removeNullCondition(filter);
_.set(field.componentProps, 'service.params.filter', filter);
fieldSchema['x-component-props'] = field.componentProps;
dn.emit('patch', {
void dn.emit('patch', {
schema: {
['x-uid']: fieldSchema['x-uid'],
'x-component-props': field.componentProps,
@ -450,7 +373,17 @@ FormItem.Designer = function Designer() {
schema['x-component-props'] = fieldSchema['x-component-props'];
field.componentProps = field.componentProps || {};
field.componentProps.mode = mode;
dn.emit('patch', {
// 子表单状态不允许设置默认值
if (isSubMode(fieldSchema) && isAddNewForm) {
// @ts-ignore
schema.default = null;
fieldSchema.default = null;
field.setInitialValue(null);
field.setValue(null);
}
void dn.emit('patch', {
schema,
});
dn.refresh();
@ -800,10 +733,9 @@ export function isFileCollection(collection: Collection) {
FormItem.FilterFormDesigner = FilterFormDesigner;
export function getFieldDefaultValue(fieldSchema: ISchema, collectionField: CollectionFieldOptions) {
const result = fieldSchema?.default ?? collectionField?.defaultValue;
if (collectionField?.uiSchema?.['x-component'] === 'DatePicker' && result) {
return dayjs(result);
}
return result;
function useIsAddNewForm() {
const record = useRecord();
const isAddNewForm = _.isEmpty(_.omit(record, '__parent'));
return isAddNewForm;
}

View File

@ -382,7 +382,7 @@ export const EditComponent = () => {
const fieldModeOptions = useFieldModeOptions();
const isAssociationField = ['belongsTo', 'hasOne', 'hasMany', 'belongsToMany'].includes(collectionField?.type);
const targetCollection = getCollection(collectionField?.target);
const isFileField = isFileCollection(targetCollection);
const isFileField = isFileCollection(targetCollection as any);
return isAssociationField && fieldModeOptions ? (
<SchemaSettings.SelectItem

View File

@ -0,0 +1,33 @@
import { transformValue } from '../../hooks/useSpecialCase';
describe('transformValue', () => {
it('value is an array', () => {
const value = [{ a: 3 }, { a: 4 }];
const deps = {
field: {
value: [{ a: 1, b: 11 }],
},
subFieldSchema: {
name: 'a',
},
};
expect(transformValue(value, deps as any)).toMatchInlineSnapshot(`
[
{
"a": {
"a": 3,
},
"b": 11,
},
{
"__notFromDatabase": true,
"a": {
"a": 4,
},
"b": 11,
},
]
`);
});
});

View File

@ -0,0 +1,50 @@
import { Field } from '@formily/core';
import { useField, useFieldSchema, useForm } from '@formily/react';
import { useEffect } from 'react';
import { useCollection, useCollectionManager } from '../../../../collection-manager';
import { useVariables } from '../../../../variables';
import { transformVariableValue } from '../../../../variables/utils/transformVariableValue';
/**
* Form
*/
const useLazyLoadAssociationFieldOfForm = () => {
const { name } = useCollection();
const { getCollectionJoinField } = useCollectionManager();
const form = useForm();
const fieldSchema = useFieldSchema();
const variables = useVariables();
const field = useField<Field>();
const schemaName = fieldSchema.name.toString();
useEffect(() => {
// 在数据表管理页面,也存在 `a.b` 之类的 schema name其 collectionName 为 fields所以在这里排除一下 `name === 'fields'` 的情况
if (!schemaName.includes('.') || !variables || name === 'fields') {
return;
}
const formVariable = {
name: '$nForm',
ctx: form.values,
collectionName: name,
};
const variableString = `{{ $nForm.${schemaName} }}`;
const collectionField = getCollectionJoinField(`${name}.${schemaName}`);
if (!collectionField) {
return console.error(new Error(`useLazyLoadAssociationField: ${schemaName} not found in collection ${name}`));
}
variables
.parseVariable(variableString, formVariable)
.then((value) => {
field.value = transformVariableValue(value, { targetCollectionField: collectionField });
})
.catch((err) => {
console.error(err);
});
}, [schemaName.includes('.') ? form.values[schemaName.split('.')[0]] : null]);
};
export default useLazyLoadAssociationFieldOfForm;

View File

@ -0,0 +1,62 @@
import { Field } from '@formily/core';
import { useField, useFieldSchema } from '@formily/react';
import _ from 'lodash';
import { useEffect } from 'react';
import { useRecord } from '../../../..';
import { useCollection, useCollectionManager } from '../../../../collection-manager';
import { useFlag } from '../../../../flag-provider';
import { useVariables } from '../../../../variables';
import { transformVariableValue } from '../../../../variables/utils/transformVariableValue';
/**
* subForm
*/
const useLazyLoadAssociationFieldOfSubForm = () => {
const { name } = useCollection();
const { getCollectionJoinField } = useCollectionManager();
const record = useRecord();
const fieldSchema = useFieldSchema();
const variables = useVariables();
const field = useField<Field>();
const { isInAssignFieldValues } = useFlag() || {};
const schemaName = fieldSchema.name.toString();
useEffect(() => {
if (isInAssignFieldValues) {
return;
}
const collectionField = getCollectionJoinField(`${name}.${schemaName}`);
if (!collectionField) {
return;
}
const cloneRecord = { ...record };
delete cloneRecord['__parent'];
if (_.isEmpty(cloneRecord) || !variables || record[schemaName] != null) {
return;
}
// 通过模拟一个 record 变量,用解析变量的方法去获取关系字段的值
const recordVariable = {
name: '$nRecord',
ctx: cloneRecord,
collectionName: name,
};
const variableString = `{{ $nRecord.${schemaName} }}`;
variables
.parseVariable(variableString, recordVariable)
.then((value) => {
field.value = transformVariableValue(value, { targetCollectionField: collectionField });
})
.catch((err) => {
console.error(err);
});
}, [field, name, record, schemaName, isInAssignFieldValues]);
};
export default useLazyLoadAssociationFieldOfSubForm;

View File

@ -0,0 +1,112 @@
import { Field } from '@formily/core';
import { useField, useFieldSchema } from '@formily/react';
import { reaction } from '@formily/reactive';
import _ from 'lodash';
import { useEffect, useRef } from 'react';
import { useRecord, useRecordIndex } from '../../../../../src/record-provider';
import { useCollection } from '../../../../collection-manager';
import { useFlag } from '../../../../flag-provider';
import { DEBOUNCE_WAIT, useLocalVariables, useVariables } from '../../../../variables';
import { getPath } from '../../../../variables/utils/getPath';
import { getVariableName } from '../../../../variables/utils/getVariableName';
import { isVariable } from '../../../../variables/utils/isVariable';
import { transformVariableValue } from '../../../../variables/utils/transformVariableValue';
import { isSubMode } from '../../association-field/util';
import { isFromDatabase, useSpecialCase } from './useSpecialCase';
/**
* FormItem
*/
const useParseDefaultValue = () => {
const field = useField<Field>();
const fieldSchema = useFieldSchema();
const variables = useVariables();
const localVariables = useLocalVariables();
const record = useRecord();
const { isInAssignFieldValues, isInSetDefaultValueDialog, isInFormDataTemplate } = useFlag() || {};
const { getField } = useCollection();
const { isSpecialCase, setDefaultValue } = useSpecialCase();
const index = useRecordIndex();
// 需要保存 record 的初始值,因为设置默认值的时候 record 会被修改,导致初始值丢失
const recordRef = useRef(_.omit(record, '__parent'));
useEffect(() => {
if (
fieldSchema.default == null ||
isInSetDefaultValueDialog ||
isInFormDataTemplate ||
isSubMode(fieldSchema) ||
// 根据 record 是否为空,判断当前是否是新建状态,编辑状态下不需要设置默认值,否则会覆盖用户输入的值,只有新建状态下才需要设置默认值
(!_.isEmpty(recordRef.current) && isFromDatabase(record) && !isInAssignFieldValues)
) {
return;
}
const _run = async () => {
// 如果默认值是一个变量,则需要解析之后再显示出来
if (isVariable(fieldSchema.default) && variables && field) {
// 一个变量字符串如果显示出来会比较奇怪
if (isVariable(field.value)) {
field.setValue(null);
}
field.loading = true;
const collectionField = !fieldSchema.name.toString().includes('.') && getField(fieldSchema.name);
if (process.env.NODE_ENV !== 'production') {
if (!collectionField) {
throw new Error(`useParseDefaultValue: can not find field ${fieldSchema.name}`);
}
}
const value = transformVariableValue(await variables.parseVariable(fieldSchema.default, localVariables), {
targetCollectionField: collectionField,
});
if (value == null || value === '') {
field.setValue(null);
} else if (isSpecialCase()) {
// 只需要设置一次就可以了
if (index === 0) {
setDefaultValue(value);
}
} else {
// TODO: 可能会因为异步时序问题导致设置默认值无效
// eslint-disable-next-line promise/catch-or-return
void Promise.resolve().then(() => {
field.setInitialValue(value);
});
}
field.loading = false;
// 如果不是一个有效的变量字符串(如:`{{ $user.name }}`)却依然包含 `{{` 和 `}}`
// 则可以断定是一个需要 compile`useCompile` 返回的函数) 的字符串,这样的字符串会由 formily 自动解析,无需在这里赋值
} else if (!/\{\{.+\}\}/g.test(fieldSchema.default) && field.setInitialValue) {
field.setInitialValue(fieldSchema.default);
}
};
// 使用防抖,提高性能和用户体验
const run = _.debounce(_run, DEBOUNCE_WAIT);
void _run();
if (isVariable(fieldSchema.default)) {
const variableName = getVariableName(fieldSchema.default);
const variableValue = localVariables.find((item) => item.name === variableName);
if (variableValue) {
// 实现联动的效果,当依赖的变量变化时(如 `$nForm` 变量),重新解析默认值
const dispose = reaction(() => {
return _.get({ [variableName]: variableValue?.ctx }, getPath(fieldSchema.default));
}, run);
return dispose;
}
}
}, [fieldSchema.default]);
};
export default useParseDefaultValue;

View File

@ -0,0 +1,174 @@
import { Field } from '@formily/core';
import { Schema, useFieldSchema, useForm } from '@formily/react';
import _ from 'lodash';
import { useCallback, useEffect, useMemo } from 'react';
import { CollectionFieldOptions, useCollection, useCollectionManager } from '../../../../collection-manager';
import { isSubMode } from '../../association-field/util';
/**
* #### `子表单` `子表格`
*
* -
* -
*/
export const useSpecialCase = () => {
const form = useForm();
const fieldSchema = useFieldSchema();
const { getField } = useCollection();
const { getCollectionField } = useCollectionManager();
const collectionField = useMemo(() => {
return getField(fieldSchema.name);
}, [fieldSchema.name, getField]);
const isSpecialCase = useCallback(() => {
return isSpecialCaseField({ collectionField, fieldSchema, getCollectionField });
}, [collectionField, fieldSchema, getCollectionField]);
const setDefaultValue = useCallback(
(value: any) => {
const parentFieldSchema = getParentFieldSchema(fieldSchema);
if (parentFieldSchema) {
const parentField: any = form.query(parentFieldSchema.name).take();
if (parentField) {
parentField.setInitialValue(transformValue(value, { field: parentField, subFieldSchema: fieldSchema }));
}
}
},
[fieldSchema, form],
);
return {
/**
* `对一` `对多` `子表格` `子表单`
* https://nocobase.feishu.cn/docx/EmNEdEBOnoQohUx2UmBcqIQ5nyh#CUdLdy6OpoPKjyx9DLPc3lqknVc
*/
isSpecialCase,
setDefaultValue,
};
};
/**
* `子表格` `子表单` fieldSchema
* @param fieldSchema
*/
export function getParentFieldSchema(fieldSchema: Schema) {
while (fieldSchema?.parent) {
fieldSchema = fieldSchema.parent;
if (isSubMode(fieldSchema)) {
return fieldSchema;
}
}
return fieldSchema;
}
/**
* `对一` `对多` `子表格` `子表单`
* https://nocobase.feishu.cn/docx/EmNEdEBOnoQohUx2UmBcqIQ5nyh#CUdLdy6OpoPKjyx9DLPc3lqknVc
* @returns
*/
export function isSpecialCaseField({
collectionField,
fieldSchema,
getCollectionField,
}: {
collectionField: CollectionFieldOptions;
fieldSchema: Schema;
getCollectionField: (name: string) => CollectionFieldOptions;
}) {
if (collectionField && ['hasOne', 'belongsTo'].includes(collectionField.type) && fieldSchema) {
const parentFieldSchema = getParentFieldSchema(fieldSchema);
if (parentFieldSchema && parentFieldSchema['x-collection-field']) {
const parentCollectionField = getCollectionField(parentFieldSchema['x-collection-field']);
if (['hasMany', 'belongsToMany'].includes(parentCollectionField?.type)) {
return true;
}
}
}
return false;
}
/**
* `对多` `对一` `对多`
* @param value `子表单` `子表格` `对一`
* @param deps
* @returns
*/
export function transformValue(
value: any,
deps: {
field: Field;
subFieldSchema: Schema;
},
) {
const { field, subFieldSchema } = deps;
const oldValues = field.value;
let newValues = null;
if (Array.isArray(value)) {
newValues = value.map((item) => {
return {
[subFieldSchema.name]: item,
};
});
} else {
newValues = [
{
[subFieldSchema.name]: value,
},
];
}
const result = _.assignWith([...oldValues], newValues, (oldValue, newValue) => {
if (!oldValue && newValue) {
// 用于标记这个值不是从数据库中获取的
newValue.__notFromDatabase = true;
}
// 之所以取第一个值,是因为 `对一` 字段的默认值可能是异步的,导致会赋默认值值会晚于普通字段,导致普通字段的值会被覆盖
return _.assign({ ...oldValues[0] }, newValue);
});
return result;
}
/**
* record true false
* @param value useRecord
* @returns boolean
*/
export function isFromDatabase(value: Record<string, any>) {
if (!value) {
return false;
}
return !value.__notFromDatabase;
}
/**
* `子表格` /
*
* FormItem FormItem
*
*
* 1. FormItem
* 2. FormItem
* 3.
* @param param0
*/
export const useSubTableSpecialCase = ({ field }) => {
useEffect(() => {
if (_.isEmpty(field.value)) {
field.value = [{}];
// 因为默认值的解析是异步的,所以下面的代码会优先于默认值的设置,这样就防止了设置完默认值后又被清空的问题
Promise.resolve()
.then(() => {
field.value = [];
})
.catch(console.error);
}
}, []);
};

View File

@ -5,12 +5,11 @@ import { useTranslation } from 'react-i18next';
import { useFormBlockContext } from '../../../block-provider';
import { useDetailsBlockContext } from '../../../block-provider/DetailsBlockProvider';
import { useCollection } from '../../../collection-manager';
import { useCollectionFilterOptions, useSortFields } from '../../../collection-manager/action-hooks';
import { useSortFields } from '../../../collection-manager/action-hooks';
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
import { useSchemaTemplate } from '../../../schema-templates';
import { useDesignable } from '../../hooks';
import { removeNullCondition } from '../filter';
import { FilterDynamicComponent } from '../table-v2/FilterDynamicComponent';
export const FormDesigner = () => {
const { name, title } = useCollection();
@ -74,12 +73,11 @@ export const DetailsDesigner = () => {
const template = useSchemaTemplate();
const { t } = useTranslation();
const fieldSchema = useFieldSchema();
const { form } = useFormBlockContext();
const field = useField();
const dataSource = useCollectionFilterOptions(name);
const { service } = useDetailsBlockContext();
const { dn } = useDesignable();
const sortFields = useSortFields(name);
const defaultFilter = fieldSchema?.['x-decorator-props']?.params?.filter || {};
const defaultSort = fieldSchema?.['x-decorator-props']?.params?.sort || [];
const defaultResource = fieldSchema?.['x-decorator-props']?.resource;
const sort = defaultSort?.map((item: string) => {
@ -96,25 +94,10 @@ export const DetailsDesigner = () => {
return (
<GeneralSchemaDesigner template={template} title={title || name}>
<SchemaSettings.BlockTitleItem />
<SchemaSettings.ModalItem
title={t('Set the data scope')}
schema={
{
type: 'object',
title: t('Set the data scope'),
properties: {
filter: {
default: defaultFilter,
// title: '数据范围',
enum: dataSource,
'x-component': 'Filter',
'x-component-props': {
dynamicComponent: (props) => FilterDynamicComponent({ ...props }),
},
},
},
} as ISchema
}
<SchemaSettings.DataScope
collectionName={name}
defaultFilter={fieldSchema?.['x-decorator-props']?.params?.filter || {}}
form={form}
onSubmit={({ filter }) => {
filter = removeNullCondition(filter);
const params = field.decoratorProps.params || {};

View File

@ -1,12 +1,14 @@
import { css } from '@emotion/css';
import { FormLayout } from '@formily/antd-v5';
import { createForm, Field, onFieldChange, onFieldInit, onFieldReact, onFormInputChange } from '@formily/core';
import { createForm, Field, Form as FormilyForm, onFieldInit, onFormInputChange } from '@formily/core';
import { FieldContext, FormContext, observer, RecursionField, useField, useFieldSchema } from '@formily/react';
import { autorun } from '@formily/reactive';
import { uid } from '@formily/shared';
import { ConfigProvider, Spin } from 'antd';
import React, { useEffect, useMemo } from 'react';
import { useActionContext } from '..';
import { useAttach, useComponent } from '../..';
import { useLocalVariables, useVariables } from '../../../variables';
import { useProps } from '../../hooks/useProps';
import { linkageMergeAction } from './utils';
@ -66,81 +68,91 @@ const getLinkageRules = (fieldSchema) => {
return linkageRules;
};
const WithForm = (props) => {
interface WithFormProps {
form: FormilyForm;
disabled?: boolean;
}
const WithForm = (props: WithFormProps) => {
const { form } = props;
const fieldSchema = useFieldSchema();
const { setFormValueChanged } = useActionContext();
const linkageRules =
const variables = useVariables();
const localVariables = useLocalVariables({ currentForm: form });
const linkageRules: any[] =
(getLinkageRules(fieldSchema) || fieldSchema.parent?.['x-linkage-rules'])?.filter((k) => !k.disabled) || [];
useEffect(() => {
const id = uid();
form.addEffects(id, () => {
onFormInputChange((form) => {
onFormInputChange(() => {
setFormValueChanged?.(true);
});
});
if (props.disabled) {
form.disabled = props.disabled;
}
return () => {
form.removeEffects(id);
};
}, []);
}, [props.disabled]);
useEffect(() => {
const id = uid();
const disposes = [];
form.addEffects(id, () => {
return linkageRules.map((v) => {
return v.actions?.map((h) => {
if (h.targetFields) {
linkageRules.forEach((v) => {
v.actions?.forEach((h) => {
if (h.targetFields?.length) {
const fields = h.targetFields.join(',');
onFieldInit(`*(${fields})`, (field: any, form) => {
field['initProperty'] = {
display: field.display,
required: field.required,
pattern: field.pattern,
value: field.value || field.initialValue,
};
});
onFieldChange(`*(${fields})`, ['value', 'required', 'pattern', 'display'], (field: any) => {
field.linkageProperty = {
display: field.linkageProperty?.display,
};
});
// 当 `linkageRules` 变更时,需要把 `field` 上之前已设置的值还原成初始值,以防止下面所述的情况:
//
// 1. 更改 `linkageRules`,使 a 字段满足条件时被隐藏
// 2. 设置表单,使其满足条件,进而隐藏 a 字段
// 3. 再次更改 `linkageRules`,使 a 字段满足条件时被禁用
// 4. 设置表单,使其满足条件,会发现 a 字段还是隐藏状态,这里期望的是只显示禁用状态
onFieldInit(`*(${fields})`, (field: any) => {
Object.keys(field).forEach((key) => {
if (key.startsWith('_') && field[key] !== undefined) {
field[key.slice(1)] = field[key];
}
});
});
// `onFieldReact` 有问题,没有办法被取消监听,所以这里用 `onFieldInit` 代替
onFieldInit(`*(${fields})`, (field: any, form) => {
disposes.push(
autorun(() => {
linkageMergeAction({
operator: h.operator,
value: h.value,
field,
condition: v.condition,
values: form?.values,
variables,
localVariables,
});
return () => {
form.removeEffects(id);
};
}, []);
useEffect(() => {
const id = uid();
const linkagefields = [];
form.addEffects(id, () => {
return linkageRules.map((v, index) => {
return v.actions?.map((h) => {
if (h.targetFields) {
const fields = h.targetFields.join(',');
return onFieldReact(`*(${fields})`, (field: any, form) => {
linkagefields.push(field);
linkageMergeAction(h, field, v.condition, form?.values);
if (index === linkageRules.length - 1) {
setTimeout(() =>
linkagefields.map((v) => {
v.linkageProperty = {};
}),
);
}
});
}
});
});
});
return () => {
form.removeEffects(id);
disposes.forEach((dispose) => {
dispose();
});
};
}, [linkageRules]);
}, [JSON.stringify(linkageRules)]);
return fieldSchema['x-decorator'] === 'FormV2' ? <FormDecorator {...props} /> : <FormComponent {...props} />;
};

View File

@ -1,83 +1,148 @@
import { Field } from '@formily/core';
import { evaluators } from '@nocobase/evaluators/client';
import { cloneDeep, last } from 'lodash';
import { uid } from '@nocobase/utils';
import _, { cloneDeep } from 'lodash';
import { ActionType } from '../../../schema-settings/LinkageRules/type';
import { conditionAnalyse } from '../../common/utils/uitls';
import { VariableOption, VariablesContextType } from '../../../variables/types';
import { REGEX_OF_VARIABLE } from '../../../variables/utils/isVariable';
import { conditionAnalyses } from '../../common/utils/uitls';
export const linkageMergeAction = ({ operator, value }, field, condition, values) => {
const requiredResult = field?.linkageProperty?.required || [field?.initProperty?.required || false];
const displayResult = field?.linkageProperty?.display || [field?.initProperty?.display];
const patternResult = field?.linkageProperty?.pattern || [field?.initProperty?.pattern];
const valueResult = field?.linkageProperty?.value || [field.value || field?.initProperty?.value];
interface Props {
operator;
value;
field: Field & {
[key: string]: any;
};
condition;
values;
variables: VariablesContextType;
localVariables: VariableOption[];
}
export const linkageMergeAction = async ({
operator,
value,
field,
condition,
values,
variables,
localVariables,
}: Props) => {
const { evaluate } = evaluators.get('formula.js');
switch (operator) {
case ActionType.Required:
if (conditionAnalyse(condition, values)) {
requiredResult.push(true);
if (await conditionAnalyses({ rules: condition, formValues: values, variables, localVariables })) {
field.required = true;
} else {
field.required = false;
}
field.linkageProperty = {
...field.linkageProperty,
required: requiredResult,
};
field.required = last(field.linkageProperty?.required);
break;
case ActionType.InRequired:
if (conditionAnalyse(condition, values)) {
requiredResult.push(false);
if (await conditionAnalyses({ rules: condition, formValues: values, variables, localVariables })) {
field.required = false;
} else {
field.required = true;
}
field.linkageProperty = {
...field.linkageProperty,
required: requiredResult,
};
field.required = last(field.linkageProperty?.required);
break;
case ActionType.Visible:
case ActionType.None:
case ActionType.Hidden:
if (conditionAnalyse(condition, values)) {
displayResult.push(operator);
if (await conditionAnalyses({ rules: condition, formValues: values, variables, localVariables })) {
field._display = '_display' in field ? field._display : field.display;
field.display = operator;
} else {
field.display = field._display;
}
field.linkageProperty = {
...field.linkageProperty,
display: displayResult,
};
field.display = last(field.linkageProperty?.display);
break;
case ActionType.Editable:
case ActionType.ReadOnly:
case ActionType.ReadPretty:
if (conditionAnalyse(condition, values)) {
patternResult.push(operator);
if (await conditionAnalyses({ rules: condition, formValues: values, variables, localVariables })) {
field._pattern = '_pattern' in field ? field._pattern : field.pattern;
field.pattern = operator;
} else {
field.pattern = field._pattern;
}
field.linkageProperty = {
...field.linkageProperty,
pattern: patternResult,
};
field.pattern = last(field.linkageProperty.pattern);
break;
case ActionType.Value:
if (conditionAnalyse(condition, values)) {
if (
isConditionEmpty(condition) ||
(await conditionAnalyses({ rules: condition, formValues: values, variables, localVariables }))
) {
let result = null;
if (value?.mode === 'express') {
const scope = cloneDeep(values);
// 1. 解析如 `{{$user.name}}` 之类的变量
const { exp, scope: expScope } = await replaceVariables(value.value || value.result, {
variables,
localVariables,
});
try {
const result = evaluate(value.result || value.value, { ...scope, now: () => new Date().toString() });
valueResult.push(result);
// 2. TODO: 需要把里面解析变量的逻辑删除,因为在上一步已经解析过了
result = evaluate(exp, { ...scope, now: () => new Date().toString(), ...expScope });
} catch (error) {
console.log(error);
if (process.env.NODE_ENV !== 'production') {
throw error;
}
}
} else if (value?.mode === 'constant') {
valueResult.push(value?.value || value);
result = value?.value || value;
} else {
valueResult.push(null);
result = null;
}
field._value = '_value' in field ? field._value : field.value;
field.value = result === undefined ? field.value : result;
} else {
field.value = field._value;
}
field.linkageProperty = {
...field.linkageProperty,
value: valueResult,
};
field.value = last(valueResult) === undefined ? field.value : last(valueResult);
break;
default:
return null;
}
};
async function replaceVariables(
value: string,
{
variables,
localVariables,
}: {
variables: VariablesContextType;
localVariables: VariableOption[];
},
) {
const store = {};
const scope = {};
const waitForParsing = value.match(REGEX_OF_VARIABLE)?.map(async (item) => {
const result = await variables.parseVariable(item, localVariables);
// 在开头加 `_` 是为了保证 id 不能以数字开头,否则在解析表达式的时候(不是解析变量)会报错
const id = `_${uid()}`;
scope[id] = result;
store[item] = id;
return result;
});
if (waitForParsing) {
await Promise.all(waitForParsing);
}
return {
exp: value.replace(REGEX_OF_VARIABLE, (match) => {
return store[match] || match;
}),
scope,
};
}
function isConditionEmpty(rules: { $and?: any; $or?: any }) {
const type = Object.keys(rules)[0] || '$and';
const conditions = rules[type];
return _.isEmpty(conditions);
}

View File

@ -13,18 +13,24 @@ describe('Form', () => {
it('basic', async () => {
render(<App2 />);
await sleep();
const submit = screen.getByText('Submit');
const input = document.querySelector('.ant-input') as HTMLInputElement;
await waitFor(() => {
expect(submit).toBeInTheDocument();
expect(input).toBeInTheDocument();
expect(screen.getByText(/form title/i)).toBeInTheDocument();
expect(screen.getByText('T1')).toBeInTheDocument();
expect(screen.getByText(/\{\}/i)).toBeInTheDocument();
expect(screen.queryByText(/form title/i)).toBeInTheDocument();
expect(screen.queryByText('T1')).toBeInTheDocument();
expect(screen.queryByText(/\{\}/i)).toBeInTheDocument();
});
// 使用 waitFor 防止报错参见https://github.com/testing-library/user-event/issues/662#issuecomment-904365493
await waitFor(() => userEvent.type(input, '123'));
expect(screen.getByText(/\{ "field1": "123" \}/i)).toBeInTheDocument();
await waitFor(() => {
expect(screen.queryByText(/\{ "field1": "123" \}/i)).toBeInTheDocument();
});
});
it('decorator', async () => {
@ -46,7 +52,9 @@ describe('Form', () => {
});
await userEvent.type(input, '123');
expect(screen.getByText(/\{ "field1": "aaa123" \}/i)).toBeInTheDocument();
await waitFor(() => {
expect(screen.queryByText(/\{ "field1": "aaa123" \}/i)).toBeInTheDocument();
});
});
it('Form & Drawer', async () => {

View File

@ -1,10 +1,9 @@
import { ISchema, useField, useFieldSchema } from '@formily/react';
import { useField, useFieldSchema } from '@formily/react';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useCompile, useDesignable } from '../..';
import { useGanttBlockContext } from '../../../block-provider';
import { useFormBlockContext, useGanttBlockContext } from '../../../block-provider';
import { useCollection } from '../../../collection-manager';
import { useCollectionFilterOptions } from '../../../collection-manager/action-hooks';
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
import { useSchemaTemplate } from '../../../schema-templates';
@ -25,14 +24,13 @@ const useOptions = (type = 'string') => {
export const GanttDesigner = () => {
const field = useField();
const fieldSchema = useFieldSchema();
const { name, title, fields } = useCollection();
const dataSource = useCollectionFilterOptions(name);
const { form } = useFormBlockContext();
const { name, title } = useCollection();
const { service } = useGanttBlockContext();
const { dn } = useDesignable();
const compile = useCompile();
const { t } = useTranslation();
const template = useSchemaTemplate();
const defaultFilter = fieldSchema?.['x-decorator-props']?.params?.filter || {};
const fieldNames = fieldSchema?.['x-decorator-props']?.['fieldNames'] || {};
const defaultResource = fieldSchema?.['x-decorator-props']?.resource;
return (
@ -146,28 +144,10 @@ export const GanttDesigner = () => {
dn.refresh();
}}
/>
<SchemaSettings.ModalItem
title={t('Set the data scope')}
schema={
{
type: 'object',
title: t('Set the data scope'),
properties: {
filter: {
default: defaultFilter,
enum: dataSource,
'x-component': 'Filter',
'x-component-props': {},
},
},
} as ISchema
}
initialValues={
{
// title: field.title,
// icon: field.componentProps.icon,
}
}
<SchemaSettings.DataScope
collectionName={name}
defaultFilter={fieldSchema?.['x-decorator-props']?.params?.filter || {}}
form={form}
onSubmit={({ filter }) => {
const params = field.decoratorProps.params || {};
params.filter = filter;

View File

@ -4,13 +4,13 @@ import { Slider } from 'antd';
import _ from 'lodash';
import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useCollection, useCollectionFilterOptions, useSortFields } from '../../../collection-manager';
import { useFormBlockContext } from '../../../block-provider';
import { useCollection, useSortFields } from '../../../collection-manager';
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
import { useSchemaTemplate } from '../../../schema-templates';
import { SchemaComponentOptions } from '../../core';
import { useDesignable } from '../../hooks';
import { removeNullCondition } from '../filter';
import { FilterDynamicComponent } from '../table-v2/FilterDynamicComponent';
import { defaultColumnCount, gridSizes, pageSizeOptions, screenSizeMaps, screenSizeTitleMaps } from './options';
const columnCountMarks = [1, 2, 3, 4, 6, 8, 12, 24].reduce((obj, cur) => {
@ -23,11 +23,10 @@ export const GridCardDesigner = () => {
const template = useSchemaTemplate();
const { t } = useTranslation();
const fieldSchema = useFieldSchema();
const { form } = useFormBlockContext();
const field = useField();
const dataSource = useCollectionFilterOptions(name);
const { dn } = useDesignable();
const sortFields = useSortFields(name);
const defaultFilter = fieldSchema?.['x-decorator-props']?.params?.filter || {};
const defaultSort = fieldSchema?.['x-decorator-props']?.params?.sort || [];
const defaultResource = fieldSchema?.['x-decorator-props']?.resource;
const columnCount = field.decoratorProps.columnCount || defaultColumnCount;
@ -94,25 +93,10 @@ export const GridCardDesigner = () => {
});
}}
/>
<SchemaSettings.ModalItem
title={t('Set the data scope')}
schema={
{
type: 'object',
title: t('Set the data scope'),
properties: {
filter: {
default: defaultFilter,
// title: '数据范围',
enum: dataSource,
'x-component': 'Filter',
'x-component-props': {
dynamicComponent: (props) => FilterDynamicComponent({ ...props }),
},
},
},
} as ISchema
}
<SchemaSettings.DataScope
collectionName={name}
defaultFilter={fieldSchema?.['x-decorator-props']?.params?.filter || {}}
form={form}
onSubmit={({ filter }) => {
filter = removeNullCondition(filter);
_.set(fieldSchema, 'x-decorator-props.params.filter', filter);

View File

@ -1,49 +1,30 @@
import { ISchema, useField, useFieldSchema } from '@formily/react';
import { useField, useFieldSchema } from '@formily/react';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useKanbanBlockContext } from '../../../block-provider';
import { useFormBlockContext, useKanbanBlockContext } from '../../../block-provider';
import { useCollection } from '../../../collection-manager';
import { useCollectionFilterOptions } from '../../../collection-manager/action-hooks';
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
import { useSchemaTemplate } from '../../../schema-templates';
import { useDesignable } from '../../hooks';
import { removeNullCondition } from '../filter';
import { FixedBlockDesignerItem } from '../page';
import { FilterDynamicComponent } from '../table-v2/FilterDynamicComponent';
export const KanbanDesigner = () => {
const { name, title } = useCollection();
const field = useField();
const fieldSchema = useFieldSchema();
const dataSource = useCollectionFilterOptions(name);
const { form } = useFormBlockContext();
const { service } = useKanbanBlockContext();
const { t } = useTranslation();
const { dn } = useDesignable();
const defaultFilter = fieldSchema?.['x-decorator-props']?.params?.filter || {};
const defaultResource = fieldSchema?.['x-decorator-props']?.resource;
const template = useSchemaTemplate();
return (
<GeneralSchemaDesigner template={template} title={title || name}>
<SchemaSettings.BlockTitleItem />
<SchemaSettings.ModalItem
title={t('Set the data scope')}
schema={
{
type: 'object',
title: t('Set the data scope'),
properties: {
filter: {
default: defaultFilter,
enum: dataSource,
'x-component': 'Filter',
'x-component-props': {
dynamicComponent: (props) => FilterDynamicComponent({ ...props }),
},
},
},
} as ISchema
}
<SchemaSettings.DataScope
collectionName={name}
defaultFilter={fieldSchema?.['x-decorator-props']?.params?.filter || {}}
form={form}
onSubmit={({ filter }) => {
filter = removeNullCondition(filter);
const params = field.decoratorProps.params || {};

View File

@ -3,25 +3,22 @@ import { ISchema, useField, useFieldSchema } from '@formily/react';
import _ from 'lodash';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useCollection, useCollectionFilterOptions, useSortFields } from '../../../collection-manager';
import { useFormBlockContext } from '../../../block-provider';
import { useCollection, useSortFields } from '../../../collection-manager';
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
import { useSchemaTemplate } from '../../../schema-templates';
import { useDesignable } from '../../hooks';
import { removeNullCondition } from '../filter';
import { FilterDynamicComponent } from '../table-v2/FilterDynamicComponent';
import { useListBlockContext } from './List.Decorator';
export const ListDesigner = () => {
const { name, title } = useCollection();
const template = useSchemaTemplate();
const { t } = useTranslation();
const fieldSchema = useFieldSchema();
const { form } = useFormBlockContext();
const field = useField();
const dataSource = useCollectionFilterOptions(name);
const { service } = useListBlockContext();
const { dn } = useDesignable();
const sortFields = useSortFields(name);
const defaultFilter = fieldSchema?.['x-decorator-props']?.params?.filter || {};
const defaultSort = fieldSchema?.['x-decorator-props']?.params?.sort || [];
const defaultResource = fieldSchema?.['x-decorator-props']?.resource;
const sort = defaultSort?.map((item: string) => {
@ -38,25 +35,10 @@ export const ListDesigner = () => {
return (
<GeneralSchemaDesigner template={template} title={title || name}>
<SchemaSettings.BlockTitleItem />
<SchemaSettings.ModalItem
title={t('Set the data scope')}
schema={
{
type: 'object',
title: t('Set the data scope'),
properties: {
filter: {
default: defaultFilter,
// title: '数据范围',
enum: dataSource,
'x-component': 'Filter',
'x-component-props': {
dynamicComponent: (props) => FilterDynamicComponent({ ...props }),
},
},
},
} as ISchema
}
<SchemaSettings.DataScope
collectionName={name}
defaultFilter={fieldSchema?.['x-decorator-props']?.params?.filter || {}}
form={form}
onSubmit={({ filter }) => {
filter = removeNullCondition(filter);
_.set(fieldSchema, 'x-decorator-props.params.filter', filter);

View File

@ -1,8 +1,8 @@
import { LoadingOutlined } from '@ant-design/icons';
import { connect, mapProps, mapReadPretty, useFieldSchema } from '@formily/react';
import dayjs from 'dayjs';
import { Divider, SelectProps, Tag } from 'antd';
import _, { uniqBy } from 'lodash';
import dayjs from 'dayjs';
import { uniqBy } from 'lodash';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { ResourceActionOptions, useRequest } from '../../../api-client';
import { mergeFilter } from '../../../block-provider/SharedFilterProvider';
@ -127,7 +127,7 @@ const InternalRemoteSelect = connect(
},
},
{
manual: manual && Object.prototype.toString.call(value) === '[object Object]',
manual,
debounceWait: wait,
},
);
@ -194,6 +194,7 @@ const InternalRemoteSelect = connect(
}
firstRun.current = true;
};
return (
<Select
open={open}

View File

@ -1,6 +1,7 @@
import { connect, mapReadPretty, mapProps } from '@formily/react';
import { connect, mapProps, mapReadPretty } from '@formily/react';
import React from 'react';
import ReactQuill from 'react-quill';
import { isVariable } from '../../../variables/utils/isVariable';
import { ReadPretty as InputReadPretty } from '../input';
import { useStyles } from './style';
@ -23,24 +24,22 @@ export const RichText = connect(
'link',
'image',
];
const { value, onChange, disabled } = props;
const { value, defaultValue, onChange, disabled } = props;
const resultValue = isVariable(value || defaultValue) ? undefined : value || defaultValue || '';
return wrapSSR(
<ReactQuill
className={`${componentCls} ${hashId}`}
modules={modules}
formats={formats}
value={typeof value === 'string' ? value : ''}
value={resultValue}
onChange={onChange}
readOnly={disabled}
/>,
);
},
mapProps((props) => {
const { value } = props;
return {
...props,
value: typeof value === 'object' ? value?.default : value,
};
mapProps({
initialValue: 'defaultValue',
}),
mapReadPretty((props) => {
return <InputReadPretty.Html {...props} />;

View File

@ -1,14 +0,0 @@
import React from 'react';
import { useVariableOptions } from '../../../schema-settings/VariableInput/hooks/useVariableOptions';
import { Variable } from '../variable';
export function FilterDynamicComponent(props) {
const { value, onChange, renderSchemaComponent, form, collectionField, ...other } = props;
const options = useVariableOptions({ form, collectionField, ...other });
return (
<Variable.Input value={value} onChange={onChange} scope={options}>
{renderSchemaComponent()}
</Variable.Input>
);
}

View File

@ -2,13 +2,13 @@ import { ISchema, useField, useFieldSchema } from '@formily/react';
import { set } from 'lodash';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { ArrayItems } from '@formily/antd-v5';
import { useCollectionFilterOptions, useCollectionManager } from '../../../collection-manager';
import { GeneralSchemaDesigner, SchemaSettings, isPatternDisabled, isShowDefaultValue } from '../../../schema-settings';
import { useFormBlockContext } from '../../../block-provider';
import { useCollectionManager } from '../../../collection-manager';
import { GeneralSchemaDesigner, SchemaSettings, isPatternDisabled } from '../../../schema-settings';
import useIsAllowToSetDefaultValue from '../../../schema-settings/hooks/useIsAllowToSetDefaultValue';
import { useCompile, useDesignable, useFieldModeOptions } from '../../hooks';
import { useAssociationFieldContext } from '../association-field/hooks';
import { removeNullCondition } from '../filter';
import { FilterDynamicComponent } from './FilterDynamicComponent';
const useLabelFields = (collectionName?: any) => {
// 需要在组件顶层调用
@ -46,6 +46,7 @@ export const useColorFields = (collectionName?: any) => {
};
export const TableColumnDesigner = (props) => {
const { uiSchema, fieldSchema, collectionField } = props;
const { form } = useFormBlockContext();
const { getInterface, getCollection } = useCollectionManager();
const field: any = useField();
const { t } = useTranslation();
@ -55,13 +56,13 @@ export const TableColumnDesigner = (props) => {
fieldSchema?.['x-component-props']?.['fieldNames'] || uiSchema?.['x-component-props']?.['fieldNames'];
const options = useLabelFields(collectionField?.target ?? collectionField?.targetCollection);
const colorFieldOptions = useColorFields(collectionField?.target ?? collectionField?.targetCollection);
const intefaceCfg = getInterface(collectionField?.interface);
const interfaceCfg = getInterface(collectionField?.interface);
const targetCollection = getCollection(collectionField?.target);
const isFileField = isFileCollection(targetCollection);
const isSubTableColumn = ['QuickEdit', 'FormItem'].includes(fieldSchema['x-decorator']);
const { currentMode } = useAssociationFieldContext();
const defaultFilter = fieldSchema?.['x-component-props']?.service?.params?.filter || {};
const dataSource = useCollectionFilterOptions(collectionField?.target);
const { isAllowToSetDefaultValue } = useIsAllowToSetDefaultValue({ collectionField, fieldSchema });
const isDateField = ['datetime', 'createdAt', 'updatedAt'].includes(collectionField?.interface);
const isAssociationField = ['obo', 'oho', 'o2o', 'o2m', 'm2m', 'm2o', 'snapshot'].includes(
collectionField?.interface,
@ -76,6 +77,7 @@ export const TableColumnDesigner = (props) => {
readOnlyMode = 'read-pretty';
}
const isSelectFieldMode = isAssociationField && fieldMode === 'Select';
return (
<GeneralSchemaDesigner disableInitializer>
<SchemaSettings.ModalItem
@ -111,24 +113,10 @@ export const TableColumnDesigner = (props) => {
}}
/>
{isSelectFieldMode && !field.readPretty && !uiSchema?.['x-read-pretty'] && isSubTableColumn && (
<SchemaSettings.ModalItem
title={t('Set the data scope')}
schema={
{
type: 'object',
title: t('Set the data scope'),
properties: {
filter: {
default: defaultFilter,
enum: dataSource,
'x-component': 'Filter',
'x-component-props': {
dynamicComponent: (props) => FilterDynamicComponent({ ...props }),
},
},
},
} as ISchema
}
<SchemaSettings.DataScope
collectionName={collectionField?.target}
defaultFilter={fieldSchema?.['x-component-props']?.service?.params?.filter || {}}
form={form}
onSubmit={({ filter }) => {
filter = removeNullCondition(filter);
set(field.componentProps, 'service.params.filter', filter);
@ -178,7 +166,7 @@ export const TableColumnDesigner = (props) => {
dn.refresh();
}}
/>
{intefaceCfg && intefaceCfg.sortable === true && !currentMode && (
{interfaceCfg && interfaceCfg.sortable === true && !currentMode && (
<SchemaSettings.SwitchItem
title={t('Sortable')}
checked={field.componentProps.sorter}
@ -407,9 +395,7 @@ export const TableColumnDesigner = (props) => {
}}
/>
)}
{isSubTableColumn && !field?.readPretty && isShowDefaultValue(collectionField, getInterface) && (
<SchemaSettings.DefaultValue fieldSchema={fieldSchema} />
)}
{isAllowToSetDefaultValue(isSubTableColumn) && <SchemaSettings.DefaultValue fieldSchema={fieldSchema} />}
<SchemaSettings.Divider />
<SchemaSettings.Remove
removeParentsIfNoChildren={!isSubTableColumn}

View File

@ -2,10 +2,11 @@ import { DeleteOutlined, MenuOutlined } from '@ant-design/icons';
import { TinyColor } from '@ctrl/tinycolor';
import { SortableContext, SortableContextProps, useSortable } from '@dnd-kit/sortable';
import { css } from '@emotion/css';
import { ArrayField, Field } from '@formily/core';
import { ArrayField } from '@formily/core';
import { spliceArrayState } from '@formily/core/esm/shared/internals';
import { RecursionField, Schema, observer, useField, useFieldSchema } from '@formily/react';
import { action, reaction } from '@formily/reactive';
import { action } from '@formily/reactive';
import { isPortalInBody } from '@nocobase/utils/client';
import { useMemoizedFn } from 'ahooks';
import { Table as AntdTable, TableColumnProps } from 'antd';
import { default as classNames, default as cls } from 'classnames';
@ -20,16 +21,17 @@ import {
useTableSelectorContext,
} from '../../../';
import { useACLFieldWhitelist } from '../../../acl/ACLProvider';
import { LocalVariablesProvider } from '../../../variables/hooks/useLocalVariables';
import { useToken } from '../__builtins__';
import { ColumnFieldProvider } from './components/ColumnFieldProvider';
import { extractIndex, isCollectionFieldComponent, isColumnComponent, isPortalInBody } from './utils';
import { extractIndex, isCollectionFieldComponent, isColumnComponent } from './utils';
const useArrayField = (props) => {
const field = useField<ArrayField>();
return (props.field || field) as ArrayField;
};
const useTableColumns = (props) => {
const useTableColumns = (props: { showDel?: boolean; isSubTable?: boolean }) => {
const field = useArrayField(props);
const schema = useFieldSchema();
const { schemaInWhitelist } = useACLFieldWhitelist();
@ -60,6 +62,7 @@ const useTableColumns = (props) => {
const index = field.value?.indexOf(record);
return (
<RecordIndexProvider index={record.__index || index}>
<LocalVariablesProvider iterationCtx={record}>
<RecordProvider record={record}>
<ColumnFieldProvider schema={s} basePath={field.address.concat(record.__index || index)}>
<RecursionField
@ -69,6 +72,7 @@ const useTableColumns = (props) => {
/>
</ColumnFieldProvider>
</RecordProvider>
</LocalVariablesProvider>
</RecordIndexProvider>
);
},
@ -97,7 +101,7 @@ const useTableColumns = (props) => {
<DeleteOutlined
style={{ cursor: 'pointer' }}
onClick={() => {
action(() => {
void action(() => {
spliceArrayState(field as any, {
startIndex: index,
deleteCount: 1,
@ -161,7 +165,11 @@ const SortHandle = (props) => {
const TableIndex = (props) => {
const { index } = props;
return (
<div className={classNames('nb-table-index')} style={{ padding: '0 8px 0 16px' }}>
<div
data-testid={`table-index-${index}`}
className={classNames('nb-table-index')}
style={{ padding: '0 8px 0 16px' }}
>
{index}
</div>
);
@ -184,28 +192,22 @@ const usePaginationProps = (pagination1, pagination2) => {
return result.total <= result.pageSize ? false : result;
};
const useValidator = (validator: (value: any) => string) => {
const field = useField<Field>();
useEffect(() => {
const dispose = reaction(
() => field.value,
(value) => {
const message = validator(value);
field.setFeedback({
type: 'error',
code: 'ValidateError',
messages: message ? [message] : [],
});
},
);
return () => {
dispose();
};
}, []);
};
export const Table: any = observer(
(props: any) => {
(props: {
useProps?: () => any;
onChange?: (pagination, filters, sorter, extra) => void;
onRowSelectionChange?: (selectedRowKeys: any[], selectedRows: any[]) => void;
onRowDragEnd?: (e: { from: any; to: any }) => void;
onClickRow?: (record: any, setSelectedRow: (selectedRow: any[]) => void, selectedRow: any[]) => void;
pagination?: any;
showIndex?: boolean;
dragSort?: boolean;
rowKey?: string | ((record: any) => string);
rowSelection?: any;
required?: boolean;
onExpand?: (flag: boolean, record: any) => void;
isSubTable?: boolean;
}) => {
const { token } = useToken();
const { pagination: pagination1, useProps, onChange, ...others1 } = props;
const { pagination: pagination2, onClickRow, ...others2 } = useProps?.() || {};
@ -306,7 +308,7 @@ export const Table: any = observer(
const toIndex = e.over?.data.current?.sortable?.index;
const from = field.value[fromIndex] || e.active;
const to = field.value[toIndex] || e.over;
field.move(fromIndex, toIndex);
void field.move(fromIndex, toIndex);
onRowDragEnd({ from, to });
}}
>

View File

@ -1,31 +1,28 @@
import { ArrayItems } from '@formily/antd-v5';
import { ISchema, useField, useFieldSchema } from '@formily/react';
import React, { useCallback, useMemo } from 'react';
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useTableBlockContext } from '../../../block-provider';
import { useFormBlockContext, useTableBlockContext } from '../../../block-provider';
import { mergeFilter } from '../../../block-provider/SharedFilterProvider';
import { useCollection, useCollectionManager } from '../../../collection-manager';
import { useCollectionFilterOptions, useSortFields } from '../../../collection-manager/action-hooks';
import { useSortFields } from '../../../collection-manager/action-hooks';
import { FilterBlockType } from '../../../filter-provider/utils';
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
import { useSchemaTemplate } from '../../../schema-templates';
import { useCompile, useDesignable } from '../../hooks';
import { useDesignable } from '../../hooks';
import { removeNullCondition } from '../filter';
import { FixedBlockDesignerItem } from '../page';
import { FilterDynamicComponent } from './FilterDynamicComponent';
export const TableBlockDesigner = () => {
const { name, title, sortable } = useCollection();
const { getCollectionField, getCollection } = useCollectionManager();
const field = useField();
const fieldSchema = useFieldSchema();
const dataSource = useCollectionFilterOptions(name);
const { form } = useFormBlockContext();
const sortFields = useSortFields(name);
const { service } = useTableBlockContext();
const { t } = useTranslation();
const { dn } = useDesignable();
const compile = useCompile();
const defaultFilter = fieldSchema?.['x-decorator-props']?.params?.filter || {};
const defaultSort = fieldSchema?.['x-decorator-props']?.params?.sort || [];
const defaultResource = fieldSchema?.['x-decorator-props']?.resource;
const supportTemplate = !fieldSchema?.['x-decorator-props']?.disableTemplate;
@ -45,23 +42,6 @@ export const TableBlockDesigner = () => {
const { dragSort, resource } = field.decoratorProps;
const collectionField = resource && getCollectionField(resource);
const treeCollection = resource?.includes('.') ? getCollection(collectionField?.target)?.tree : !!collection?.tree;
const dataScopeSchema = useMemo(() => {
return {
type: 'object',
title: t('Set the data scope'),
properties: {
filter: {
default: defaultFilter,
// title: '数据范围',
enum: compile(dataSource),
'x-component': 'Filter',
'x-component-props': {
dynamicComponent: (props) => FilterDynamicComponent({ ...props }),
},
},
},
} as ISchema;
}, [dataSource, defaultFilter]);
const onDataScopeSubmit = useCallback(
({ filter }) => {
filter = removeNullCondition(filter);
@ -81,7 +61,7 @@ export const TableBlockDesigner = () => {
},
});
},
[field],
[dn, field.decoratorProps, fieldSchema, service],
);
return (
<GeneralSchemaDesigner template={template} title={title || name}>
@ -124,7 +104,12 @@ export const TableBlockDesigner = () => {
/>
)}
<FixedBlockDesignerItem />
<SchemaSettings.ModalItem title={t('Set the data scope')} schema={dataScopeSchema} onSubmit={onDataScopeSubmit} />
<SchemaSettings.DataScope
collectionName={name}
defaultFilter={fieldSchema?.['x-decorator-props']?.params?.filter || {}}
form={form}
onSubmit={onDataScopeSubmit}
/>
{!dragSort && (
<SchemaSettings.ModalItem
title={t('Set default sorting rules')}

View File

@ -3,27 +3,25 @@ import { ISchema, useField, useFieldSchema } from '@formily/react';
import { cloneDeep } from 'lodash';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useTableSelectorContext } from '../../../block-provider';
import { useFormBlockContext, useTableSelectorContext } from '../../../block-provider';
import { recursiveParent } from '../../../block-provider/TableSelectorProvider';
import { useCollection, useCollectionManager } from '../../../collection-manager';
import { useCollectionFilterOptions, useSortFields } from '../../../collection-manager/action-hooks';
import { useSortFields } from '../../../collection-manager/action-hooks';
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
import { useSchemaTemplate } from '../../../schema-templates';
import { useDesignable } from '../../hooks';
import { removeNullCondition } from '../filter';
import { FilterDynamicComponent } from './FilterDynamicComponent';
import { recursiveParent } from '../../../block-provider/TableSelectorProvider';
export const TableSelectorDesigner = () => {
const { name, title } = useCollection();
const { getCollectionJoinField } = useCollectionManager();
const field = useField();
const fieldSchema = useFieldSchema();
const dataSource = useCollectionFilterOptions(name);
const { form } = useFormBlockContext();
const sortFields = useSortFields(name);
const { service, extraFilter } = useTableSelectorContext();
const { t } = useTranslation();
const { dn } = useDesignable();
const defaultFilter = fieldSchema?.['x-decorator-props']?.params?.filter || {};
const defaultSort = fieldSchema?.['x-decorator-props']?.params?.sort || [];
const collectionFieldSchema = recursiveParent(fieldSchema, 'CollectionField');
const collectionField = getCollectionJoinField(collectionFieldSchema?.['x-collection-field']);
@ -43,25 +41,10 @@ export const TableSelectorDesigner = () => {
const { dragSort } = field.decoratorProps;
return (
<GeneralSchemaDesigner template={template} title={title || name} disableInitializer>
<SchemaSettings.ModalItem
title={t('Set the data scope')}
schema={
{
type: 'object',
title: t('Set the data scope'),
properties: {
filter: {
default: defaultFilter,
// title: '数据范围',
enum: dataSource,
'x-component': 'Filter',
'x-component-props': {
dynamicComponent: (props) => FilterDynamicComponent({ ...props }),
},
},
},
} as ISchema
}
<SchemaSettings.DataScope
collectionName={name}
defaultFilter={fieldSchema?.['x-decorator-props']?.params?.filter || {}}
form={form}
onSubmit={({ filter }) => {
filter = removeNullCondition(filter);
const params = field.decoratorProps.params || {};

View File

@ -10,7 +10,6 @@ import { TableSelector } from './TableSelector';
export * from './TableBlockDesigner';
export * from './TableField';
export * from './TableSelectorDesigner';
export * from './FilterDynamicComponent';
export const TableV2 = Table;

View File

@ -17,17 +17,3 @@ export function extractIndex(str) {
});
return numbers.join('.');
}
/**
* DOM createPortal body
* @param domNode DOM
*/
export const isPortalInBody = (dom: HTMLElement) => {
while (dom) {
if (dom.id === 'root') {
return false;
}
dom = dom.parentNode as HTMLElement;
}
return true;
};

View File

@ -15,23 +15,6 @@ import { XButton } from './XButton';
import { useStyles } from './style';
const JT_VALUE_RE = /^\s*{{\s*([^{}]+)\s*}}\s*$/;
const groupClass = css`
width: auto;
display: flex;
&.ant-input-group-compact {
display: flex;
}
.ant-input-disabled {
.ant-tag {
color: #bfbfbf;
border-color: #d9d9d9;
}
}
.ant-input.null-value {
width: 4em;
min-width: 4em;
}
`;
function parseValue(value: any): string | string[] {
if (value == null) {
@ -217,20 +200,20 @@ export function Input(props) {
};
const onSwitch = useCallback(
(next) => {
(next, optionPath: any[]) => {
if (next[0] === '') {
if (next[1]) {
if (next[1] !== type) {
onChange(ConstantTypes[next[1]]?.default ?? null);
onChange(ConstantTypes[next[1]]?.default ?? null, optionPath);
}
} else {
if (variable) {
onChange(null);
onChange(null, optionPath);
}
}
return;
}
onChange(`{{${next.join('.')}}}`);
onChange(`{{${next.join('.')}}}`, optionPath);
},
[type, variable, onChange],
);
@ -254,6 +237,13 @@ export function Input(props) {
}
prevOption = prevOption.children.find((item) => item[names.value] === key);
}
// 如果为空则说明相关字段已被删除
// fix T-1565
if (!prevOption) {
return;
}
labels.push(prevOption[names.label]);
} catch (err) {
error(err);
@ -270,7 +260,7 @@ export function Input(props) {
const disabled = props.disabled || form.disabled;
return wrapSSR(
<Space.Compact style={style} className={classNames(groupClass, componentCls, hashId, className)}>
<Space.Compact style={style} className={classNames(componentCls, hashId, className)}>
{variable ? (
<div
className={cx(
@ -332,7 +322,7 @@ export function Input(props) {
) : null}
</div>
) : (
children ?? <ConstantComponent value={value} onChange={onChange} />
<div style={{ flex: 1 }}>{children ?? <ConstantComponent value={value} onChange={onChange} />}</div>
)}
{options.length > 1 && !disabled ? (
<Cascader

View File

@ -1,7 +1,6 @@
import React, { createContext, useContext } from 'react';
import { connect, mapReadPretty } from '@formily/react';
import React, { createContext, useContext } from 'react';
import { IField } from '../../../collection-manager';
import { Input } from './Input';
import { JSONInput } from './JSONInput';
import { RawTextArea } from './RawTextArea';
@ -10,11 +9,7 @@ import { TextArea } from './TextArea';
const VariableScopeContext = createContext([]);
export function VariableScopeProvider({ scope = [], children }) {
return (
<VariableScopeContext.Provider value={scope}>
{children}
</VariableScopeContext.Provider>
)
return <VariableScopeContext.Provider value={scope}>{children}</VariableScopeContext.Provider>;
}
export function useVariableScope() {
@ -34,7 +29,3 @@ Variable.RawTextArea = connect(RawTextArea);
Variable.JSON = connect(JSONInput);
export default Variable;
export function isInvariable(value: IField) {
return !!value?.invariable;
}

View File

@ -13,7 +13,7 @@ export function VariableSelect({ options, setOptions, onInsert, changeOnSelect =
async function loadData(selectedOptions) {
const option = selectedOptions[selectedOptions.length - 1];
if (!option.children && !option.isLeaf && option.loadChildren) {
if (!option.children?.length && !option.isLeaf && option.loadChildren) {
await option.loadChildren(option);
setOptions((prev) => [...prev]);
}

View File

@ -11,6 +11,14 @@ export const useStyles = genStyleHook('nb-variable', (token) => {
return {
[componentCls]: {
width: 'auto',
display: 'flex',
'&.ant-input-group-compact': { display: 'flex' },
'.ant-input-disabled': {
'.ant-tag': { color: '#bfbfbf', borderColor: '#d9d9d9' },
},
'.ant-input.null-value': { width: '4em', minWidth: '4em' },
'.ant-formily-item .ant-formily-item-control .ant-formily-item-control-content .ant-formily-item-control-content-component':
{
lineHeight: 'normal',
@ -82,6 +90,10 @@ export const useStyles = genStyleHook('nb-variable', (token) => {
'.ant-btn': {
height: 'auto',
},
'.variable': {
flex: 1,
},
},
};
});

View File

@ -1,4 +1,7 @@
/* globals define,module */
import dayjs from 'dayjs';
/*
Using a Universal Module Loader that should be browser, require, and AMD friendly
http://ricostacruz.com/cheatsheets/umdjs.html
@ -146,50 +149,58 @@ export function getJsonLogic() {
if (!a || !b) {
return false;
}
const milliseconds1 = new Date(a).getTime();
const milliseconds2 = new Date(b).getTime();
return milliseconds1 === milliseconds2;
if (!Array.isArray(a)) {
a = [a, a];
}
if (!Array.isArray(b)) {
b = [b, b];
}
a = a.map((date) => dayjs(date));
b = b.map((date) => dayjs(date));
return a[0].isBetween(b[0], b[1], null, '[]') && a[1].isBetween(b[0], b[1], null, '[]');
},
$dateBefore: function (a, b) {
if (!a || !b) {
return false;
}
if (!Array.isArray(a)) {
a = [a, a];
}
if (!Array.isArray(b)) {
b = [b, b];
}
a = a.map((date) => dayjs(date));
b = b.map((date) => dayjs(date));
return new Date(a) < new Date(b);
return a[0].isBefore(b[0]) && a[1].isBefore(b[0]);
},
$dateNotBefore: function (a, b) {
if (!a || !b) {
return false;
}
return new Date(a) <= new Date(b);
return !operations.$dateBefore(a, b);
},
$dateAfter: function (a, b) {
if (!a || !b) {
return false;
}
if (!Array.isArray(a)) {
a = [a, a];
}
if (!Array.isArray(b)) {
b = [b, b];
}
a = a.map((date) => dayjs(date));
b = b.map((date) => dayjs(date));
return new Date(a) > new Date(b);
return a[0].isAfter(b[1]) && a[1].isAfter(b[1]);
},
$dateNotAfter: function (a, b) {
if (!a || !b) {
return false;
}
return new Date(a) >= new Date(b);
return !operations.$dateAfter(a, b);
},
$dateBetween: function (a, b) {
if (!a || !b) {
return false;
}
const startTime = new Date(b?.[0]);
const endTime = new Date(b?.[1]);
return new Date(a) > startTime && new Date(a) < endTime;
return operations.$dateOn(a, b);
},
$dateNotOn: function (a, b) {
if (!a || !b) {
return false;
}
return new Date(a) != new Date(b);
return !operations.$dateOn(a, b);
},
$isTruly: function (a) {
if (Array.isArray(a)) return a.some((k) => k === true || k === 1);

View File

@ -1,9 +1,7 @@
import dayjs from 'dayjs';
import flat from 'flat';
import _, { every, findIndex, isArray, some } from 'lodash';
import { useMemo } from 'react';
import { useTableBlockContext } from '../../../block-provider';
import { useCurrentUserContext } from '../../../user';
import _, { every, findIndex, some } from 'lodash';
import { VariableOption, VariablesContextType } from '../../../variables/types';
import { isVariable } from '../../../variables/utils/isVariable';
import { transformVariableValue } from '../../../variables/utils/transformVariableValue';
import { getJsonLogic } from '../../common/utils/logic';
type VariablesCtx = {
@ -13,42 +11,6 @@ type VariablesCtx = {
$form?: Record<string, any>;
};
function flattenDeep(data, result = []) {
for (let i = 0; i < data?.length; i++) {
const { children, ...rest } = data[i];
result.push(rest);
if (children) {
flattenDeep(children, result);
}
}
return result;
}
export const useVariablesCtx = (): VariablesCtx => {
const currentUser = useCurrentUserContext();
const { field, service, rowKey } = useTableBlockContext();
const tableData = flattenDeep(service?.data?.data);
const contextData = tableData?.filter((v) => (field?.data?.selectedRowKeys || [])?.includes(v[rowKey]));
return useMemo(() => {
return {
$user: currentUser?.data?.data || {},
$date: {
now: () => dayjs().toISOString(),
},
$context: contextData,
};
}, [contextData, currentUser?.data?.data]);
};
export const isVariable = (str: unknown) => {
if (typeof str !== 'string') {
return false;
}
const regex = /{{(.*?)}}/;
const matches = str?.match?.(regex);
return matches ? true : false;
};
export const parseVariables = (str: string, ctx: VariablesCtx | any) => {
const regex = /{{(.*?)}}/;
const matches = str?.match?.(regex);
@ -76,41 +38,6 @@ export function getInnermostKeyAndValue(obj) {
return null;
}
const getFieldValue = (fieldPath, values) => {
const v = fieldPath[0];
const h = fieldPath[1];
const regex = new RegExp('^' + v + '\\..+\\.' + h + '$'); // 构建匹配的正则表达式
const matchedValues = [];
const data = flat.flatten(values, { maxDepth: 3 });
for (const key in data) {
if (regex.test(key)) {
matchedValues.push(data[key]);
}
}
return matchedValues;
};
const getValue = (str: string, values) => {
const regex = /{{(.*?)}}/;
const matches = str?.match?.(regex);
if (matches) {
return getVariableValue(str, values);
} else {
return str;
}
};
const getVariableValue = (str, values) => {
const regex = /{{[^.]+\.([^}]+)}}/;
const match = regex.exec(str);
const targetField = match?.[1]?.split('.') || [];
const isArrayField = isArray(values[targetField[0]]);
if (isArrayField && targetField.length > 1) {
//对多关系字段
return getFieldValue(targetField, values);
} else {
return flat(values)[match?.[1]];
}
};
const getTargetField = (obj) => {
const keys = getAllKeys(obj);
const index = findIndex(keys, (key, index, keys) => {
@ -138,39 +65,71 @@ function getAllKeys(obj) {
return keys;
}
export const conditionAnalyse = (rules, scope) => {
const values = { ...scope, now: new Date() };
export const conditionAnalyses = async ({
rules,
formValues,
variables,
localVariables,
}: {
rules;
formValues;
variables: VariablesContextType;
localVariables: VariableOption[];
}) => {
if (process.env.NODE_ENV !== 'production') {
if (!variables) {
throw new Error(`conditionAnalyses: variables cannot be ${variables}`);
}
}
const type = Object.keys(rules)[0] || '$and';
const conditions = rules[type];
const results = conditions.map((c) => {
let results = conditions.map(async (c) => {
const jsonlogic = getInnermostKeyAndValue(c);
const operator = jsonlogic?.key;
const value = getValue(jsonlogic?.value, values);
const targetField = getTargetField(c);
if (!operator) {
return true;
}
try {
const isArrayField = isArray(values[targetField[0]]);
const jsonLogic = getJsonLogic();
if (isArrayField && targetField.length > 1) {
//对多关系字段比较
const currentValue = getFieldValue(targetField, values);
const result = jsonLogic.apply({ [operator]: [currentValue, value] });
return result;
} else {
const currentValue = targetField.length > 1 ? flat(values)?.[targetField.join('.')] : values?.[targetField[0]];
const result = jsonLogic.apply({ [operator]: [currentValue, value] });
return result;
}
const targetVariableName = targetFieldToVariableString(getTargetField(c));
const parsingResult = isVariable(jsonlogic?.value)
? [
variables.parseVariable(jsonlogic?.value, localVariables),
variables.parseVariable(targetVariableName, localVariables),
]
: [jsonlogic?.value, variables.parseVariable(targetVariableName, localVariables)];
try {
const jsonLogic = getJsonLogic();
const [value, targetValue] = await Promise.all(parsingResult);
const targetCollectionField = await variables.getCollectionField(targetVariableName, localVariables);
return jsonLogic.apply({
[operator]: [
transformVariableValue(targetValue, { targetCollectionField }),
transformVariableValue(value, { targetCollectionField }),
],
});
} catch (error) {
console.error(error);
throw error;
}
});
results = await Promise.all(results);
if (type === '$and') {
return every(results, (v) => v);
} else {
return some(results, (v) => v);
}
};
/**
* 便
* @param targetField
* @returns
*/
function targetFieldToVariableString(targetField: string[]) {
// Action 中的联动规则虽然没有 form 上下文但是在这里也使用的是 `$nForm` 变量,这样实现更简单
return `{{ $nForm.${targetField.join('.')} }}`;
}

View File

@ -1,9 +1,16 @@
import { Schema, SchemaExpressionScopeContext, SchemaOptionsContext } from '@formily/react';
import { useContext, isValidElement } from 'react';
import { isValidElement, useContext } from 'react';
interface Props {
/**
* 使
*/
noCache?: boolean;
}
const compileCache = {};
export const useCompile = () => {
export const useCompile = ({ noCache }: Props = { noCache: false }) => {
const options = useContext(SchemaOptionsContext);
const scope = useContext(SchemaExpressionScopeContext);
return (source: any, ext?: any) => {
@ -33,6 +40,9 @@ export const useCompile = () => {
return Schema.compile(source, mergedScope);
}
try {
if (noCache) {
return Schema.compile(source, mergedScope);
}
compileCache[cacheKey] = compileCache[cacheKey] || Schema.compile(source, mergedScope);
return compileCache[cacheKey];
} catch (e) {

View File

@ -39,7 +39,7 @@ export const TableColumnInitializers = (props: any) => {
{
type: 'itemGroup',
title: t(`Parent collection fields`) + '(' + compile(`${Object.keys(inherit)[0]}`) + ')',
children: Object.values(inherit)[0].filter((v) => !v?.field?.isForeignKey),
children: Object.values(inherit)[0].filter((v: any) => !v?.field?.isForeignKey),
},
);
});

View File

@ -10,6 +10,7 @@ import { useRecord } from '../../record-provider';
import { ActionContextProvider, useActionContext, useCompile } from '../../schema-component';
import { linkageAction } from '../../schema-component/antd/action/utils';
import { parseVariables } from '../../schema-component/common/utils/uitls';
import { useLocalVariables, useVariables } from '../../variables';
export const actionDesignerCss = css`
position: relative;
@ -89,19 +90,28 @@ export const CreateRecordAction = observer(
const fieldSchema = useFieldSchema();
const field: any = useField();
const [currentCollection, setCurrentCollection] = useState(collection.name);
const linkageRules = fieldSchema?.['x-linkage-rules'] || [];
const linkageRules: any[] = fieldSchema?.['x-linkage-rules'] || [];
const values = useRecord();
const ctx = useActionContext();
const variables = useVariables();
const localVariables = useLocalVariables({ currentForm: { values } as any });
useEffect(() => {
field.linkageProperty = {};
linkageRules
.filter((k) => !k.disabled)
.map((v) => {
return v.actions?.map((h) => {
linkageAction(h.operator, field, v.condition, values);
.forEach((v) => {
v.actions?.forEach((h) => {
linkageAction({
operator: h.operator,
field,
condition: v.condition,
values,
variables,
localVariables,
});
});
}, [linkageRules, values]);
});
}, [JSON.stringify(linkageRules), values]);
return (
<div className={actionDesignerCss}>
<ActionContextProvider value={{ ...ctx, visible, setVisible }}>
@ -142,6 +152,8 @@ export const CreateAction = observer(
const fieldSchema = useFieldSchema();
const field: any = useField();
const form = useForm();
const variables = useVariables();
const enableChildren = fieldSchema['x-enable-children'] || [];
const allowAddToCurrent = fieldSchema?.['x-allow-add-to-current'];
const linkageFromForm = fieldSchema?.['x-component-props']?.['linkageFromForm'];
@ -168,8 +180,9 @@ export const CreateAction = observer(
return v && actionAclCheck(`${v.name}:create`);
});
}, [enableChildren, totalChildCollections]);
const linkageRules = fieldSchema?.['x-linkage-rules'] || [];
const linkageRules: any[] = fieldSchema?.['x-linkage-rules'] || [];
const values = useRecord();
const localVariables = useLocalVariables({ currentForm: { values } as any });
const compile = useCompile();
const { designable } = useDesignable();
const icon = props.icon || null;
@ -188,15 +201,21 @@ export const CreateAction = observer(
}, [menuItems]);
useEffect(() => {
field.linkageProperty = {};
linkageRules
.filter((k) => !k.disabled)
.map((v) => {
return v.actions?.map((h) => {
linkageAction(h.operator, field, v.condition, values);
.forEach((v) => {
v.actions?.forEach((h) => {
linkageAction({
operator: h.operator,
field,
condition: v.condition,
values,
variables,
localVariables,
});
});
}, [linkageRules, values]);
});
}, [JSON.stringify(linkageRules), values]);
return (
<div className={actionDesignerCss}>
{FinallyButton({

View File

@ -1,19 +1,30 @@
import { css } from '@emotion/css';
import { Field } from '@formily/core';
import { connect, useField, useFieldSchema } from '@formily/react';
import { useField, useFieldSchema } from '@formily/react';
import { merge } from '@formily/shared';
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import _ from 'lodash';
import React, { useCallback, useEffect, useMemo } from 'react';
import { useFormBlockContext } from '../../../block-provider';
import {
CollectionFieldProvider,
useCollection,
useCollectionField,
useCollectionFilterOptions,
useCollectionManager,
} from '../../../collection-manager';
import { Variable, useCompile, useComponent, useVariableScope } from '../../../schema-component';
import { useRecord } from '../../../record-provider';
import { useCompile, useComponent } from '../../../schema-component';
import { VariableInput, getShouldChange } from '../../../schema-settings/VariableInput/VariableInput';
import { Option } from '../../../schema-settings/VariableInput/type';
import { formatVariableScop } from '../../../schema-settings/VariableInput/utils/formatVariableScop';
import { useLocalVariables, useVariables } from '../../../variables';
import { DeletedField } from '../DeletedField';
interface AssignedFieldProps {
value: any;
onChange: (value: any) => void;
[key: string]: any;
}
const InternalField: React.FC = (props) => {
const field = useField<Field>();
const fieldSchema = useFieldSchema();
@ -68,72 +79,73 @@ const InternalField: React.FC = (props) => {
return React.createElement(component, props, props.children);
};
const CollectionField = connect((props) => {
const CollectionField = (props) => {
const fieldSchema = useFieldSchema();
return (
<CollectionFieldProvider name={fieldSchema.name} fallback={<DeletedField />}>
<InternalField {...props} />
</CollectionFieldProvider>
);
});
};
export enum AssignedFieldValueType {
ConstantValue = 'constantValue',
DynamicValue = 'dynamicValue',
}
export const AssignedField = (props: any) => {
export const AssignedField = (props: AssignedFieldProps) => {
const { value, onChange } = props;
const { t } = useTranslation();
const compile = useCompile();
const fieldSchema = useFieldSchema();
const { getField } = useCollection();
const collectionField = getField(fieldSchema.name);
const [options, setOptions] = useState<any[]>([]);
const { getCollectionFields, getAllCollectionsInheritChain } = useCollectionManager();
const collection = useCollection();
const fields = compile(useCollectionFilterOptions(collection));
const userFields = compile(useCollectionFilterOptions('users'));
const scope = useVariableScope();
useEffect(() => {
const opt = [
{
name: 'currentRecord',
title: t('Current record'),
children: fields,
},
{
name: 'currentUser',
title: t('Current user'),
children: userFields,
},
];
if (['createdAt', 'datetime', 'time', 'updatedAt'].includes(collectionField?.interface)) {
opt.unshift({
name: 'currentTime',
title: t('Current time'),
children: null,
});
const { form } = useFormBlockContext();
const fieldSchema = useFieldSchema();
const record = useRecord();
const variables = useVariables();
const localVariables = useLocalVariables();
const currentFormFields = useCollectionFilterOptions(collection);
const { name, getField } = collection;
const collectionField = getField(fieldSchema.name);
const shouldChange = useMemo(
() => getShouldChange({ collectionField, variables, localVariables, getAllCollectionsInheritChain }),
[collectionField, getAllCollectionsInheritChain, localVariables, variables],
);
const returnScope = useCallback(
(scope: Option[]) => {
const currentForm = scope.find((item) => item.value === '$nForm');
const fields = getCollectionFields(name);
// fix https://nocobase.height.app/T-1355
// 工作流人工节点的 `自定义表单` 区块,与其它表单区块不同,根据它的数据表名称,获取到的字段列表为空,所以需要在这里特殊处理一下
if (!fields?.length && currentForm) {
currentForm.children = formatVariableScop(currentFormFields);
}
const next = opt.concat(scope);
setOptions(next);
}, [fields, userFields, scope]);
return scope;
},
[currentFormFields, name],
);
const renderSchemaComponent = useCallback(
({ value, onChange }): React.JSX.Element => {
return <CollectionField {...props} value={value} onChange={onChange} />;
},
[JSON.stringify(_.omit(props, 'value'))],
);
return (
<Variable.Input
<VariableInput
form={form}
record={record}
value={value}
onChange={onChange}
scope={options}
className={css`
.variable {
width: 100%;
}
`}
fieldNames={{
label: 'title',
value: 'name',
}}
>
<CollectionField value={value} onChange={onChange} />
</Variable.Input>
renderSchemaComponent={renderSchemaComponent}
collectionField={collectionField}
shouldChange={shouldChange}
returnScope={returnScope}
targetFieldSchema={fieldSchema}
/>
);
};

View File

@ -1,8 +1,9 @@
import { Field, Form } from '@formily/core';
import { ISchema, Schema, useFieldSchema, useForm } from '@formily/react';
import { uid } from '@formily/shared';
import { useContext, useMemo } from 'react';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { BlockRequestContext, SchemaInitializerItemOptions } from '..';
import { SchemaInitializerItemOptions, useFormActiveFields, useFormBlockContext } from '../';
import { CollectionFieldOptions, FieldOptions, useCollection, useCollectionManager } from '../collection-manager';
import { isAssocField } from '../filter-provider/utils';
import { useActionContext, useDesignable } from '../schema-component';
@ -744,7 +745,10 @@ const recursiveParent = (schema: Schema) => {
};
export const useCurrentSchema = (action: string, key: string, find = findSchema, rm = removeSchema) => {
const { removeActiveFieldName } = useFormActiveFields() || {};
const { form }: { form: Form } = useFormBlockContext();
let fieldSchema = useFieldSchema();
if (!fieldSchema?.['x-initializer']) {
const recursiveInitializerSchema = recursiveParent(fieldSchema);
if (recursiveInitializerSchema) {
@ -753,16 +757,15 @@ export const useCurrentSchema = (action: string, key: string, find = findSchema,
}
const { remove } = useDesignable();
const schema = find(fieldSchema, key, action);
const ctx = useContext(BlockRequestContext);
return {
schema,
exists: !!schema,
remove() {
if (ctx.field) {
ctx.field.data = ctx.field.data || {};
ctx.field.data.activeFields = ctx.field.data.activeFields || new Set();
ctx.field.data.activeFields.delete(schema.name);
}
removeActiveFieldName?.(schema.name);
form?.query(schema.name).forEach((field: Field) => {
field.setInitialValue(null);
field.reset();
});
schema && rm(schema, remove);
},
};

View File

@ -6,11 +6,10 @@ import _ from 'lodash';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { GeneralSchemaDesigner, SchemaSettings } from '../..';
import { mergeFilter } from '../../../block-provider';
import { useCollectionFilterOptions, useCollectionManager } from '../../../collection-manager';
import { mergeFilter, useFormBlockContext } from '../../../block-provider';
import { useCollectionManager } from '../../../collection-manager';
import { removeNullCondition, useCompile, useDesignable } from '../../../schema-component';
import { ITemplate } from '../../../schema-component/antd/form-v2/Templates';
import { FilterDynamicComponent } from '../../../schema-component/antd/table-v2/FilterDynamicComponent';
export const Designer = observer(
() => {
@ -24,13 +23,13 @@ export const Designer = observer(
formSchema: ISchema;
data?: ITemplate;
};
const { form } = useFormBlockContext();
// 在这里读取 resource 的值,当 resource 变化时,会触发该组件的更新
const collectionName = field.componentProps.service.resource;
const collection = getCollection(collectionName);
const collectionFields = getCollectionFields(collectionName);
const dataSource = useCollectionFilterOptions(collectionName);
if (!data) {
error('data is required');
@ -63,25 +62,10 @@ export const Designer = observer(
return (
<GeneralSchemaDesigner draggable={false}>
<SchemaSettings.ModalItem
title={t('Set the data scope')}
schema={
{
type: 'object',
title: t('Set the data scope'),
properties: {
filter: {
default: getFilter(),
// title: '数据范围',
enum: dataSource,
'x-component': 'Filter',
'x-component-props': {
dynamicComponent: (props) => FilterDynamicComponent({ ...props }),
},
},
},
} as ISchema
}
<SchemaSettings.DataScope
collectionName={collectionName}
defaultFilter={getFilter()}
form={form}
onSubmit={({ filter }) => {
setFilter(filter);

View File

@ -7,21 +7,18 @@ import { useFormVariable } from '../VariableInput/hooks/useFormVariable';
import { useIterationVariable } from '../VariableInput/hooks/useIterationVariable';
export const ChildDynamicComponent = observer(
(props: { rootCollection: string; form: any; onChange; value; default; collectionField }) => {
const { form, rootCollection, onChange, value, collectionField } = props;
(props: { rootCollection: string; onChange; value; default; collectionField }) => {
const { rootCollection, onChange, value, collectionField } = props;
const fieldSchema = useFieldSchema();
const formVariabele = useFormVariable({ blockForm: form, rootCollection });
const iterationVariabele = useIterationVariable({
blockForm: form,
currentCollection: collectionField.collectionName,
rootCollection,
const formVariable = useFormVariable({ collectionName: rootCollection, collectionField });
const iterationVariable = useIterationVariable({
currentCollection: collectionField?.collectionName,
schema: collectionField?.uiSchema,
collectionField,
});
const compile = useCompile();
const result = useMemo(
() => [formVariabele, iterationVariabele].filter(Boolean),
[formVariabele, iterationVariabele],
);
const result = useMemo(() => [formVariable, iterationVariable].filter(Boolean), [formVariable, iterationVariable]);
const scope = compile(result);
useEffect(() => {
onChange(fieldSchema.default);

View File

@ -1,20 +0,0 @@
import React from 'react';
import { Variable } from '../../schema-component';
import { useVariableOptions } from './Variables';
type Props = {
value: any;
onChange: (value: any) => void;
renderSchemaComponent: () => React.ReactNode;
collectionName: string;
};
export function FilterDynamicComponent(props: Props) {
const { value, onChange, renderSchemaComponent, collectionName } = props;
const scope = useVariableOptions(collectionName, ['o2m', 'm2m']);
return (
<Variable.Input value={value} onChange={onChange} scope={scope}>
{renderSchemaComponent()}
</Variable.Input>
);
}

View File

@ -4,7 +4,7 @@ import { TreeSelect } from '@formily/antd-v5';
import { observer } from '@formily/react';
import { uid } from '@formily/shared';
import { Select, Space } from 'antd';
import React, { useContext, useState } from 'react';
import React, { useCallback, useContext, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useCompile } from '../..';
import { DynamicComponent } from './DynamicComponent';
@ -110,40 +110,57 @@ export const FormButtonLinkageRuleAction = observer(
const { value, options } = props;
const { t } = useTranslation();
const compile = useCompile();
const [editFalg, setEditFlag] = useState(false);
const [editFlag, setEditFlag] = useState(false);
const remove = useContext(RemoveActionContext);
const { schema, operator, setOperator, setValue } = useValues(options);
const operators = [
const operators = useMemo(
() =>
compile([
{ label: t('Visible'), value: ActionType.Visible, schema: {} },
{ label: t('Hidden'), value: ActionType.Hidden, schema: {} },
{ label: t('Disabled'), value: ActionType.Disabled, schema: {} },
{ label: t('Enabled'), value: ActionType.Active, schema: {} },
];
]),
[compile, t],
);
const onChange = useCallback(
(value) => {
const flag = [ActionType.Value].includes(value);
setEditFlag(flag);
setOperator(value);
},
[setOperator],
);
const onChangeValue = useCallback(
(value) => {
setValue(value);
},
[setValue],
);
const closeStyle = useMemo(() => ({ color: '#bfbfbf' }), []);
return (
<div style={{ marginBottom: 8 }}>
<Space>
<Select
popupMatchSelectWidth={false}
value={operator}
options={compile(operators)}
onChange={(value) => {
const flag = [ActionType.Value].includes(value);
setEditFlag(flag);
setOperator(value);
}}
options={operators}
onChange={onChange}
placeholder={t('action')}
/>
{editFalg &&
{editFlag &&
React.createElement(DynamicComponent, {
value,
schema,
onChange(value) {
setValue(value);
},
onChange: onChangeValue,
})}
{!props.disabled && (
<a data-testid="close-icon-button">
<CloseCircleOutlined onClick={() => remove()} style={{ color: '#bfbfbf' }} />
<CloseCircleOutlined onClick={remove} style={closeStyle} />
</a>
)}
</Space>

View File

@ -1,10 +1,10 @@
import { ArrayField as ArrayFieldModel, VoidField } from '@formily/core';
import { ArrayField, ObjectField, observer, useField } from '@formily/react';
import { Space } from 'antd';
import React from 'react';
import React, { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { RemoveActionContext } from './context';
import { FormButtonLinkageRuleAction, FormFieldLinkageRuleAction } from './LinkageRuleAction';
import { RemoveActionContext } from './context';
export const LinkageRuleActions = observer(
(props: any): any => {
const { type, linkageOptions } = props;
@ -26,29 +26,38 @@ export const LinkageRuleActions = observer(
{ displayName: 'LinkageRuleActions' },
);
export const LinkageRuleActionGroup = (props) => {
interface LinkageRuleActionGroupProps {
useProps: () => {
type: 'button' | 'field';
linkageOptions: any;
collectionName: string;
};
}
export const LinkageRuleActionGroup = (props: LinkageRuleActionGroupProps) => {
const { t } = useTranslation();
const field = useField<VoidField>();
const logic = 'actions';
const { type, linkageOptions, collectionName } = props?.useProps();
return (
<div style={{ marginLeft: 10 }}>
<ArrayField
name={logic}
component={[LinkageRuleActions, { type, linkageOptions, collectionName }]}
disabled={false}
/>
<Space size={16} style={{ marginTop: 8, marginBottom: 8 }}>
<a
onClick={() => {
const { type, linkageOptions, collectionName } = props.useProps();
const style = useMemo(() => ({ marginLeft: 10 }), []);
const components = useMemo(
() => [LinkageRuleActions, { type, linkageOptions, collectionName }],
[collectionName, linkageOptions, type],
);
const spaceStyle = useMemo(() => ({ marginTop: 8, marginBottom: 8 }), []);
const onClick = useCallback(() => {
const f = field.query('.actions').take() as ArrayFieldModel;
const items = f.value || [];
items.push({});
f.value = items;
}}
>
{t('Add property')}
</a>
}, [field]);
return (
<div style={style}>
<ArrayField name={logic} component={components} disabled={false} />
<Space size={16} style={spaceStyle}>
<a onClick={onClick}>{t('Add property')}</a>
</Space>
</div>
);

View File

@ -1,17 +1,107 @@
import { Input, Select } from 'antd';
import React, { useState } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useFormBlockContext } from '../../block-provider';
import { useRecord } from '../../record-provider';
import { Variable } from '.././../schema-component';
import { useCompatOldVariables } from '../VariableInput/VariableInput';
import { useVariableOptions } from '../VariableInput/hooks/useVariableOptions';
import { DynamicComponent } from './DynamicComponent';
import { useVariableOptions } from './Variables';
const { Option } = Select;
export const ValueDynamicComponent = (props) => {
interface ValueDynamicComponentProps {
fieldValue: any;
schema: any;
setValue: (value: any) => void;
collectionName: string;
}
export const ValueDynamicComponent = (props: ValueDynamicComponentProps) => {
const { fieldValue, schema, setValue, collectionName } = props;
const [mode, setMode] = useState(fieldValue?.mode || 'constant');
const { t } = useTranslation();
const scope = useVariableOptions(collectionName);
const { form } = useFormBlockContext();
const record = useRecord();
const scope = useVariableOptions({
collectionField: { uiSchema: schema },
form,
record,
uiSchema: schema,
});
const { compatOldVariables } = useCompatOldVariables({
collectionField: { uiSchema: schema, collectionName },
uiSchema: schema,
blockCollectionName: collectionName,
});
const constantStyle = useMemo(() => {
return { minWidth: 150, maxWidth: 430, marginLeft: 5 };
}, []);
const handleChangeOfConstant = useCallback(
(value) => {
setValue({
mode,
value,
});
},
[mode, setValue],
);
const expressStyle = useMemo(() => {
return { minWidth: 150, maxWidth: 430, fontSize: 13 };
}, []);
const handleChangeOfExpress = useCallback(
(value) => {
const result = value.replaceAll(`${collectionName}.`, '').replaceAll('$system.', '').trim();
setValue({
mode,
value,
result,
});
},
[collectionName, mode, setValue],
);
const textAreaStyle = useMemo(() => {
return { minWidth: 460, marginRight: 15 };
}, []);
const compatScope = useMemo(() => {
return compatOldVariables(scope, {
value: fieldValue?.value,
});
}, [compatOldVariables, fieldValue?.value, scope]);
const modeMap = {
// 常量
constant: (
<div style={constantStyle}>
{React.createElement(DynamicComponent, {
value: fieldValue?.value || fieldValue,
schema,
onChange: handleChangeOfConstant,
})}
</div>
),
// 表达式
express: (
<div style={expressStyle}>
<Variable.TextArea
value={fieldValue?.value}
onChange={handleChangeOfExpress}
scope={compatScope}
style={textAreaStyle}
/>
<>
<span style={{ marginLeft: '.25em' }} className={'ant-formily-item-extra'}>
{t('Syntax references')}:
</span>
<a href="https://formulajs.info/functions/" target="_blank" rel="noreferrer">
Formula.js
</a>
</>
</div>
),
};
return (
<Input.Group compact>
<Select
@ -29,44 +119,7 @@ export const ValueDynamicComponent = (props) => {
<Option value="express">{t('Expression')}</Option>
<Option value="empty">{t('Empty')}</Option>
</Select>
{mode === 'constant' ? (
<div style={{ minWidth: 150, maxWidth: 430, marginLeft: 5 }}>
{React.createElement(DynamicComponent, {
value: fieldValue?.value || fieldValue,
schema,
onChange(value) {
setValue({
mode,
value,
});
},
})}
</div>
) : mode === 'express' ? (
<div style={{ minWidth: 150, maxWidth: 430, fontSize: 13 }}>
<Variable.TextArea
value={fieldValue?.value}
onChange={(value) => {
const result = value.replaceAll(`${collectionName}.`, '').replaceAll('$system.', '').trim();
setValue({
mode,
value,
result,
});
}}
scope={scope}
style={{ minWidth: 460, marginRight: 15 }}
/>
<>
<span style={{ marginLeft: '.25em' }} className={'ant-formily-item-extra'}>
{t('Syntax references')}:
</span>
<a href="https://formulajs.info/functions/" target="_blank" rel="noreferrer">
Formula.js
</a>
</>
</div>
) : null}
{modeMap[mode]}
</Input.Group>
);
};

Some files were not shown because too many files have changed in this diff Show More