@nocobase/client-v2 2.1.0-beta.25 → 2.1.0-beta.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/es/flow/actions/linkageRulesFormValueRefresh.d.ts +10 -0
- package/es/flow/components/code-editor/types.d.ts +1 -0
- package/es/flow/index.d.ts +1 -0
- package/es/flow/models/actions/AssociateActionModel.d.ts +19 -0
- package/es/flow/models/actions/AssociationActionUtils.d.ts +17 -0
- package/es/flow/models/actions/DisassociateActionModel.d.ts +16 -0
- package/es/flow/models/actions/index.d.ts +3 -0
- package/es/flow/models/base/GridModel.d.ts +3 -1
- package/es/flow/models/blocks/filter-form/FilterFormGridModel.d.ts +15 -6
- package/es/flow/models/blocks/shared/filterOperators.d.ts +9 -0
- package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +1 -0
- package/es/flow/models/fields/AssociationFieldModel/recordSelectSettingsUtils.d.ts +9 -0
- package/es/flow-compat/data.d.ts +9 -2
- package/es/flow-compat/index.d.ts +1 -1
- package/es/index.d.ts +1 -0
- package/es/index.mjs +90 -90
- package/lib/index.js +89 -89
- package/package.json +5 -5
- package/src/BaseApplication.tsx +1 -1
- package/src/__tests__/app.test.tsx +23 -6
- package/src/__tests__/globalDeps.test.ts +5 -0
- package/src/flow/actions/__tests__/linkageRules.formValueDrivenRefresh.test.ts +438 -0
- package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +42 -0
- package/src/flow/actions/linkageRules.tsx +8 -1
- package/src/flow/actions/linkageRulesFormValueRefresh.ts +492 -0
- package/src/flow/actions/linkageRulesRefresh.tsx +4 -2
- package/src/flow/actions/titleField.tsx +8 -3
- package/src/flow/components/FieldAssignValueInput.tsx +1 -0
- package/src/flow/components/code-editor/__tests__/linter.test.ts +18 -0
- package/src/flow/components/code-editor/__tests__/runjsDiagnostics.test.ts +23 -0
- package/src/flow/components/code-editor/index.tsx +18 -17
- package/src/flow/components/code-editor/linter.ts +222 -158
- package/src/flow/components/code-editor/runjsDiagnostics.ts +161 -97
- package/src/flow/components/code-editor/types.ts +1 -0
- package/src/flow/components/filter/LinkageFilterItem.tsx +6 -5
- package/src/flow/components/filter/VariableFilterItem.tsx +14 -13
- package/src/flow/components/filter/__tests__/LinkageFilterItem.test.tsx +33 -0
- package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +48 -5
- package/src/flow/index.ts +1 -0
- package/src/flow/internal/utils/__tests__/titleFieldQuickSync.test.ts +1 -0
- package/src/flow/internal/utils/titleFieldQuickSync.ts +2 -2
- package/src/flow/models/actions/AssociateActionModel.tsx +196 -0
- package/src/flow/models/actions/AssociationActionUtils.ts +90 -0
- package/src/flow/models/actions/DisassociateActionModel.tsx +57 -0
- package/src/flow/models/actions/FilterActionModel.tsx +17 -9
- package/src/flow/models/actions/__tests__/AssociationActionModel.test.ts +250 -0
- package/src/flow/models/actions/index.ts +3 -0
- package/src/flow/models/base/GridModel.tsx +21 -1
- package/src/flow/models/base/__tests__/GridModel.dragSnapshotContainer.test.ts +98 -0
- package/src/flow/models/blocks/details/DetailsItemModel.tsx +3 -0
- package/src/flow/models/blocks/filter-form/FilterFormGridModel.tsx +200 -36
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormGridModel.toggleFormFieldsCollapse.test.ts +270 -1
- package/src/flow/models/blocks/filter-form/__tests__/customFieldOperators.test.tsx +23 -0
- package/src/flow/models/blocks/filter-form/customFieldOperators.ts +12 -1
- package/src/flow/models/blocks/filter-form/fields/FieldComponentProps.tsx +22 -8
- package/src/flow/models/blocks/filter-form/fields/__tests__/FilterFormCustomFieldModel.recordSelect.test.tsx +18 -0
- package/src/flow/models/blocks/filter-manager/FilterManager.ts +51 -1
- package/src/flow/models/blocks/filter-manager/__tests__/FilterManager.test.ts +75 -0
- package/src/flow/models/blocks/form/FormItemModel.tsx +48 -28
- package/src/flow/models/blocks/shared/filterOperators.ts +14 -0
- package/src/flow/models/blocks/table/TableBlockModel.tsx +19 -3
- package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +5 -1
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +21 -5
- package/src/flow/models/fields/AssociationFieldModel/recordSelectSettingsUtils.ts +20 -0
- package/src/flow/models/fields/DividerItemModel.tsx +30 -15
- package/src/flow/models/fields/mobile-components/MobileSelect.tsx +11 -3
- package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +235 -0
- package/src/flow-compat/data.ts +25 -3
- package/src/flow-compat/index.ts +7 -1
- package/src/index.ts +1 -0
- package/src/utils/globalDeps.ts +6 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { FlowModel, FlowModelRenderer, tExpr, useFlowViewContext } from '@nocobase/flow-engine';
|
|
11
|
+
import { useRequest } from 'ahooks';
|
|
12
|
+
import { Button } from 'antd';
|
|
13
|
+
import type { ButtonProps } from 'antd';
|
|
14
|
+
import React from 'react';
|
|
15
|
+
import { SkeletonFallback } from '../../components/SkeletonFallback';
|
|
16
|
+
import { ActionModel, ActionSceneEnum } from '../base';
|
|
17
|
+
import {
|
|
18
|
+
applyAssociateAction,
|
|
19
|
+
getAssociationTargetResourceSettings,
|
|
20
|
+
isAssociationBlockContext,
|
|
21
|
+
} from './AssociationActionUtils';
|
|
22
|
+
|
|
23
|
+
function AssociateSelectorGridRenderer({ options }: { options: any }) {
|
|
24
|
+
const ctx = useFlowViewContext();
|
|
25
|
+
const { data, loading } = useRequest(
|
|
26
|
+
async () => {
|
|
27
|
+
return await ctx.engine.loadOrCreateModel(options, {
|
|
28
|
+
delegateToParent: false,
|
|
29
|
+
delegate: ctx,
|
|
30
|
+
skipSave: !ctx.flowSettingsEnabled,
|
|
31
|
+
});
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
refreshDeps: [ctx, options],
|
|
35
|
+
},
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
if (loading || !data?.uid) {
|
|
39
|
+
return <SkeletonFallback style={{ margin: 16 }} />;
|
|
40
|
+
}
|
|
41
|
+
return <FlowModelRenderer model={data as FlowModel} fallback={<SkeletonFallback style={{ margin: 16 }} />} />;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function AssociateSelectorContent({ model }: { model: AssociateActionModel }) {
|
|
45
|
+
const ctx = useFlowViewContext();
|
|
46
|
+
const { Header, Footer, type } = ctx.view;
|
|
47
|
+
return (
|
|
48
|
+
<div>
|
|
49
|
+
<Header
|
|
50
|
+
title={
|
|
51
|
+
type === 'dialog' ? (
|
|
52
|
+
<div
|
|
53
|
+
style={{
|
|
54
|
+
padding: `${ctx.themeToken.paddingLG}px ${ctx.themeToken.paddingLG}px 0`,
|
|
55
|
+
marginBottom: -ctx.themeToken.marginSM,
|
|
56
|
+
backgroundColor: 'var(--colorBgLayout)',
|
|
57
|
+
}}
|
|
58
|
+
>
|
|
59
|
+
{ctx.t('Select record')}
|
|
60
|
+
</div>
|
|
61
|
+
) : (
|
|
62
|
+
ctx.t('Select record')
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
/>
|
|
66
|
+
<AssociateSelectorGridRenderer
|
|
67
|
+
options={{
|
|
68
|
+
parentId: ctx.view.inputArgs.parentId,
|
|
69
|
+
subKey: 'associate-selector-grid',
|
|
70
|
+
async: true,
|
|
71
|
+
delegateToParent: false,
|
|
72
|
+
subType: 'object',
|
|
73
|
+
use: 'BlockGridModel',
|
|
74
|
+
}}
|
|
75
|
+
/>
|
|
76
|
+
<Footer>
|
|
77
|
+
{type === 'dialog' ? (
|
|
78
|
+
<div style={{ padding: `0 ${ctx.themeToken.paddingLG}px ${ctx.themeToken.paddingLG}px` }}>
|
|
79
|
+
<Button
|
|
80
|
+
type="primary"
|
|
81
|
+
onClick={async () => {
|
|
82
|
+
await model.associateSelectedRows();
|
|
83
|
+
ctx.view.close();
|
|
84
|
+
}}
|
|
85
|
+
>
|
|
86
|
+
{ctx.t('Submit')}
|
|
87
|
+
</Button>
|
|
88
|
+
</div>
|
|
89
|
+
) : (
|
|
90
|
+
<Button
|
|
91
|
+
type="primary"
|
|
92
|
+
onClick={async () => {
|
|
93
|
+
await model.associateSelectedRows();
|
|
94
|
+
ctx.view.close();
|
|
95
|
+
}}
|
|
96
|
+
>
|
|
97
|
+
{ctx.t('Submit')}
|
|
98
|
+
</Button>
|
|
99
|
+
)}
|
|
100
|
+
</Footer>
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export class AssociateActionModel extends ActionModel {
|
|
106
|
+
static scene = ActionSceneEnum.collection;
|
|
107
|
+
static capabilityActionName = 'update';
|
|
108
|
+
|
|
109
|
+
defaultPopupTitle = tExpr('Select record');
|
|
110
|
+
selectedRows: any[] = [];
|
|
111
|
+
|
|
112
|
+
defaultProps: ButtonProps = {
|
|
113
|
+
title: tExpr('Associate'),
|
|
114
|
+
icon: 'LinkOutlined',
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
getAclActionName() {
|
|
118
|
+
return 'update';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async associateSelectedRows() {
|
|
122
|
+
await applyAssociateAction(this.context, this.selectedRows);
|
|
123
|
+
this.selectedRows = [];
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
AssociateActionModel.define({
|
|
128
|
+
label: tExpr('Associate'),
|
|
129
|
+
sort: 15,
|
|
130
|
+
hide(ctx) {
|
|
131
|
+
return !isAssociationBlockContext(ctx);
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
AssociateActionModel.registerFlow({
|
|
136
|
+
key: 'associateSettings',
|
|
137
|
+
title: tExpr('Associate settings'),
|
|
138
|
+
on: 'click',
|
|
139
|
+
steps: {
|
|
140
|
+
openSelector: {
|
|
141
|
+
async handler(ctx, params) {
|
|
142
|
+
const blockModel = ctx.blockModel;
|
|
143
|
+
const targetResourceSettings = getAssociationTargetResourceSettings(ctx);
|
|
144
|
+
const openMode = ctx.inputArgs?.isMobileLayout ? 'embed' : ctx.inputArgs?.mode || params?.mode || 'drawer';
|
|
145
|
+
const size = ctx.inputArgs?.size || params?.size || 'medium';
|
|
146
|
+
const sizeToWidthMap: Record<string, Record<string, string | undefined>> = {
|
|
147
|
+
drawer: {
|
|
148
|
+
small: '30%',
|
|
149
|
+
medium: '50%',
|
|
150
|
+
large: '70%',
|
|
151
|
+
},
|
|
152
|
+
dialog: {
|
|
153
|
+
small: '40%',
|
|
154
|
+
medium: '50%',
|
|
155
|
+
large: '80%',
|
|
156
|
+
},
|
|
157
|
+
embed: {},
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
ctx.model.selectedRows = [];
|
|
161
|
+
await ctx.viewer.open({
|
|
162
|
+
type: openMode,
|
|
163
|
+
width: sizeToWidthMap[openMode][size],
|
|
164
|
+
inheritContext: false,
|
|
165
|
+
target: ctx.layoutContentElement,
|
|
166
|
+
inputArgs: {
|
|
167
|
+
parentId: ctx.model.uid,
|
|
168
|
+
scene: 'select',
|
|
169
|
+
dataSourceKey: targetResourceSettings.dataSourceKey,
|
|
170
|
+
collectionName: targetResourceSettings.collectionName,
|
|
171
|
+
rowSelectionProps: {
|
|
172
|
+
type: 'checkbox',
|
|
173
|
+
defaultSelectedRows: () => blockModel?.resource?.getData?.() || [],
|
|
174
|
+
renderCell: undefined,
|
|
175
|
+
selectedRowKeys: undefined,
|
|
176
|
+
onChange: (_, selectedRows) => {
|
|
177
|
+
ctx.model.selectedRows = selectedRows || [];
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
content: () => <AssociateSelectorContent model={ctx.model as AssociateActionModel} />,
|
|
182
|
+
styles: {
|
|
183
|
+
content: {
|
|
184
|
+
padding: 0,
|
|
185
|
+
backgroundColor: ctx.model.flowEngine.context.themeToken.colorBgLayout,
|
|
186
|
+
...(openMode === 'embed' ? { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 } : {}),
|
|
187
|
+
},
|
|
188
|
+
body: {
|
|
189
|
+
padding: 0,
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { FlowModelContext } from '@nocobase/flow-engine';
|
|
11
|
+
|
|
12
|
+
export const getAssociationBlockResourceSettings = (ctx: FlowModelContext | any) => {
|
|
13
|
+
const blockModel = ctx?.blockModel || ctx?.model?.context?.blockModel;
|
|
14
|
+
return (
|
|
15
|
+
blockModel?.getResourceSettingsInitParams?.() ||
|
|
16
|
+
blockModel?.getStepParams?.('resourceSettings', 'init') ||
|
|
17
|
+
ctx?.model?.getStepParams?.('resourceSettings', 'init')
|
|
18
|
+
);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const isAssociationBlockContext = (ctx: FlowModelContext | any) => {
|
|
22
|
+
return !!getAssociationBlockResourceSettings(ctx)?.associationName;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const getAssociationTargetResourceSettings = (ctx: FlowModelContext | any) => {
|
|
26
|
+
const resourceSettings = getAssociationBlockResourceSettings(ctx);
|
|
27
|
+
const association = ctx?.blockModel?.association || ctx?.model?.context?.blockModel?.association;
|
|
28
|
+
const targetCollection = association?.targetCollection;
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
dataSourceKey: targetCollection?.dataSourceKey || resourceSettings?.dataSourceKey,
|
|
32
|
+
collectionName: targetCollection?.name || association?.target || resourceSettings?.collectionName,
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const callAssociationResourceAction = async (resource: any, action: 'add' | 'remove', values: any[]) => {
|
|
37
|
+
if (typeof resource?.[action] === 'function') {
|
|
38
|
+
return await resource[action]({ values });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return await resource?.runAction?.(action, {
|
|
42
|
+
data: values,
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const applyDisassociateAction = async (ctx: FlowModelContext | any) => {
|
|
47
|
+
const resource = ctx?.blockModel?.resource || ctx?.resource;
|
|
48
|
+
const collection = ctx?.blockModel?.collection || ctx?.collection;
|
|
49
|
+
|
|
50
|
+
if (!isAssociationBlockContext(ctx)) {
|
|
51
|
+
ctx.message?.error?.(ctx.t('No association block selected'));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (!resource) {
|
|
55
|
+
ctx.message?.error?.(ctx.t('No resource selected for disassociation'));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (!ctx.record) {
|
|
59
|
+
ctx.message?.error?.(ctx.t('No record selected for disassociation'));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const filterByTk = collection?.getFilterByTK?.(ctx.record);
|
|
64
|
+
await callAssociationResourceAction(resource, 'remove', [filterByTk]);
|
|
65
|
+
await resource.refresh?.();
|
|
66
|
+
ctx.message?.success?.(ctx.t('Record disassociated successfully'));
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export const applyAssociateAction = async (ctx: FlowModelContext | any, selectedRows: any[]) => {
|
|
70
|
+
const resource = ctx?.blockModel?.resource || ctx?.resource;
|
|
71
|
+
const collection = ctx?.blockModel?.collection || ctx?.collection;
|
|
72
|
+
|
|
73
|
+
if (!isAssociationBlockContext(ctx)) {
|
|
74
|
+
ctx.message?.error?.(ctx.t('No association block selected'));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (!resource) {
|
|
78
|
+
ctx.message?.error?.(ctx.t('No resource selected for association'));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (!selectedRows?.length) {
|
|
82
|
+
ctx.message?.warning?.(ctx.t('Please select at least one record'));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const values = selectedRows.map((row) => collection?.getFilterByTK?.(row) ?? row).filter((value) => value != null);
|
|
87
|
+
await callAssociationResourceAction(resource, 'add', values);
|
|
88
|
+
await resource.refresh?.();
|
|
89
|
+
ctx.message?.success?.(ctx.t('Record associated successfully'));
|
|
90
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { tExpr } from '@nocobase/flow-engine';
|
|
11
|
+
import type { ButtonProps } from 'antd';
|
|
12
|
+
import { ActionModel, ActionSceneEnum } from '../base';
|
|
13
|
+
import { applyDisassociateAction, isAssociationBlockContext } from './AssociationActionUtils';
|
|
14
|
+
|
|
15
|
+
export class DisassociateActionModel extends ActionModel {
|
|
16
|
+
static scene = ActionSceneEnum.record;
|
|
17
|
+
static capabilityActionName = 'update';
|
|
18
|
+
|
|
19
|
+
defaultProps: ButtonProps = {
|
|
20
|
+
type: 'link',
|
|
21
|
+
title: tExpr('Disassociate'),
|
|
22
|
+
icon: 'DisconnectOutlined',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
getAclActionName() {
|
|
26
|
+
return 'update';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
DisassociateActionModel.define({
|
|
31
|
+
label: tExpr('Disassociate'),
|
|
32
|
+
sort: 65,
|
|
33
|
+
hide(ctx) {
|
|
34
|
+
return !isAssociationBlockContext(ctx);
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
DisassociateActionModel.registerFlow({
|
|
39
|
+
key: 'disassociateSettings',
|
|
40
|
+
title: tExpr('Disassociate settings'),
|
|
41
|
+
on: 'click',
|
|
42
|
+
steps: {
|
|
43
|
+
confirm: {
|
|
44
|
+
use: 'confirm',
|
|
45
|
+
defaultParams: {
|
|
46
|
+
enable: true,
|
|
47
|
+
title: tExpr('Disassociate record'),
|
|
48
|
+
content: tExpr('Are you sure you want to disassociate it?'),
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
disassociate: {
|
|
52
|
+
async handler(ctx) {
|
|
53
|
+
await applyDisassociateAction(ctx);
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
});
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { tExpr, MobilePopup, MultiRecordResource, useFlowSettingsContext } from '@nocobase/flow-engine';
|
|
11
|
+
import type { CollectionFieldInterfaceDataSourceManager } from '@nocobase/flow-engine';
|
|
11
12
|
import { isEmptyFilter, transformFilter } from '@nocobase/utils/client';
|
|
12
13
|
import { ButtonProps, Popover, Transfer } from 'antd';
|
|
13
14
|
import React from 'react';
|
|
@@ -15,6 +16,7 @@ import { FilterGroup, VariableFilterItem } from '../../components/filter';
|
|
|
15
16
|
import { ActionModel, CollectionBlockModel } from '../base';
|
|
16
17
|
import { FilterContainer } from '../../components/filter/FilterContainer';
|
|
17
18
|
import _ from 'lodash';
|
|
19
|
+
import { getFlowFieldInterfaceOptions } from '../../../flow-compat';
|
|
18
20
|
|
|
19
21
|
export class FilterActionModel extends ActionModel {
|
|
20
22
|
static scene = 'collection';
|
|
@@ -135,9 +137,11 @@ FilterActionModel.registerFlow({
|
|
|
135
137
|
'x-component': (props) => {
|
|
136
138
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
137
139
|
const { model } = useFlowSettingsContext();
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
140
|
+
const filterable = getFilterableFields(
|
|
141
|
+
model.context.blockModel.collection,
|
|
142
|
+
model.context.dataSourceManager,
|
|
143
|
+
model.context.app?.dataSourceManager,
|
|
144
|
+
);
|
|
141
145
|
const dataSource = filterable.map((field: any) => ({ key: field.name, title: field.title }));
|
|
142
146
|
return (
|
|
143
147
|
<Transfer
|
|
@@ -157,9 +161,11 @@ FilterActionModel.registerFlow({
|
|
|
157
161
|
},
|
|
158
162
|
defaultParams(ctx) {
|
|
159
163
|
// 默认仅包含“可筛选”的字段(与 1.0 一致),以避免 JSON 等未提供 operators 的字段出现在默认允许集合中
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
164
|
+
const names = getFilterableFields(
|
|
165
|
+
ctx.blockModel.collection,
|
|
166
|
+
ctx.model?.context?.dataSourceManager,
|
|
167
|
+
ctx.model?.context?.app?.dataSourceManager,
|
|
168
|
+
).map((field: any) => field.name);
|
|
163
169
|
return {
|
|
164
170
|
filterableFieldNames: names || [],
|
|
165
171
|
};
|
|
@@ -304,13 +310,15 @@ FilterActionModel.registerFlow({
|
|
|
304
310
|
},
|
|
305
311
|
});
|
|
306
312
|
|
|
307
|
-
function getFilterableFields(
|
|
313
|
+
function getFilterableFields(
|
|
314
|
+
collection: any,
|
|
315
|
+
...dataSourceManagers: Array<CollectionFieldInterfaceDataSourceManager | null | undefined>
|
|
316
|
+
) {
|
|
308
317
|
const fields = collection?.getFields?.() || [];
|
|
309
|
-
if (!fiMgr) return [];
|
|
310
318
|
return fields.filter((field: any) => {
|
|
311
319
|
if (!field?.interface) return false;
|
|
312
320
|
if (field?.filterable === false) return false;
|
|
313
|
-
const fi =
|
|
321
|
+
const fi = getFlowFieldInterfaceOptions(field.interface, ...dataSourceManagers);
|
|
314
322
|
return !!fi?.filterable;
|
|
315
323
|
});
|
|
316
324
|
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
11
|
+
import { BaseRecordResource, FlowEngine } from '@nocobase/flow-engine';
|
|
12
|
+
import {
|
|
13
|
+
applyDisassociateAction,
|
|
14
|
+
ActionModel,
|
|
15
|
+
applyAssociateAction,
|
|
16
|
+
AssociateActionModel,
|
|
17
|
+
CollectionActionGroupModel,
|
|
18
|
+
DisassociateActionModel,
|
|
19
|
+
getAssociationTargetResourceSettings,
|
|
20
|
+
PopupActionModel,
|
|
21
|
+
RecordActionGroupModel,
|
|
22
|
+
} from '../../..';
|
|
23
|
+
|
|
24
|
+
class TestAssociationResource<T = any> extends BaseRecordResource<T> {
|
|
25
|
+
async refresh(): Promise<void> {}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const createEngine = () => {
|
|
29
|
+
const engine = new FlowEngine();
|
|
30
|
+
engine.registerModels({
|
|
31
|
+
ActionModel,
|
|
32
|
+
AssociateActionModel,
|
|
33
|
+
DisassociateActionModel,
|
|
34
|
+
PopupActionModel,
|
|
35
|
+
CollectionActionGroupModel,
|
|
36
|
+
RecordActionGroupModel,
|
|
37
|
+
});
|
|
38
|
+
return engine;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const createContext = (engine: FlowEngine, resourceSettingsInit: any) => {
|
|
42
|
+
const collection = {
|
|
43
|
+
options: {
|
|
44
|
+
availableActions: ['list', 'update'],
|
|
45
|
+
},
|
|
46
|
+
getFilterByTK: vi.fn((record) => record.id),
|
|
47
|
+
};
|
|
48
|
+
return {
|
|
49
|
+
engine,
|
|
50
|
+
collection,
|
|
51
|
+
blockModel: {
|
|
52
|
+
collection,
|
|
53
|
+
getResourceSettingsInitParams: () => resourceSettingsInit,
|
|
54
|
+
},
|
|
55
|
+
} as any;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
describe('association action models', () => {
|
|
59
|
+
it('uses update permission for association operations', () => {
|
|
60
|
+
expect(AssociateActionModel.prototype.getAclActionName.call({})).toBe('update');
|
|
61
|
+
expect(DisassociateActionModel.prototype.getAclActionName.call({})).toBe('update');
|
|
62
|
+
expect(AssociateActionModel.capabilityActionName).toBe('update');
|
|
63
|
+
expect(DisassociateActionModel.capabilityActionName).toBe('update');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('uses association target collection for selector table blocks', () => {
|
|
67
|
+
const ctx: any = {
|
|
68
|
+
blockModel: {
|
|
69
|
+
association: {
|
|
70
|
+
target: 'transports',
|
|
71
|
+
targetCollection: {
|
|
72
|
+
name: 'transports',
|
|
73
|
+
dataSourceKey: 'main',
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
getResourceSettingsInitParams: () => ({
|
|
77
|
+
dataSourceKey: 'main',
|
|
78
|
+
collectionName: 'orders',
|
|
79
|
+
associationName: 'products.transports',
|
|
80
|
+
}),
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
expect(getAssociationTargetResourceSettings(ctx)).toEqual({
|
|
85
|
+
dataSourceKey: 'main',
|
|
86
|
+
collectionName: 'transports',
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('shows Associate only in collection actions of association blocks', async () => {
|
|
91
|
+
const engine = createEngine();
|
|
92
|
+
|
|
93
|
+
const relationItems = await CollectionActionGroupModel.defineChildren(
|
|
94
|
+
createContext(engine, {
|
|
95
|
+
dataSourceKey: 'main',
|
|
96
|
+
collectionName: 'orders',
|
|
97
|
+
associationName: 'products.o2m_orders',
|
|
98
|
+
}),
|
|
99
|
+
);
|
|
100
|
+
const normalItems = await CollectionActionGroupModel.defineChildren(
|
|
101
|
+
createContext(engine, {
|
|
102
|
+
dataSourceKey: 'main',
|
|
103
|
+
collectionName: 'orders',
|
|
104
|
+
}),
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
expect(relationItems.map((item: any) => item.useModel)).toContain('AssociateActionModel');
|
|
108
|
+
expect(relationItems.map((item: any) => item.useModel)).not.toContain('DisassociateActionModel');
|
|
109
|
+
expect(normalItems.map((item: any) => item.useModel)).not.toContain('AssociateActionModel');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('shows Disassociate only in record actions of association blocks', async () => {
|
|
113
|
+
const engine = createEngine();
|
|
114
|
+
|
|
115
|
+
const relationItems = await RecordActionGroupModel.defineChildren(
|
|
116
|
+
createContext(engine, {
|
|
117
|
+
dataSourceKey: 'main',
|
|
118
|
+
collectionName: 'orders',
|
|
119
|
+
associationName: 'products.o2m_orders',
|
|
120
|
+
}),
|
|
121
|
+
);
|
|
122
|
+
const normalItems = await RecordActionGroupModel.defineChildren(
|
|
123
|
+
createContext(engine, {
|
|
124
|
+
dataSourceKey: 'main',
|
|
125
|
+
collectionName: 'orders',
|
|
126
|
+
}),
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
expect(relationItems.map((item: any) => item.useModel)).toContain('DisassociateActionModel');
|
|
130
|
+
expect(relationItems.map((item: any) => item.useModel)).not.toContain('AssociateActionModel');
|
|
131
|
+
expect(normalItems.map((item: any) => item.useModel)).not.toContain('DisassociateActionModel');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('disassociates the current record through association resource remove action', async () => {
|
|
135
|
+
const resource = {
|
|
136
|
+
runAction: vi.fn(async () => ({})),
|
|
137
|
+
refresh: vi.fn(async () => {}),
|
|
138
|
+
};
|
|
139
|
+
const collection = {
|
|
140
|
+
getFilterByTK: vi.fn(() => 12),
|
|
141
|
+
};
|
|
142
|
+
const ctx: any = {
|
|
143
|
+
blockModel: {
|
|
144
|
+
collection,
|
|
145
|
+
resource,
|
|
146
|
+
getResourceSettingsInitParams: () => ({
|
|
147
|
+
dataSourceKey: 'main',
|
|
148
|
+
collectionName: 'orders',
|
|
149
|
+
associationName: 'products.o2m_orders',
|
|
150
|
+
}),
|
|
151
|
+
},
|
|
152
|
+
record: {
|
|
153
|
+
id: 12,
|
|
154
|
+
},
|
|
155
|
+
message: {
|
|
156
|
+
success: vi.fn(),
|
|
157
|
+
error: vi.fn(),
|
|
158
|
+
},
|
|
159
|
+
t: (value: string) => value,
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
await applyDisassociateAction(ctx);
|
|
163
|
+
|
|
164
|
+
expect(collection.getFilterByTK).toHaveBeenCalledWith(ctx.record);
|
|
165
|
+
expect(resource.runAction).toHaveBeenCalledWith('remove', {
|
|
166
|
+
data: [12],
|
|
167
|
+
});
|
|
168
|
+
expect(resource.refresh).toHaveBeenCalled();
|
|
169
|
+
expect(ctx.message.success).toHaveBeenCalledWith('Record disassociated successfully');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('associates selected records through association resource add action', async () => {
|
|
173
|
+
const resource = {
|
|
174
|
+
runAction: vi.fn(async () => ({})),
|
|
175
|
+
refresh: vi.fn(async () => {}),
|
|
176
|
+
};
|
|
177
|
+
const collection = {
|
|
178
|
+
getFilterByTK: vi.fn((record) => record.id),
|
|
179
|
+
};
|
|
180
|
+
const ctx: any = {
|
|
181
|
+
blockModel: {
|
|
182
|
+
collection,
|
|
183
|
+
resource,
|
|
184
|
+
getResourceSettingsInitParams: () => ({
|
|
185
|
+
dataSourceKey: 'main',
|
|
186
|
+
collectionName: 'orders',
|
|
187
|
+
associationName: 'products.o2m_orders',
|
|
188
|
+
}),
|
|
189
|
+
},
|
|
190
|
+
message: {
|
|
191
|
+
success: vi.fn(),
|
|
192
|
+
warning: vi.fn(),
|
|
193
|
+
error: vi.fn(),
|
|
194
|
+
},
|
|
195
|
+
t: (value: string) => value,
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
await applyAssociateAction(ctx, [{ id: 11 }, { id: 12 }]);
|
|
199
|
+
|
|
200
|
+
expect(collection.getFilterByTK).toHaveBeenCalledWith({ id: 11 });
|
|
201
|
+
expect(collection.getFilterByTK).toHaveBeenCalledWith({ id: 12 });
|
|
202
|
+
expect(resource.runAction).toHaveBeenCalledWith('add', {
|
|
203
|
+
data: [11, 12],
|
|
204
|
+
});
|
|
205
|
+
expect(resource.refresh).toHaveBeenCalled();
|
|
206
|
+
expect(ctx.message.success).toHaveBeenCalledWith('Record associated successfully');
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('uses nested association resource url when adding associated records', async () => {
|
|
210
|
+
const engine = createEngine();
|
|
211
|
+
const resource = engine.createResource(TestAssociationResource);
|
|
212
|
+
const api = {
|
|
213
|
+
request: vi.fn().mockResolvedValue({ data: { data: {} } }),
|
|
214
|
+
};
|
|
215
|
+
resource.setAPIClient(api as any);
|
|
216
|
+
resource.setResourceName('products.o2m_orders');
|
|
217
|
+
resource.setSourceId('362872646860800');
|
|
218
|
+
|
|
219
|
+
const ctx: any = {
|
|
220
|
+
blockModel: {
|
|
221
|
+
collection: {
|
|
222
|
+
getFilterByTK: vi.fn((record) => record.id),
|
|
223
|
+
},
|
|
224
|
+
resource,
|
|
225
|
+
getResourceSettingsInitParams: () => ({
|
|
226
|
+
dataSourceKey: 'main',
|
|
227
|
+
collectionName: 'orders',
|
|
228
|
+
associationName: 'products.o2m_orders',
|
|
229
|
+
sourceId: '362872646860800',
|
|
230
|
+
}),
|
|
231
|
+
},
|
|
232
|
+
message: {
|
|
233
|
+
success: vi.fn(),
|
|
234
|
+
warning: vi.fn(),
|
|
235
|
+
error: vi.fn(),
|
|
236
|
+
},
|
|
237
|
+
t: (value: string) => value,
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
await applyAssociateAction(ctx, [{ id: 11 }]);
|
|
241
|
+
|
|
242
|
+
expect(api.request).toHaveBeenCalledWith(
|
|
243
|
+
expect.objectContaining({
|
|
244
|
+
method: 'post',
|
|
245
|
+
url: 'products/362872646860800/o2m_orders:add',
|
|
246
|
+
data: [11],
|
|
247
|
+
}),
|
|
248
|
+
);
|
|
249
|
+
});
|
|
250
|
+
});
|
|
@@ -8,8 +8,11 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
export * from './AddNewActionModel';
|
|
11
|
+
export * from './AssociateActionModel';
|
|
12
|
+
export * from './AssociationActionUtils';
|
|
11
13
|
export * from './BulkDeleteActionModel';
|
|
12
14
|
export * from './DeleteActionModel';
|
|
15
|
+
export * from './DisassociateActionModel';
|
|
13
16
|
export * from './EditActionModel';
|
|
14
17
|
export * from './FilterActionModel';
|
|
15
18
|
export * from './JSActionModel';
|