@nocobase/client-v2 2.3.0-beta.11 → 2.3.0-beta.13

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 (29) hide show
  1. package/es/RouteRepository.d.ts +6 -0
  2. package/es/flow/components/filter/VariableFilterItem.d.ts +3 -1
  3. package/es/flow/internal/utils/quickEditDataScope.d.ts +48 -0
  4. package/es/flow/internal/utils/quickEditViewContainer.d.ts +27 -0
  5. package/es/flow/models/blocks/form/QuickEditFormModel.d.ts +1 -0
  6. package/es/index.mjs +63 -63
  7. package/lib/index.js +63 -63
  8. package/package.json +7 -7
  9. package/src/BaseApplication.tsx +2 -0
  10. package/src/RouteRepository.ts +14 -0
  11. package/src/__tests__/RouteRepository.test.ts +51 -0
  12. package/src/__tests__/app.test.tsx +13 -0
  13. package/src/flow/components/filter/VariableFilterItem.tsx +3 -1
  14. package/src/flow/internal/utils/__tests__/quickEditDataScope.test.ts +156 -0
  15. package/src/flow/internal/utils/__tests__/quickEditViewContainer.test.ts +52 -0
  16. package/src/flow/internal/utils/quickEditDataScope.tsx +177 -0
  17. package/src/flow/internal/utils/quickEditViewContainer.ts +46 -0
  18. package/src/flow/models/blocks/form/QuickEditFormModel.tsx +45 -11
  19. package/src/flow/models/blocks/form/__tests__/QuickEditFormModel.quickEdit.test.ts +120 -0
  20. package/src/flow/models/blocks/table/TableColumnModel.tsx +31 -0
  21. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +64 -0
  22. package/src/flow/models/fields/AssociationFieldModel/CascadeSelectFieldModel.tsx +7 -1
  23. package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +9 -0
  24. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +17 -10
  25. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +1 -0
  26. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableField.disabled.test.tsx +90 -0
  27. package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordSelectFieldModel.quickCreatePopup.test.tsx +183 -0
  28. package/src/flow/models/fields/SelectFieldModel.tsx +5 -0
  29. package/src/flow/models/fields/__tests__/SelectFieldModel.test.tsx +29 -1
@@ -0,0 +1,183 @@
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 {
11
+ FlowContext,
12
+ FlowEngine,
13
+ FlowEngineProvider,
14
+ FlowViewContextProvider,
15
+ type FlowModel,
16
+ } from '@nocobase/flow-engine';
17
+ import { cleanup, render, screen, waitFor } from '@testing-library/react';
18
+ import React from 'react';
19
+ import { afterEach, describe, expect, it, vi } from 'vitest';
20
+ import '../../../index';
21
+ import { BlockGridModel } from '../../../base/BlockGridModel';
22
+ import { RecordSelectFieldModel } from '../RecordSelectFieldModel';
23
+
24
+ type OpenViewHandler = (ctx: unknown, params: Record<string, unknown>) => void;
25
+
26
+ function getOpenViewHandler(): OpenViewHandler {
27
+ const flow = RecordSelectFieldModel.globalFlowRegistry.getFlow('popupSettings');
28
+ const handler = flow?.getStep('openView')?.serialize().handler;
29
+
30
+ if (!handler) {
31
+ throw new Error('popupSettings.openView handler is not registered');
32
+ }
33
+
34
+ return handler as OpenViewHandler;
35
+ }
36
+
37
+ function createContext(record: Record<string, unknown>) {
38
+ const open = vi.fn();
39
+ const sourceCollection = {
40
+ getFilterByTK: vi.fn((sourceRecord: Record<string, unknown>) => sourceRecord.id),
41
+ };
42
+ const model = {
43
+ uid: 'record-select-uid',
44
+ props: {
45
+ quickCreate: 'modalAdd',
46
+ allowMultiple: true,
47
+ },
48
+ context: {
49
+ inputArgs: {},
50
+ flowSettingsEnabled: false,
51
+ },
52
+ flowEngine: {
53
+ context: {
54
+ themeToken: {
55
+ colorBgLayout: '#fff',
56
+ },
57
+ },
58
+ },
59
+ };
60
+
61
+ return {
62
+ open,
63
+ context: {
64
+ inputArgs: { onChange: vi.fn() },
65
+ view: {
66
+ inputArgs: {
67
+ viewUid: 'approval-popup-uid',
68
+ },
69
+ },
70
+ viewer: { open },
71
+ layoutContentElement: {},
72
+ model,
73
+ collection: {
74
+ dataSourceKey: 'main',
75
+ filterTargetKey: 'id',
76
+ },
77
+ collectionField: {
78
+ type: 'hasMany',
79
+ target: 'orgs',
80
+ resourceName: 'users.orgs',
81
+ collection: sourceCollection,
82
+ },
83
+ record,
84
+ },
85
+ };
86
+ }
87
+
88
+ describe('RecordSelectFieldModel quick create popup', () => {
89
+ afterEach(() => {
90
+ cleanup();
91
+ vi.restoreAllMocks();
92
+ });
93
+
94
+ describe.each(['drawer', 'dialog'])('%s configuration mode', (mode) => {
95
+ it.each([
96
+ { scenario: 'configuration', modelEnabled: true, globalEnabled: false },
97
+ { scenario: 'global configuration', modelEnabled: true, globalEnabled: true },
98
+ { scenario: 'runtime', modelEnabled: false, globalEnabled: false },
99
+ { scenario: 'read-only configuration', modelEnabled: false, globalEnabled: true },
100
+ ])('uses the field configuration state in $scenario', async ({ modelEnabled, globalEnabled }) => {
101
+ const engine = new FlowEngine();
102
+ engine.registerModels({ BlockGridModel });
103
+ engine.flowSettings.enabled = globalEnabled;
104
+ engine.context.defineProperty('themeToken', {
105
+ value: { marginBlock: 16, marginSM: 8, paddingLG: 24 },
106
+ });
107
+ engine.context.defineProperty('t', { value: (key: string) => key });
108
+ const save = vi.fn(async (model: FlowModel) => model.serialize());
109
+ engine.setModelRepository({
110
+ findOne: vi.fn(async () => null),
111
+ save,
112
+ destroy: vi.fn(async () => true),
113
+ move: vi.fn(async () => undefined),
114
+ duplicate: vi.fn(async () => null),
115
+ });
116
+ const load = vi.spyOn(engine, 'loadOrCreateModel');
117
+ const { context, open } = createContext({});
118
+ context.model.context.flowSettingsEnabled = modelEnabled;
119
+ getOpenViewHandler()(context, { mode, size: 'medium' });
120
+ const config = open.mock.calls[0][0];
121
+ expect(config.inheritContext).toBe(false);
122
+
123
+ // Detached popups inherit the engine context, not the approval form's model context.
124
+ const popupContext = new FlowContext();
125
+ popupContext.addDelegate(engine.context);
126
+ popupContext.defineProperty('engine', { value: engine });
127
+ popupContext.defineProperty('view', {
128
+ value: { type: mode, inputArgs: config.inputArgs, Header: () => null, close: vi.fn() },
129
+ });
130
+ const { container } = render(
131
+ <FlowEngineProvider engine={engine}>
132
+ <FlowViewContextProvider context={popupContext}>{config.content()}</FlowViewContextProvider>
133
+ </FlowEngineProvider>,
134
+ );
135
+
136
+ await waitFor(() => expect(container.querySelector('.nb-block-grid')).toBeInTheDocument());
137
+ if (modelEnabled) {
138
+ expect(screen.getByRole('button', { name: /Add block/ })).toBeInTheDocument();
139
+ expect(save).toHaveBeenCalledOnce();
140
+ } else {
141
+ expect(screen.queryByRole('button', { name: /Add block/ })).not.toBeInTheDocument();
142
+ expect(save).not.toHaveBeenCalled();
143
+ }
144
+ expect(load).toHaveBeenCalledWith(
145
+ expect.objectContaining({ use: 'BlockGridModel', parentId: context.model.uid }),
146
+ { delegateToParent: false, delegate: popupContext, skipSave: !modelEnabled },
147
+ );
148
+ expect(engine.context.flowSettingsEnabled).toBe(globalEnabled);
149
+ });
150
+ });
151
+
152
+ it('passes the parent record reference when the parent record has a primary key', () => {
153
+ const { context, open } = createContext({ id: 1, nickname: 'Saved user' });
154
+
155
+ getOpenViewHandler()(context, { mode: 'drawer', size: 'medium' });
156
+
157
+ expect(open).toHaveBeenCalledOnce();
158
+ const { inputArgs } = open.mock.calls[0][0];
159
+ expect(inputArgs).toMatchObject({
160
+ scene: 'create',
161
+ collectionName: 'orgs',
162
+ associationName: 'users.orgs',
163
+ sourceId: 1,
164
+ });
165
+ expect(inputArgs).not.toHaveProperty('sourceAssociationName');
166
+ });
167
+
168
+ it('only passes sourceAssociationName when the parent record has no primary key yet', () => {
169
+ const { context, open } = createContext({});
170
+
171
+ getOpenViewHandler()(context, { mode: 'drawer', size: 'medium' });
172
+
173
+ expect(open).toHaveBeenCalledOnce();
174
+ const { inputArgs } = open.mock.calls[0][0];
175
+ expect(inputArgs).toMatchObject({
176
+ scene: 'create',
177
+ collectionName: 'orgs',
178
+ sourceAssociationName: 'users.orgs',
179
+ });
180
+ expect(inputArgs).not.toHaveProperty('associationName');
181
+ expect(inputArgs).not.toHaveProperty('sourceId');
182
+ });
183
+ });
@@ -13,6 +13,7 @@ import React from 'react';
13
13
  import { FieldModel } from '../base/FieldModel';
14
14
  import { MobileSelect } from './mobile-components/MobileSelect';
15
15
  import { enumToOptions, getSelectedEnumLabels, translateOptionLabel } from '../../internal/utils/enumOptionsUtils';
16
+ import { syncQuickEditPreventClose } from '../../internal/utils/quickEditViewContainer';
16
17
 
17
18
  const getOriginalEnumOptions = (model: SelectFieldModel) => {
18
19
  const fromEnum = enumToOptions(model.context.collectionField?.uiSchema?.enum, model.translate) || [];
@@ -62,6 +63,10 @@ export class SelectFieldModel extends FieldModel {
62
63
  this.props.onChange?.(nextValue?.value);
63
64
  }}
64
65
  options={options}
66
+ onDropdownVisibleChange={(open) => {
67
+ syncQuickEditPreventClose(this, open);
68
+ this.props.onDropdownVisibleChange?.(open);
69
+ }}
65
70
  labelRender={(item) => item.label}
66
71
  maxTagCount="responsive"
67
72
  maxTagPlaceholder={(omittedValues) => (
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import React from 'react';
11
- import { describe, expect, it } from 'vitest';
11
+ import { describe, expect, it, vi } from 'vitest';
12
12
  import { SelectFieldModel } from '../SelectFieldModel';
13
13
 
14
14
  function mockT(text: string) {
@@ -40,4 +40,32 @@ describe('SelectFieldModel', () => {
40
40
 
41
41
  expect(element.props.value).toEqual({ label: '是', value: true });
42
42
  });
43
+
44
+ it('keeps the quick edit popover open while the dropdown is open', () => {
45
+ vi.useFakeTimers();
46
+ try {
47
+ const update = vi.fn();
48
+ const onDropdownVisibleChange = vi.fn();
49
+ const model = {
50
+ props: { value: undefined, onDropdownVisibleChange },
51
+ parent: { use: 'QuickEditFormModel', viewContainer: { update } },
52
+ context: { collectionField: { uiSchema: { enum: [] } } },
53
+ translate: mockT,
54
+ } as unknown as SelectFieldModel;
55
+
56
+ const element = SelectFieldModel.prototype.render.call(model) as React.ReactElement;
57
+
58
+ element.props.onDropdownVisibleChange(true);
59
+ expect(update).toHaveBeenCalledWith({ preventClose: true });
60
+ expect(onDropdownVisibleChange).toHaveBeenCalledWith(true);
61
+
62
+ element.props.onDropdownVisibleChange(false);
63
+ expect(onDropdownVisibleChange).toHaveBeenCalledWith(false);
64
+ expect(update).toHaveBeenCalledTimes(1);
65
+ vi.runAllTimers();
66
+ expect(update).toHaveBeenLastCalledWith({ preventClose: false });
67
+ } finally {
68
+ vi.useRealTimers();
69
+ }
70
+ });
43
71
  });