@nocobase/client-v2 2.1.39 → 2.1.40

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.
@@ -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 { define, observable } from '@formily/reactive';
11
+ import { describe, expect, it } from 'vitest';
12
+ import { FilterFormSubmitActionModel } from '../FilterFormSubmitActionModel';
13
+
14
+ describe('FilterFormSubmitActionModel lifecycle', () => {
15
+ const createAction = (blockModel: { autoTriggerFilter: boolean }, hidden = false, lifecycleAction?: object) => {
16
+ const action = Object.create(FilterFormSubmitActionModel.prototype) as FilterFormSubmitActionModel;
17
+
18
+ Object.defineProperty(action, 'props', {
19
+ value: {},
20
+ });
21
+ Object.defineProperty(action, 'context', {
22
+ value: { blockModel, model: lifecycleAction || action, defineProperty: () => undefined },
23
+ });
24
+ action.hidden = hidden;
25
+ define(action, {
26
+ hidden: observable,
27
+ });
28
+ action.onInit({});
29
+
30
+ return action;
31
+ };
32
+
33
+ it('keeps automatic filtering disabled when the submit action remounts', () => {
34
+ const blockModel = { autoTriggerFilter: true };
35
+ const action = createAction(blockModel);
36
+
37
+ action.onMount();
38
+ expect(blockModel.autoTriggerFilter).toBe(false);
39
+
40
+ action.onUnmount();
41
+ expect(blockModel.autoTriggerFilter).toBe(true);
42
+
43
+ action.onMount();
44
+ expect(blockModel.autoTriggerFilter).toBe(false);
45
+ });
46
+
47
+ it('keeps automatic filtering disabled until every mounted submit action unmounts', () => {
48
+ const blockModel = { autoTriggerFilter: true };
49
+ const firstAction = createAction(blockModel);
50
+ const secondAction = createAction(blockModel);
51
+
52
+ firstAction.onMount();
53
+ secondAction.onMount();
54
+ firstAction.onUnmount();
55
+ expect(blockModel.autoTriggerFilter).toBe(false);
56
+
57
+ secondAction.onUnmount();
58
+ expect(blockModel.autoTriggerFilter).toBe(true);
59
+ });
60
+
61
+ it('only disables automatic filtering while a visible submit action is mounted', () => {
62
+ const blockModel = { autoTriggerFilter: true };
63
+ const action = createAction(blockModel, true);
64
+
65
+ action.onMount();
66
+ expect(blockModel.autoTriggerFilter).toBe(true);
67
+
68
+ action.hidden = false;
69
+ expect(blockModel.autoTriggerFilter).toBe(false);
70
+
71
+ action.hidden = true;
72
+ expect(blockModel.autoTriggerFilter).toBe(true);
73
+ });
74
+
75
+ it('tracks fork lifecycles independently when submit actions share the same uid', () => {
76
+ const blockModel = { autoTriggerFilter: true };
77
+ const firstFork = {};
78
+ const secondFork = {};
79
+ const firstAction = createAction(blockModel, false, firstFork);
80
+ const secondAction = createAction(blockModel, false, secondFork);
81
+
82
+ firstAction.onMount();
83
+ secondAction.onMount();
84
+ firstAction.onUnmount();
85
+ expect(blockModel.autoTriggerFilter).toBe(false);
86
+
87
+ secondAction.onUnmount();
88
+ expect(blockModel.autoTriggerFilter).toBe(true);
89
+ });
90
+ });
@@ -8,7 +8,13 @@
8
8
  */
9
9
 
10
10
  import { Cascader, Space, Button } from 'antd';
11
- import { CollectionField, EditableItemModel, tExpr, MultiRecordResource } from '@nocobase/flow-engine';
11
+ import {
12
+ CollectionField,
13
+ EditableItemModel,
14
+ FilterableItemModel,
15
+ MultiRecordResource,
16
+ tExpr,
17
+ } from '@nocobase/flow-engine';
12
18
  import { DeleteOutlined } from '@ant-design/icons';
13
19
  import { css, cx } from '@emotion/css';
14
20
  import { debounce, castArray, omit, last, isEqual } from 'lodash';
@@ -31,6 +37,23 @@ function normalizeLabelKeys(labelKeyOrKeys?: string | string[]): string[] {
31
37
  return Array.from(new Set(list.filter((item): item is string => !!item && typeof item === 'string')));
32
38
  }
33
39
 
40
+ function normalizeCascadeFieldKey(key: unknown): string | undefined {
41
+ if (typeof key === 'string' && key) {
42
+ return key;
43
+ }
44
+ if (Array.isArray(key) && key.length === 1 && typeof key[0] === 'string') {
45
+ return key[0];
46
+ }
47
+ return undefined;
48
+ }
49
+
50
+ function getDefaultCascadeFieldNames(field: CollectionField) {
51
+ const targetCollection = field?.targetCollection;
52
+ const value = normalizeCascadeFieldKey(targetCollection?.filterTargetKey) || 'id';
53
+ const label = targetCollection?.titleCollectionField?.name || value;
54
+ return { label, value };
55
+ }
56
+
34
57
  function getRecordDisplayLabel(item: any, labelKeyOrKeys?: string | string[]): string | null {
35
58
  if (!isPlainRecord(item)) return null;
36
59
  const labelKeys = normalizeLabelKeys(labelKeyOrKeys);
@@ -815,6 +838,15 @@ EditableItemModel.bindModelToInterface('CascadeSelectFieldModel', ['m2o', 'o2o',
815
838
  order: 60,
816
839
  });
817
840
 
841
+ FilterableItemModel.bindModelToInterface('CascadeSelectFieldModel', ['m2o', 'o2o', 'oho', 'obo'], {
842
+ when: (ctx, field) => field.targetCollection?.template === 'tree',
843
+ isDefault: true,
844
+ order: 60,
845
+ defaultProps: (ctx, field) => ({
846
+ fieldNames: getDefaultCascadeFieldNames(field),
847
+ }),
848
+ });
849
+
818
850
  EditableItemModel.bindModelToInterface('CascadeSelectListFieldModel', ['m2m', 'o2m', 'mbm'], {
819
851
  when: (ctx, field) => field.targetCollection?.template === 'tree',
820
852
  isDefault: true,
@@ -206,6 +206,11 @@ export function MobileLazySelect(props: Readonly<LazySelectProps>) {
206
206
  return;
207
207
  }
208
208
  const selectedId = vals[0];
209
+ if (selectedId === undefined) {
210
+ onChange(undefined as unknown as AssociationOption);
211
+ handleClose();
212
+ return;
213
+ }
209
214
  const record = optionMap.get(selectedId);
210
215
  if (record) {
211
216
  if (valueMode === 'value') {
@@ -307,6 +307,22 @@ describe('MobileLazySelect', () => {
307
307
  expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument();
308
308
  });
309
309
 
310
+ it('clears the selected relation record when it is tapped again in single mode', () => {
311
+ const { onChange } = renderMobileLazySelect({
312
+ value: RELATION_OPTIONS[0],
313
+ multiple: false,
314
+ });
315
+
316
+ openLazyPopup();
317
+ expect(mockState.checklistProps?.value).toEqual([RELATION_OPTIONS[0].uuid]);
318
+
319
+ selectValues([]);
320
+
321
+ expect(onChange).toHaveBeenCalledTimes(1);
322
+ expect(onChange).toHaveBeenCalledWith(undefined);
323
+ expect(screen.queryByTestId('popup')).not.toBeInTheDocument();
324
+ });
325
+
310
326
  it('keeps pending relation records selected until confirm', () => {
311
327
  const { onChange, rerender } = renderMobileLazySelect();
312
328