@nocobase/client-v2 2.1.0-beta.24 → 2.1.0-beta.26

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.
Files changed (57) hide show
  1. package/es/BaseApplication.d.ts +1 -0
  2. package/es/flow/actions/dataScopeFilter.d.ts +9 -0
  3. package/es/flow/actions/linkageRulesFormValueRefresh.d.ts +10 -0
  4. package/es/flow/index.d.ts +1 -0
  5. package/es/flow/internal/utils/rebuildFieldSubModel.d.ts +2 -1
  6. package/es/flow/models/actions/AssociateActionModel.d.ts +19 -0
  7. package/es/flow/models/actions/AssociationActionUtils.d.ts +17 -0
  8. package/es/flow/models/actions/DisassociateActionModel.d.ts +16 -0
  9. package/es/flow/models/actions/index.d.ts +3 -0
  10. package/es/flow/models/base/GridModel.d.ts +3 -1
  11. package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +1 -0
  12. package/es/flow/models/fields/AssociationFieldModel/recordSelectSettingsUtils.d.ts +9 -0
  13. package/es/flow/models/fields/JSFieldModel.d.ts +5 -0
  14. package/es/index.d.ts +1 -0
  15. package/es/index.mjs +101 -101
  16. package/lib/index.js +99 -99
  17. package/package.json +6 -5
  18. package/src/BaseApplication.tsx +4 -0
  19. package/src/__tests__/globalDeps.test.ts +6 -0
  20. package/src/__tests__/remotePlugins.test.ts +27 -0
  21. package/src/flow/actions/__tests__/dataScopeFilter.test.ts +158 -0
  22. package/src/flow/actions/__tests__/linkageRules.formValueDrivenRefresh.test.ts +438 -0
  23. package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +42 -0
  24. package/src/flow/actions/dataScope.tsx +6 -4
  25. package/src/flow/actions/dataScopeFilter.ts +70 -0
  26. package/src/flow/actions/linkageRules.tsx +8 -1
  27. package/src/flow/actions/linkageRulesFormValueRefresh.ts +492 -0
  28. package/src/flow/actions/linkageRulesRefresh.tsx +4 -2
  29. package/src/flow/actions/setTargetDataScope.tsx +6 -5
  30. package/src/flow/index.ts +1 -0
  31. package/src/flow/internal/utils/__tests__/rebuildFieldSubModel.test.ts +77 -2
  32. package/src/flow/internal/utils/rebuildFieldSubModel.ts +21 -5
  33. package/src/flow/models/actions/AssociateActionModel.tsx +196 -0
  34. package/src/flow/models/actions/AssociationActionUtils.ts +90 -0
  35. package/src/flow/models/actions/DisassociateActionModel.tsx +57 -0
  36. package/src/flow/models/actions/__tests__/AssociationActionModel.test.ts +250 -0
  37. package/src/flow/models/actions/index.ts +3 -0
  38. package/src/flow/models/base/GridModel.tsx +21 -1
  39. package/src/flow/models/base/__tests__/GridModel.dragSnapshotContainer.test.ts +98 -0
  40. package/src/flow/models/blocks/details/DetailsItemModel.tsx +3 -0
  41. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +9 -5
  42. package/src/flow/models/blocks/filter-form/__tests__/FilterFormBlockModel.cleanup.test.ts +138 -0
  43. package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +22 -0
  44. package/src/flow/models/blocks/table/JSColumnModel.tsx +30 -2
  45. package/src/flow/models/blocks/table/TableBlockModel.tsx +8 -1
  46. package/src/flow/models/blocks/table/TableColumnModel.tsx +1 -0
  47. package/src/flow/models/blocks/table/__tests__/JSColumnModel.test.tsx +51 -0
  48. package/src/flow/models/blocks/table/__tests__/TableBlockModel.quickEditRefresh.test.ts +49 -0
  49. package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +5 -1
  50. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +21 -5
  51. package/src/flow/models/fields/AssociationFieldModel/recordSelectSettingsUtils.ts +20 -0
  52. package/src/flow/models/fields/JSFieldModel.tsx +54 -14
  53. package/src/flow/models/fields/mobile-components/MobileSelect.tsx +11 -3
  54. package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +235 -0
  55. package/src/index.ts +1 -0
  56. package/src/utils/globalDeps.ts +10 -0
  57. package/src/utils/requirejs.ts +1 -1
@@ -12,14 +12,13 @@ import {
12
12
  defineAction,
13
13
  FlowModel,
14
14
  MultiRecordResource,
15
- pruneFilter,
16
15
  useFlowContext,
17
16
  tExpr,
18
17
  } from '@nocobase/flow-engine';
19
- import { isEmptyFilter, transformFilter } from '@nocobase/utils/client';
20
- import _ from 'lodash';
18
+ import { isEmptyFilter } from '@nocobase/utils/client';
21
19
  import React from 'react';
22
20
  import { FilterGroup, VariableFilterItem } from '../components/filter';
21
+ import { normalizeDataScopeFilter } from './dataScopeFilter';
23
22
 
24
23
  export const setTargetDataScope = defineAction({
25
24
  name: 'setTargetDataScope',
@@ -62,8 +61,10 @@ export const setTargetDataScope = defineAction({
62
61
  filter: { logic: '$and', items: [] },
63
62
  };
64
63
  },
64
+ useRawParams: true,
65
65
  async handler(ctx, params) {
66
- const targetBlockUid = params.targetBlockUid;
66
+ const resolvedParams = await ctx.resolveJsonTemplate(params);
67
+ const targetBlockUid = resolvedParams.targetBlockUid;
67
68
  if (!targetBlockUid) {
68
69
  return;
69
70
  }
@@ -74,7 +75,7 @@ export const setTargetDataScope = defineAction({
74
75
  return;
75
76
  }
76
77
 
77
- const filter = pruneFilter(transformFilter(params.filter));
78
+ const filter = normalizeDataScopeFilter(params.filter, resolvedParams.filter);
78
79
 
79
80
  if (isEmptyFilter(filter)) {
80
81
  resource.removeFilterGroup(`setTargetDataScope_${ctx.model.uid}`);
package/src/flow/index.ts CHANGED
@@ -104,5 +104,6 @@ export * from './admin-shell/AdminLayoutRouteCoordinator';
104
104
  export * from '../settings-center';
105
105
  export { openViewFlow } from './flows/openViewFlow';
106
106
  export { editMarkdownFlow } from './flows/editMarkdownFlow';
107
+ export { resolveDynamicNamePath } from './models/blocks/form/value-runtime/path';
107
108
 
108
109
  export { TextAreaWithContextSelector } from './components/TextAreaWithContextSelector';
@@ -35,7 +35,7 @@ describe('rebuildFieldSubModel', () => {
35
35
  });
36
36
  });
37
37
 
38
- test('rebuilds field with same uid and updates binding use', async () => {
38
+ test('rebuilds field with same uid and direct target field model use', async () => {
39
39
  const staleClickHandler = () => null;
40
40
  const parent = engine.createModel<DummyParentModel>({
41
41
  use: DummyParentModel,
@@ -66,7 +66,8 @@ describe('rebuildFieldSubModel', () => {
66
66
 
67
67
  expect(rebuilt).toBeInstanceOf(DummyTargetFieldModel);
68
68
  expect(rebuilt.uid).toBe('field-1');
69
- expect(getFieldBindingUse(rebuilt)).toBe('DummyTargetFieldModel');
69
+ expect(getFieldBindingUse(rebuilt)).toBeUndefined();
70
+ expect(rebuilt.use).toBe('DummyTargetFieldModel');
70
71
  expect(rebuilt.props).toMatchObject({ added: 'yes', pattern: 'readPretty' });
71
72
  expect((rebuilt.props as any).onClick).toBeUndefined();
72
73
 
@@ -106,4 +107,78 @@ describe('rebuildFieldSubModel', () => {
106
107
  expect(Array.isArray(cols)).toBe(true);
107
108
  expect(cols.map((c) => c.uid)).toEqual(['col-1', 'col-2']);
108
109
  });
110
+
111
+ test('preserves compatible step params when rebuilding with the same field model use', async () => {
112
+ const parent = engine.createModel<DummyParentModel>({
113
+ use: DummyParentModel,
114
+ uid: 'parent-3',
115
+ subModels: {
116
+ field: {
117
+ use: DummyTargetFieldModel,
118
+ uid: 'field-3',
119
+ stepParams: {
120
+ fieldSettings: { init: { initKey: true } },
121
+ displayFieldSettings: {
122
+ overflowMode: {
123
+ overflowMode: true,
124
+ },
125
+ },
126
+ } as any,
127
+ },
128
+ },
129
+ });
130
+
131
+ await rebuildFieldSubModel({
132
+ parentModel: parent,
133
+ targetUse: 'DummyTargetFieldModel',
134
+ fieldSettingsInit: { fieldPath: 'title' },
135
+ });
136
+
137
+ const rebuilt = parent.subModels.field as DummyTargetFieldModel;
138
+ expect(rebuilt.stepParams).toEqual({
139
+ fieldSettings: {
140
+ init: { fieldPath: 'title' },
141
+ },
142
+ displayFieldSettings: {
143
+ overflowMode: {
144
+ overflowMode: true,
145
+ },
146
+ },
147
+ });
148
+ });
149
+
150
+ test('drops incompatible step params when rebuilding to a different field model use', async () => {
151
+ const parent = engine.createModel<DummyParentModel>({
152
+ use: DummyParentModel,
153
+ uid: 'parent-4',
154
+ subModels: {
155
+ field: {
156
+ use: FieldModel,
157
+ uid: 'field-4',
158
+ stepParams: {
159
+ fieldBinding: { use: 'FieldModel' },
160
+ fieldSettings: { init: { initKey: true } },
161
+ numberSettings: {
162
+ format: {
163
+ separator: '0,0.00',
164
+ },
165
+ },
166
+ } as any,
167
+ },
168
+ },
169
+ });
170
+
171
+ await rebuildFieldSubModel({
172
+ parentModel: parent,
173
+ targetUse: 'DummyTargetFieldModel',
174
+ fieldSettingsInit: { fieldPath: 'title' },
175
+ });
176
+
177
+ const rebuilt = parent.subModels.field as DummyTargetFieldModel;
178
+ expect(rebuilt.stepParams).toEqual({
179
+ fieldSettings: {
180
+ init: { fieldPath: 'title' },
181
+ },
182
+ });
183
+ });
109
184
  });
@@ -10,8 +10,9 @@
10
10
  /**
11
11
  * 通用的字段子模型重建工具:
12
12
  * - 保留原有 uid
13
- * - 通过 FieldModel 入口 + fieldBinding.use 动态选择目标字段类
13
+ * - 直接重建为目标字段类,保持与 defineChildren 初始创建逻辑一致
14
14
  * - 支持同步父项模式(pattern)
15
+ * - 同一字段模型类型下保留已有字段设置;切换到其他字段模型类型时丢弃不兼容设置
15
16
  * - 重建后触发 beforeRender(useCache: false)
16
17
  */
17
18
  import { FieldModel } from '../../models/base/FieldModel';
@@ -39,6 +40,16 @@ type RebuildOptions = {
39
40
  fieldSettingsInit?: unknown;
40
41
  };
41
42
 
43
+ function normalizeModelUse(value: unknown): string | undefined {
44
+ if (typeof value === 'string') {
45
+ return value;
46
+ }
47
+ if (typeof value === 'function' && value.name) {
48
+ return value.name;
49
+ }
50
+ return undefined;
51
+ }
52
+
42
53
  export function getFieldBindingUse(fieldModel?: FieldModel): string | undefined {
43
54
  const bindingUse = (fieldModel?.stepParams as FieldStepParams | undefined)?.fieldBinding?.use;
44
55
  return typeof bindingUse === 'string' ? bindingUse : undefined;
@@ -61,13 +72,18 @@ export async function rebuildFieldSubModel({
61
72
  delete prevSubModels[key];
62
73
  }
63
74
  }
64
- const prevStepParams: FieldStepParams = (fieldModel?.stepParams as FieldStepParams) || {};
75
+ const currentUse = normalizeModelUse(getFieldBindingUse(fieldModel) || fieldModel?.use);
76
+ const shouldPreserveStepParams = currentUse === targetUse;
77
+ const prevStepParams: FieldStepParams = shouldPreserveStepParams
78
+ ? (fieldModel?.stepParams as FieldStepParams) || {}
79
+ : {};
65
80
  const nextFieldSettingsInit = fieldSettingsInit ?? parentModel.getFieldSettingsInitParams?.();
81
+ const { fieldBinding: _fieldBinding, ...restStepParams } = prevStepParams;
66
82
 
67
83
  const nextStepParams: FieldStepParams = {
68
- ...prevStepParams,
69
- fieldBinding: { ...prevStepParams.fieldBinding, use: targetUse },
84
+ ...restStepParams,
70
85
  fieldSettings: {
86
+ ...(restStepParams.fieldSettings || {}),
71
87
  init: nextFieldSettingsInit,
72
88
  },
73
89
  };
@@ -81,7 +97,7 @@ export async function rebuildFieldSubModel({
81
97
 
82
98
  const subModel = parentModel.setSubModel('field', {
83
99
  uid: fieldUid,
84
- use: FieldModel,
100
+ use: targetUse,
85
101
  props: { ...(defaultProps || {}), ...(pattern ? { pattern } : {}) },
86
102
  stepParams: nextStepParams as StepParams,
87
103
  // Preserve existing subModels (e.g. SubTable columns) so switching field component back and forth
@@ -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
+ });