@nocobase/client-v2 2.1.0-beta.32 → 2.1.0-beta.34
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/APIClient.d.ts +16 -0
- package/es/Application.d.ts +2 -1
- package/es/authRedirect.d.ts +9 -16
- package/es/components/form/EnvVariableInput.d.ts +8 -6
- package/es/components/form/VariableInput.d.ts +73 -0
- package/es/components/form/index.d.ts +1 -0
- package/es/components/form/table/RowOverlayPreview.d.ts +27 -0
- package/es/components/form/table/SelectionCell.d.ts +36 -0
- package/es/components/form/table/Table.d.ts +82 -0
- package/es/components/form/table/constants.d.ts +15 -0
- package/es/components/form/table/dnd/SortableRow.d.ts +40 -0
- package/es/components/form/table/dnd/index.d.ts +9 -0
- package/es/components/form/table/index.d.ts +9 -0
- package/es/components/form/table/styles.d.ts +41 -0
- package/es/components/form/table/utils.d.ts +44 -0
- package/es/components/index.d.ts +2 -0
- package/es/flow/components/TextAreaWithContextSelector.d.ts +15 -0
- package/es/flow/components/code-editor/index.d.ts +1 -0
- package/es/flow/models/blocks/shared/legacyDefaultValueMigrationBase.d.ts +1 -0
- package/es/flow/models/blocks/table/dragSort/dragSortComponents.d.ts +1 -6
- package/es/flow/models/blocks/table/dragSort/dragSortHooks.d.ts +5 -1
- package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +4 -0
- package/es/flow/models/fields/ClickableFieldModel.d.ts +3 -0
- package/es/flow/models/fields/DisplayEnumFieldModel.d.ts +6 -0
- package/es/flow/models/fields/DisplayTitleFieldModel.d.ts +2 -2
- package/es/flow-compat/passwordUtils.d.ts +1 -1
- package/es/index.d.ts +1 -0
- package/es/index.mjs +166 -99
- package/es/theme/globalStyles.d.ts +9 -0
- package/es/theme/index.d.ts +1 -0
- package/lib/index.js +171 -104
- package/package.json +10 -5
- package/src/APIClient.ts +68 -0
- package/src/Application.tsx +6 -2
- package/src/__tests__/authRedirect.test.ts +170 -64
- package/src/__tests__/globalDeps.test.ts +3 -0
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +6 -6
- package/src/authRedirect.ts +23 -84
- package/src/components/form/EnvVariableInput.tsx +11 -46
- package/src/components/form/VariableInput.tsx +177 -0
- package/src/components/form/__tests__/EnvVariableInput.test.tsx +175 -0
- package/src/components/form/index.tsx +1 -0
- package/src/components/form/table/RowOverlayPreview.tsx +51 -0
- package/src/components/form/table/SelectionCell.tsx +72 -0
- package/src/components/form/table/Table.tsx +279 -0
- package/src/components/form/table/__tests__/Table.pagination.test.tsx +80 -0
- package/src/components/form/table/constants.ts +16 -0
- package/src/components/form/table/dnd/SortableRow.tsx +106 -0
- package/src/components/form/table/dnd/index.ts +10 -0
- package/src/components/form/table/index.tsx +13 -0
- package/src/components/form/table/styles.ts +110 -0
- package/src/components/form/table/utils.ts +75 -0
- package/src/components/index.ts +2 -0
- package/src/flow/actions/__tests__/formAssignRules.legacyMigration.test.tsx +173 -0
- package/src/flow/actions/__tests__/pattern.test.ts +134 -0
- package/src/flow/actions/__tests__/titleField.test.ts +45 -0
- package/src/flow/actions/filterFormDefaultValues.tsx +30 -9
- package/src/flow/actions/formAssignRules.tsx +24 -9
- package/src/flow/actions/pattern.tsx +41 -6
- package/src/flow/actions/titleField.tsx +4 -2
- package/src/flow/actions/validation.tsx +1 -1
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +2 -4
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +13 -5
- package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.test.ts +111 -0
- package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.ts +2 -1
- package/src/flow/components/DynamicFlowsIcon.tsx +87 -13
- package/src/flow/components/TextAreaWithContextSelector.tsx +30 -6
- package/src/flow/components/__tests__/DynamicFlowsIcon.test.tsx +195 -8
- package/src/flow/components/code-editor/__tests__/useCodeRunner.test.tsx +81 -0
- package/src/flow/components/code-editor/hooks/useCodeRunner.ts +34 -2
- package/src/flow/components/code-editor/index.tsx +12 -8
- package/src/flow/models/base/PageModel/RootPageModel.tsx +1 -1
- package/src/flow/models/blocks/filter-form/__tests__/legacyDefaultValueMigration.test.ts +3 -0
- package/src/flow/models/blocks/form/FormActionModel.tsx +2 -8
- package/src/flow/models/blocks/form/FormItemModel.tsx +1 -1
- package/src/flow/models/blocks/form/__tests__/legacyDefaultValueMigration.test.ts +3 -0
- package/src/flow/models/blocks/shared/legacyDefaultValueMigrationBase.ts +21 -5
- package/src/flow/models/blocks/table/TableColumnModel.tsx +5 -2
- package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +36 -0
- package/src/flow/models/blocks/table/dragSort/dragSortComponents.tsx +1 -81
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +144 -3
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableColumnModel.rowRecord.test.ts +170 -1
- package/src/flow/models/fields/ClickableFieldModel.tsx +46 -2
- package/src/flow/models/fields/DisplayEnumFieldModel.tsx +44 -0
- package/src/flow/models/fields/DisplayTitleFieldModel.tsx +40 -15
- package/src/flow/models/fields/JSEditableFieldModel.tsx +107 -7
- package/src/flow/models/fields/__tests__/ClickableFieldModel.test.ts +180 -2
- package/src/flow/models/fields/__tests__/DisplayEnumFieldModel.test.tsx +39 -0
- package/src/flow/models/fields/__tests__/JSEditableFieldModel.test.tsx +97 -0
- package/src/index.ts +1 -0
- package/src/nocobase-buildin-plugin/index.tsx +4 -4
- package/src/theme/globalStyles.ts +21 -0
- package/src/theme/index.tsx +1 -0
- package/src/utils/globalDeps.ts +14 -1
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import React from 'react';
|
|
11
11
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
12
12
|
import { render, screen, userEvent, waitFor } from '@nocobase/test/client';
|
|
13
|
-
import { FlowEngine, FlowModel, GLOBAL_EMBED_CONTAINER_ID } from '@nocobase/flow-engine';
|
|
13
|
+
import { ActionScene, FlowEngine, FlowModel, GLOBAL_EMBED_CONTAINER_ID } from '@nocobase/flow-engine';
|
|
14
14
|
import { DynamicFlowsIcon } from '../DynamicFlowsIcon';
|
|
15
15
|
|
|
16
16
|
const mockState = vi.hoisted(() => ({
|
|
@@ -30,15 +30,31 @@ vi.mock('antd', async (importOriginal) => {
|
|
|
30
30
|
const actual = await importOriginal<typeof import('antd')>();
|
|
31
31
|
return {
|
|
32
32
|
...actual,
|
|
33
|
-
Button: ({ children, onClick, ...props }: any) => (
|
|
33
|
+
Button: ({ children, onClick, icon: _icon, loading: _loading, ...props }: any) => (
|
|
34
34
|
<button type="button" onClick={onClick} {...props}>
|
|
35
35
|
{children}
|
|
36
36
|
</button>
|
|
37
37
|
),
|
|
38
38
|
Collapse: ({ items }: any) => (
|
|
39
|
-
<div data-testid="collapse">
|
|
39
|
+
<div data-testid="collapse">
|
|
40
|
+
{items?.map((item: any) => (
|
|
41
|
+
<div key={item.key}>
|
|
42
|
+
{item.label}
|
|
43
|
+
{item.children}
|
|
44
|
+
</div>
|
|
45
|
+
))}
|
|
46
|
+
</div>
|
|
47
|
+
),
|
|
48
|
+
Dropdown: ({ children, menu }: any) => (
|
|
49
|
+
<div>
|
|
50
|
+
{children}
|
|
51
|
+
{menu?.items?.map((item: any) => (
|
|
52
|
+
<button key={item.key} type="button" onClick={item.onClick}>
|
|
53
|
+
{item.label}
|
|
54
|
+
</button>
|
|
55
|
+
))}
|
|
56
|
+
</div>
|
|
40
57
|
),
|
|
41
|
-
Dropdown: ({ children }: any) => <div>{children}</div>,
|
|
42
58
|
Empty: ({ description }: any) => <div>{description}</div>,
|
|
43
59
|
Input: (props: any) => <input {...props} />,
|
|
44
60
|
Select: (props: any) => {
|
|
@@ -70,10 +86,16 @@ const openDynamicFlowsEditor = async (model: FlowModel) => {
|
|
|
70
86
|
const embedCall = (model.context.viewer.embed as any).mock.calls.at(-1)?.[0];
|
|
71
87
|
expect(embedCall?.content).toBeTruthy();
|
|
72
88
|
|
|
73
|
-
render(embedCall.content);
|
|
89
|
+
const editor = render(embedCall.content);
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
embedCall,
|
|
93
|
+
view: mockState.flowContextValue.view,
|
|
94
|
+
editor,
|
|
95
|
+
};
|
|
74
96
|
};
|
|
75
97
|
|
|
76
|
-
const createModel = (options: { preventClose?: boolean; hiddenClose?: boolean } = {}) => {
|
|
98
|
+
const createModel = (options: { preventClose?: boolean; hiddenClose?: boolean; saveStepParams?: any } = {}) => {
|
|
77
99
|
const engine = new FlowEngine();
|
|
78
100
|
engine.translate = vi.fn((key: string) => key) as any;
|
|
79
101
|
engine.flowSettings.renderStepForm = vi.fn(() => null) as any;
|
|
@@ -95,6 +117,14 @@ const createModel = (options: { preventClose?: boolean; hiddenClose?: boolean }
|
|
|
95
117
|
handler: vi.fn(),
|
|
96
118
|
},
|
|
97
119
|
});
|
|
120
|
+
LocalTestModel.registerActions({
|
|
121
|
+
notify: {
|
|
122
|
+
name: 'notify',
|
|
123
|
+
title: 'Notify',
|
|
124
|
+
scene: ActionScene.DYNAMIC_EVENT_FLOW,
|
|
125
|
+
handler: vi.fn(),
|
|
126
|
+
},
|
|
127
|
+
});
|
|
98
128
|
|
|
99
129
|
const model = new LocalTestModel({
|
|
100
130
|
uid: `test-model-${Math.random().toString(36).slice(2, 8)}`,
|
|
@@ -122,17 +152,55 @@ const createModel = (options: { preventClose?: boolean; hiddenClose?: boolean }
|
|
|
122
152
|
destroy: vi.fn(),
|
|
123
153
|
},
|
|
124
154
|
});
|
|
155
|
+
model.context.defineProperty('message', {
|
|
156
|
+
value: {
|
|
157
|
+
error: vi.fn(),
|
|
158
|
+
success: vi.fn(),
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
vi.spyOn(model, 'saveStepParams').mockImplementation(options.saveStepParams || vi.fn(async () => undefined));
|
|
125
162
|
|
|
126
163
|
return model;
|
|
127
164
|
};
|
|
128
165
|
|
|
166
|
+
const createView = () => {
|
|
167
|
+
let destroyed = false;
|
|
168
|
+
let closingPromise: Promise<boolean | void> | undefined;
|
|
169
|
+
const view = {
|
|
170
|
+
close: vi.fn(function () {
|
|
171
|
+
if (destroyed) {
|
|
172
|
+
return Promise.resolve(true);
|
|
173
|
+
}
|
|
174
|
+
if (closingPromise) {
|
|
175
|
+
return closingPromise;
|
|
176
|
+
}
|
|
177
|
+
closingPromise = (async () => {
|
|
178
|
+
const allowed = await view.beforeClose?.({});
|
|
179
|
+
if (allowed === false) {
|
|
180
|
+
closingPromise = undefined;
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
view.destroy();
|
|
184
|
+
return true;
|
|
185
|
+
})();
|
|
186
|
+
return closingPromise;
|
|
187
|
+
}),
|
|
188
|
+
destroy: vi.fn(() => {
|
|
189
|
+
destroyed = true;
|
|
190
|
+
}),
|
|
191
|
+
beforeClose: undefined as any,
|
|
192
|
+
};
|
|
193
|
+
return view;
|
|
194
|
+
};
|
|
195
|
+
|
|
129
196
|
describe('DynamicFlowsIcon', () => {
|
|
130
197
|
beforeEach(() => {
|
|
131
198
|
mockState.capturedSelectProps.length = 0;
|
|
132
199
|
mockState.flowContextValue = {
|
|
133
|
-
|
|
134
|
-
|
|
200
|
+
modal: {
|
|
201
|
+
confirm: vi.fn(),
|
|
135
202
|
},
|
|
203
|
+
view: createView(),
|
|
136
204
|
};
|
|
137
205
|
document.body.innerHTML = `<div id="${GLOBAL_EMBED_CONTAINER_ID}"></div>`;
|
|
138
206
|
});
|
|
@@ -190,4 +258,123 @@ describe('DynamicFlowsIcon', () => {
|
|
|
190
258
|
expect(screen.getByTestId('collapse')).toBeInTheDocument();
|
|
191
259
|
});
|
|
192
260
|
});
|
|
261
|
+
|
|
262
|
+
it('keeps added event flows in draft until saved', async () => {
|
|
263
|
+
const model = createModel();
|
|
264
|
+
|
|
265
|
+
await openDynamicFlowsEditor(model);
|
|
266
|
+
await userEvent.click(screen.getByRole('button', { name: 'Add event flow' }));
|
|
267
|
+
|
|
268
|
+
expect(model.flowRegistry.getFlows().size).toBe(1);
|
|
269
|
+
expect(model.flowRegistry.hasFlow('flow1')).toBe(true);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('blocks cancel when draft has unsaved changes and confirmation is rejected', async () => {
|
|
273
|
+
const model = createModel();
|
|
274
|
+
mockState.flowContextValue.modal.confirm.mockResolvedValue(false);
|
|
275
|
+
const { view } = await openDynamicFlowsEditor(model);
|
|
276
|
+
|
|
277
|
+
await userEvent.click(screen.getByRole('button', { name: 'Add event flow' }));
|
|
278
|
+
await userEvent.click(screen.getByRole('button', { name: 'Cancel' }));
|
|
279
|
+
|
|
280
|
+
expect(mockState.flowContextValue.modal.confirm).toHaveBeenCalledWith({
|
|
281
|
+
title: 'Unsaved changes',
|
|
282
|
+
content: "Are you sure you don't want to save?",
|
|
283
|
+
okText: 'Confirm',
|
|
284
|
+
cancelText: 'Cancel',
|
|
285
|
+
});
|
|
286
|
+
expect(view.destroy).not.toHaveBeenCalled();
|
|
287
|
+
expect(model.flowRegistry.getFlows().size).toBe(1);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('runs only one unsaved confirmation while cancel is pending', async () => {
|
|
291
|
+
const model = createModel();
|
|
292
|
+
let resolveConfirm: (value: boolean) => void;
|
|
293
|
+
mockState.flowContextValue.modal.confirm.mockImplementation(
|
|
294
|
+
() => new Promise<boolean>((resolve) => (resolveConfirm = resolve)),
|
|
295
|
+
);
|
|
296
|
+
const { view } = await openDynamicFlowsEditor(model);
|
|
297
|
+
|
|
298
|
+
await userEvent.click(screen.getByRole('button', { name: 'Add event flow' }));
|
|
299
|
+
const cancelButton = screen.getByRole('button', { name: 'Cancel' });
|
|
300
|
+
await userEvent.click(cancelButton);
|
|
301
|
+
await userEvent.click(cancelButton);
|
|
302
|
+
|
|
303
|
+
expect(mockState.flowContextValue.modal.confirm).toHaveBeenCalledTimes(1);
|
|
304
|
+
|
|
305
|
+
resolveConfirm(true);
|
|
306
|
+
await waitFor(() => expect(view.destroy).toHaveBeenCalledTimes(1));
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('discards draft changes after confirmed cancel', async () => {
|
|
310
|
+
const model = createModel();
|
|
311
|
+
mockState.flowContextValue.modal.confirm.mockResolvedValue(true);
|
|
312
|
+
const { view } = await openDynamicFlowsEditor(model);
|
|
313
|
+
|
|
314
|
+
await userEvent.click(screen.getByRole('button', { name: 'Add event flow' }));
|
|
315
|
+
await userEvent.click(screen.getByRole('button', { name: 'Cancel' }));
|
|
316
|
+
|
|
317
|
+
expect(view.destroy).toHaveBeenCalledTimes(1);
|
|
318
|
+
expect(model.flowRegistry.getFlows().size).toBe(1);
|
|
319
|
+
expect(model.flowRegistry.hasFlow('flow1')).toBe(true);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it('discards existing event flow edits after confirmed cancel', async () => {
|
|
323
|
+
const model = createModel();
|
|
324
|
+
mockState.flowContextValue.modal.confirm.mockResolvedValue(true);
|
|
325
|
+
const { view } = await openDynamicFlowsEditor(model);
|
|
326
|
+
|
|
327
|
+
const titleInput = screen.getByPlaceholderText('Enter flow title');
|
|
328
|
+
await userEvent.clear(titleInput);
|
|
329
|
+
await userEvent.type(titleInput, 'Changed flow');
|
|
330
|
+
await userEvent.click(screen.getByRole('button', { name: 'Cancel' }));
|
|
331
|
+
|
|
332
|
+
expect(view.destroy).toHaveBeenCalledTimes(1);
|
|
333
|
+
expect(model.flowRegistry.getFlow('flow1')?.title).toBe('Event flow');
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it('discards added draft steps after confirmed cancel', async () => {
|
|
337
|
+
const model = createModel();
|
|
338
|
+
mockState.flowContextValue.modal.confirm.mockResolvedValue(true);
|
|
339
|
+
const { view } = await openDynamicFlowsEditor(model);
|
|
340
|
+
|
|
341
|
+
expect(model.flowRegistry.getFlow('flow1')?.getSteps().size).toBe(0);
|
|
342
|
+
|
|
343
|
+
await userEvent.click(screen.getByRole('button', { name: 'Notify' }));
|
|
344
|
+
await userEvent.click(screen.getByRole('button', { name: 'Cancel' }));
|
|
345
|
+
|
|
346
|
+
expect(view.destroy).toHaveBeenCalledTimes(1);
|
|
347
|
+
expect(model.flowRegistry.getFlow('flow1')?.getSteps().size).toBe(0);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it('does not keep discarded draft changes after reopening the editor', async () => {
|
|
351
|
+
const model = createModel();
|
|
352
|
+
mockState.flowContextValue.modal.confirm.mockResolvedValue(true);
|
|
353
|
+
|
|
354
|
+
const firstEditor = await openDynamicFlowsEditor(model);
|
|
355
|
+
await userEvent.click(screen.getByRole('button', { name: 'Add event flow' }));
|
|
356
|
+
await userEvent.click(screen.getByRole('button', { name: 'Cancel' }));
|
|
357
|
+
firstEditor.editor.unmount();
|
|
358
|
+
|
|
359
|
+
mockState.capturedSelectProps.length = 0;
|
|
360
|
+
mockState.flowContextValue.view = createView();
|
|
361
|
+
const secondEditor = await openDynamicFlowsEditor(model);
|
|
362
|
+
|
|
363
|
+
expect(secondEditor.editor.container.querySelectorAll('input[placeholder="Enter flow title"]')).toHaveLength(1);
|
|
364
|
+
expect(model.flowRegistry.getFlows().size).toBe(1);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it('saves draft event flows into the real registry', async () => {
|
|
368
|
+
const saveStepParams = vi.fn(async () => undefined);
|
|
369
|
+
const model = createModel({ saveStepParams });
|
|
370
|
+
const { view } = await openDynamicFlowsEditor(model);
|
|
371
|
+
|
|
372
|
+
await userEvent.click(screen.getByRole('button', { name: 'Add event flow' }));
|
|
373
|
+
await userEvent.click(screen.getByRole('button', { name: 'Save' }));
|
|
374
|
+
|
|
375
|
+
expect(saveStepParams).toHaveBeenCalledTimes(1);
|
|
376
|
+
expect(model.flowRegistry.getFlows().size).toBe(2);
|
|
377
|
+
expect(view.destroy).toHaveBeenCalledTimes(1);
|
|
378
|
+
expect(model.context.message.success).toHaveBeenCalledWith('Configuration saved');
|
|
379
|
+
});
|
|
193
380
|
});
|
|
@@ -14,6 +14,7 @@ import { render, waitFor } from '@testing-library/react';
|
|
|
14
14
|
import { App, ConfigProvider } from 'antd';
|
|
15
15
|
import { useCodeRunner } from '../hooks/useCodeRunner';
|
|
16
16
|
import {
|
|
17
|
+
FlowContext,
|
|
17
18
|
FlowEngine,
|
|
18
19
|
FlowModel,
|
|
19
20
|
FlowEngineProvider,
|
|
@@ -21,6 +22,7 @@ import {
|
|
|
21
22
|
ElementProxy,
|
|
22
23
|
createSafeWindow,
|
|
23
24
|
createSafeDocument,
|
|
25
|
+
createViewScopedEngine,
|
|
24
26
|
} from '@nocobase/flow-engine';
|
|
25
27
|
import { JSEditableFieldModel } from '../../../models/fields/JSEditableFieldModel';
|
|
26
28
|
|
|
@@ -31,6 +33,20 @@ class DummyJsAutoModel extends FlowModel {
|
|
|
31
33
|
}
|
|
32
34
|
}
|
|
33
35
|
|
|
36
|
+
function registerRunJsPreviewFlow(model: FlowModel) {
|
|
37
|
+
model.registerFlow('jsSettings', {
|
|
38
|
+
steps: {
|
|
39
|
+
runJs: {
|
|
40
|
+
useRawParams: true,
|
|
41
|
+
async handler(ctx) {
|
|
42
|
+
const code = ctx?.inputArgs?.preview?.code || '';
|
|
43
|
+
return ctx.runjs(code, undefined, { preprocessTemplates: true });
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
34
50
|
describe('useCodeRunner (beforeRender)', () => {
|
|
35
51
|
it('logs success and captures console output', async () => {
|
|
36
52
|
const engine = new FlowEngine();
|
|
@@ -192,6 +208,71 @@ describe('useCodeRunner (beforeRender)', () => {
|
|
|
192
208
|
});
|
|
193
209
|
});
|
|
194
210
|
|
|
211
|
+
it('keeps popup context when a top scoped engine has another model with the same uid', async () => {
|
|
212
|
+
const engine = new FlowEngine();
|
|
213
|
+
engine.registerModels({ DummyJsAutoModel });
|
|
214
|
+
const model = engine.createModel<DummyJsAutoModel>({ use: 'DummyJsAutoModel', uid: 'same-popup-uid' });
|
|
215
|
+
model.context.defineProperty('popup', {
|
|
216
|
+
value: {
|
|
217
|
+
uid: 'popup-view',
|
|
218
|
+
record: { username: 'alice' },
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
registerRunJsPreviewFlow(model);
|
|
222
|
+
|
|
223
|
+
const scopedEngine = createViewScopedEngine(engine);
|
|
224
|
+
const topModel = scopedEngine.createModel<DummyJsAutoModel>({ use: 'DummyJsAutoModel', uid: 'same-popup-uid' });
|
|
225
|
+
registerRunJsPreviewFlow(topModel);
|
|
226
|
+
|
|
227
|
+
const { result } = renderHook(() => useCodeRunner(model.context, 'v1'));
|
|
228
|
+
let runResult: any;
|
|
229
|
+
await act(async () => {
|
|
230
|
+
runResult = await result.current.run(`
|
|
231
|
+
const currentUsername = await ctx.getVar('ctx.popup.record.username');
|
|
232
|
+
console.log(currentUsername);
|
|
233
|
+
return currentUsername;
|
|
234
|
+
`);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
expect(runResult?.success).toBe(true);
|
|
238
|
+
expect(runResult?.value).toBe('alice');
|
|
239
|
+
expect(result.current.logs.some((l) => l.level === 'log' && l.msg.includes('alice'))).toBe(true);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('runs direct event-flow previews against the popup-bound model context when the settings view has no popup', async () => {
|
|
243
|
+
const engine = new FlowEngine();
|
|
244
|
+
engine.registerModels({ DummyJsAutoModel });
|
|
245
|
+
const model = engine.createModel<DummyJsAutoModel>({ use: 'DummyJsAutoModel', uid: 'direct-popup-uid' });
|
|
246
|
+
model.context.defineProperty('popup', {
|
|
247
|
+
value: {
|
|
248
|
+
uid: 'popup-view',
|
|
249
|
+
record: { username: 'alice' },
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
const scopedEngine = createViewScopedEngine(engine);
|
|
254
|
+
scopedEngine.createModel<DummyJsAutoModel>({ use: 'DummyJsAutoModel', uid: 'direct-popup-uid' });
|
|
255
|
+
|
|
256
|
+
const settingsCtx = new FlowContext();
|
|
257
|
+
settingsCtx.defineProperty('engine', { value: scopedEngine });
|
|
258
|
+
settingsCtx.defineProperty('popup', { value: undefined });
|
|
259
|
+
settingsCtx.addDelegate(model.context);
|
|
260
|
+
|
|
261
|
+
const { result } = renderHook(() => useCodeRunner(settingsCtx as any, 'v1'));
|
|
262
|
+
let runResult: any;
|
|
263
|
+
await act(async () => {
|
|
264
|
+
runResult = await result.current.run(`
|
|
265
|
+
const currentUsername = await ctx.getVar('ctx.popup.record.username');
|
|
266
|
+
console.log(currentUsername);
|
|
267
|
+
return currentUsername;
|
|
268
|
+
`);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
expect(runResult?.success).toBe(true);
|
|
272
|
+
expect(runResult?.value).toBe('alice');
|
|
273
|
+
expect(result.current.logs.some((l) => l.level === 'log' && l.msg.includes('alice'))).toBe(true);
|
|
274
|
+
});
|
|
275
|
+
|
|
195
276
|
it('compiles JSX in preview and renders antd Input without syntax error', async () => {
|
|
196
277
|
const engine = new FlowEngine();
|
|
197
278
|
engine.registerModels({ DummyJsAutoModel });
|
|
@@ -112,6 +112,31 @@ function createLoggerWrapperFactory(append: (level: RunLog['level'], args: any[]
|
|
|
112
112
|
return wrap;
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
function hasPopupViewMarkers(view: any): boolean {
|
|
116
|
+
const inputArgs = view?.inputArgs || {};
|
|
117
|
+
const openerUids = inputArgs?.openerUids;
|
|
118
|
+
const viewStack = view?.navigation?.viewStack;
|
|
119
|
+
|
|
120
|
+
return (Array.isArray(openerUids) && openerUids.length > 0) || (Array.isArray(viewStack) && viewStack.length >= 2);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function hasPreviewPopupContext(ctx: any): Promise<boolean> {
|
|
124
|
+
if (!ctx) return false;
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const popup = await ctx.popup;
|
|
128
|
+
if (popup) return true;
|
|
129
|
+
} catch (_) {
|
|
130
|
+
// ignore unavailable popup getters
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
return hasPopupViewMarkers(await ctx.view);
|
|
135
|
+
} catch (_) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
115
140
|
export function useCodeRunner(hostCtx: FlowModelContext, version = 'v1') {
|
|
116
141
|
const [logs, setLogs] = useState<RunLog[]>([]);
|
|
117
142
|
const [running, setRunning] = useState(false);
|
|
@@ -131,7 +156,14 @@ export function useCodeRunner(hostCtx: FlowModelContext, version = 'v1') {
|
|
|
131
156
|
const model = hostCtx?.model;
|
|
132
157
|
if (!model) throw new Error('No model in FlowContext');
|
|
133
158
|
const engine = hostCtx.engine;
|
|
134
|
-
const
|
|
159
|
+
const globalRuntimeModel = engine.getModel(model.uid, true) || model;
|
|
160
|
+
const [hostHasPopupContext, modelHasPopupContext] = await Promise.all([
|
|
161
|
+
hasPreviewPopupContext(hostCtx),
|
|
162
|
+
hasPreviewPopupContext(model.context),
|
|
163
|
+
]);
|
|
164
|
+
const shouldPreservePopupModel = hostHasPopupContext || modelHasPopupContext;
|
|
165
|
+
const runtimeModel = shouldPreservePopupModel ? model : globalRuntimeModel;
|
|
166
|
+
const directRunCtx = hostHasPopupContext ? hostCtx : modelHasPopupContext ? model.context : hostCtx;
|
|
135
167
|
|
|
136
168
|
const nativeConsole: Record<RunLog['level'], (...args: any[]) => void> = {
|
|
137
169
|
log: (...args) => console.log(...args),
|
|
@@ -255,7 +287,7 @@ export function useCodeRunner(hostCtx: FlowModelContext, version = 'v1') {
|
|
|
255
287
|
if (!flow) {
|
|
256
288
|
// 无可用流程(典型场景:联动规则里的 RunJS 预览),直接在当前上下文执行代码
|
|
257
289
|
const navigator = createSafeNavigator();
|
|
258
|
-
await
|
|
290
|
+
await directRunCtx.runjs(
|
|
259
291
|
code,
|
|
260
292
|
{ window: createSafeWindow({ navigator }), document: createSafeDocument(), navigator },
|
|
261
293
|
{ version },
|
|
@@ -42,6 +42,7 @@ interface CodeEditorProps {
|
|
|
42
42
|
language?: string;
|
|
43
43
|
scene?: string | string[];
|
|
44
44
|
RightExtra?: React.FC<any>;
|
|
45
|
+
showLogs?: boolean;
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
export * from './types';
|
|
@@ -64,6 +65,7 @@ export const CodeEditor: React.FC<CodeEditorProps> = ({
|
|
|
64
65
|
language,
|
|
65
66
|
scene,
|
|
66
67
|
RightExtra,
|
|
68
|
+
showLogs = true,
|
|
67
69
|
}) => {
|
|
68
70
|
const wrapperRef = useRef<HTMLDivElement>(null);
|
|
69
71
|
const viewRef = useRef<EditorView | null>(null);
|
|
@@ -249,14 +251,16 @@ export const CodeEditor: React.FC<CodeEditorProps> = ({
|
|
|
249
251
|
completionSource={completionSource}
|
|
250
252
|
viewRef={viewRef}
|
|
251
253
|
/>
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
254
|
+
{showLogs ? (
|
|
255
|
+
<LogsPanel
|
|
256
|
+
logs={logs}
|
|
257
|
+
onJumpTo={(line, column) => {
|
|
258
|
+
const view = viewRef.current;
|
|
259
|
+
if (view) jumpTo(view, line, column);
|
|
260
|
+
}}
|
|
261
|
+
tr={tr}
|
|
262
|
+
/>
|
|
263
|
+
) : null}
|
|
260
264
|
<SnippetsDrawer
|
|
261
265
|
open={snippetOpen}
|
|
262
266
|
onClose={() => setSnippetOpen(false)}
|
|
@@ -162,7 +162,7 @@ RootPageModel.registerFlow({
|
|
|
162
162
|
const route = ctx.routeRepository.getRouteBySchemaUid(ctx.model.parentId);
|
|
163
163
|
ctx.model.setProps('routeId', route?.id);
|
|
164
164
|
const routes: NocoBaseDesktopRoute[] = _.castArray(route?.children);
|
|
165
|
-
for (const route of routes.sort((a, b) => a.sort - b.sort)) {
|
|
165
|
+
for (const route of routes.filter(Boolean).sort((a, b) => (a.sort || 0) - (b.sort || 0))) {
|
|
166
166
|
// 过滤掉隐藏的路由
|
|
167
167
|
if (route.hideInMenu) {
|
|
168
168
|
continue;
|
|
@@ -18,6 +18,7 @@ function createMockFieldModel(options: { uid: string; props?: Record<string, any
|
|
|
18
18
|
const model: any = {
|
|
19
19
|
uid: options.uid,
|
|
20
20
|
props: { ...(options.props || {}) },
|
|
21
|
+
_options: { props: { ...(options.props || {}) } },
|
|
21
22
|
stepParams: { ...(options.stepParams || {}) },
|
|
22
23
|
emitter: { emit: vi.fn() },
|
|
23
24
|
setProps(patch: any) {
|
|
@@ -97,7 +98,9 @@ describe('filter-form legacyDefaultValueMigration', () => {
|
|
|
97
98
|
clearLegacyDefaultValuesFromFilterFormModel(filterFormModel);
|
|
98
99
|
|
|
99
100
|
expect(field1.props.initialValue).toBeUndefined();
|
|
101
|
+
expect(field1._options.props.initialValue).toBeUndefined();
|
|
100
102
|
expect(field1.props.keep).toBe(true);
|
|
103
|
+
expect(field1._options.props.keep).toBe(true);
|
|
101
104
|
expect(field1.stepParams.filterFormItemSettings?.initialValue).toBeUndefined();
|
|
102
105
|
expect(field1.stepParams.otherFlow?.s?.x).toBe(1);
|
|
103
106
|
|
|
@@ -94,8 +94,6 @@ FormSubmitActionModel.registerFlow({
|
|
|
94
94
|
ctx.model.setProps('loading', true);
|
|
95
95
|
const { submitHandler } = await import('./submitHandler');
|
|
96
96
|
await submitHandler(ctx, params);
|
|
97
|
-
ctx.message.success(ctx.t('Saved successfully'));
|
|
98
|
-
ctx.model.setProps('loading', false);
|
|
99
97
|
} catch (error) {
|
|
100
98
|
ctx.model.setProps('loading', false);
|
|
101
99
|
// 显示保存失败提示
|
|
@@ -107,12 +105,8 @@ FormSubmitActionModel.registerFlow({
|
|
|
107
105
|
}
|
|
108
106
|
},
|
|
109
107
|
},
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (ctx.view) {
|
|
113
|
-
ctx.view.close();
|
|
114
|
-
}
|
|
115
|
-
},
|
|
108
|
+
afterSuccess: {
|
|
109
|
+
use: 'afterSuccess',
|
|
116
110
|
},
|
|
117
111
|
},
|
|
118
112
|
});
|
|
@@ -431,7 +431,7 @@ FormItemModel.registerFlow({
|
|
|
431
431
|
},
|
|
432
432
|
defaultParams: (ctx: any) => {
|
|
433
433
|
const titleField =
|
|
434
|
-
ctx.model.props.titleField || ctx.model
|
|
434
|
+
ctx.model.props.titleField || ctx.model?.context?.collectionField?.targetCollectionTitleFieldName;
|
|
435
435
|
return {
|
|
436
436
|
titleField: titleField,
|
|
437
437
|
};
|
|
@@ -18,6 +18,7 @@ function createMockFieldModel(options: { uid: string; props?: Record<string, any
|
|
|
18
18
|
const model: any = {
|
|
19
19
|
uid: options.uid,
|
|
20
20
|
props: { ...(options.props || {}) },
|
|
21
|
+
_options: { props: { ...(options.props || {}) } },
|
|
21
22
|
stepParams: { ...(options.stepParams || {}) },
|
|
22
23
|
emitter: { emit: vi.fn() },
|
|
23
24
|
setProps(patch: any) {
|
|
@@ -124,7 +125,9 @@ describe('legacyDefaultValueMigration', () => {
|
|
|
124
125
|
|
|
125
126
|
// field1: props.initialValue removed, stepParams cleared for initialValue
|
|
126
127
|
expect(field1.props.initialValue).toBeUndefined();
|
|
128
|
+
expect(field1._options.props.initialValue).toBeUndefined();
|
|
127
129
|
expect(field1.props.keep).toBe(true);
|
|
130
|
+
expect(field1._options.props.keep).toBe(true);
|
|
128
131
|
expect(field1.stepParams.editItemSettings?.initialValue).toBeUndefined();
|
|
129
132
|
expect(field1.stepParams.otherFlow?.s?.x).toBe(1);
|
|
130
133
|
// field2: legacy flow cleared
|
|
@@ -23,6 +23,11 @@ export interface LegacyClearer {
|
|
|
23
23
|
(model: any): void;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
export function hasPersistedAssignRulesValue(model: any, flowKey: string, stepKey: string): boolean {
|
|
27
|
+
const params = model?.getStepParams?.(flowKey, stepKey);
|
|
28
|
+
return !!params && Object.prototype.hasOwnProperty.call(params, 'value');
|
|
29
|
+
}
|
|
30
|
+
|
|
26
31
|
function getPropsInitialValue(model: any): any | undefined {
|
|
27
32
|
if (!model) return undefined;
|
|
28
33
|
const props = typeof model.getProps === 'function' ? model.getProps() : model.props;
|
|
@@ -70,15 +75,26 @@ function deleteStepParams(model: any, flowKey: string, stepKey: string) {
|
|
|
70
75
|
model.emitter?.emit?.('onStepParamsChanged');
|
|
71
76
|
}
|
|
72
77
|
|
|
78
|
+
function deletePropsInitialValue(model: any) {
|
|
79
|
+
if (!model) return;
|
|
80
|
+
|
|
81
|
+
model.setProps?.({ initialValue: undefined });
|
|
82
|
+
|
|
83
|
+
if (model.props && Object.prototype.hasOwnProperty.call(model.props, 'initialValue')) {
|
|
84
|
+
delete model.props.initialValue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const optionsProps = model._options?.props;
|
|
88
|
+
if (optionsProps && Object.prototype.hasOwnProperty.call(optionsProps, 'initialValue')) {
|
|
89
|
+
delete optionsProps.initialValue;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
73
93
|
export function createLegacyClearer(flowKeys: string[]): LegacyClearer {
|
|
74
94
|
return (model: any): void => {
|
|
75
95
|
if (!model) return;
|
|
76
96
|
|
|
77
|
-
model
|
|
78
|
-
|
|
79
|
-
if (model.props && Object.prototype.hasOwnProperty.call(model.props, 'initialValue')) {
|
|
80
|
-
delete model.props.initialValue;
|
|
81
|
-
}
|
|
97
|
+
deletePropsInitialValue(model);
|
|
82
98
|
|
|
83
99
|
for (const flowKey of flowKeys) {
|
|
84
100
|
deleteStepParams(model, flowKey, 'initialValue');
|
|
@@ -449,6 +449,9 @@ TableColumnModel.registerFlow({
|
|
|
449
449
|
quickEdit: {
|
|
450
450
|
title: tExpr('Enable quick edit'),
|
|
451
451
|
uiMode: { type: 'switch', key: 'editable' },
|
|
452
|
+
hideInSettings(ctx) {
|
|
453
|
+
return !!ctx.model.associationPathName;
|
|
454
|
+
},
|
|
452
455
|
defaultParams(ctx) {
|
|
453
456
|
if (ctx.model.collectionField.readonly || ctx.model.associationPathName) {
|
|
454
457
|
return {
|
|
@@ -460,7 +463,7 @@ TableColumnModel.registerFlow({
|
|
|
460
463
|
};
|
|
461
464
|
},
|
|
462
465
|
handler(ctx, params) {
|
|
463
|
-
ctx.model.setProps('editable', params.editable);
|
|
466
|
+
ctx.model.setProps('editable', ctx.model.associationPathName ? false : params.editable);
|
|
464
467
|
},
|
|
465
468
|
},
|
|
466
469
|
model: {
|
|
@@ -541,7 +544,7 @@ TableColumnModel.registerFlow({
|
|
|
541
544
|
ctx.model.setProps(targetCollectionField.getComponentProps());
|
|
542
545
|
},
|
|
543
546
|
defaultParams: (ctx: any) => {
|
|
544
|
-
const titleField = ctx.model
|
|
547
|
+
const titleField = ctx.model?.context?.collectionField?.targetCollectionTitleFieldName;
|
|
545
548
|
return {
|
|
546
549
|
label: getSavedAssociationTitleField(ctx.model) || titleField,
|
|
547
550
|
};
|
|
@@ -12,6 +12,42 @@ import { describe, expect, it, vi } from 'vitest';
|
|
|
12
12
|
import { TableColumnModel } from '../TableColumnModel';
|
|
13
13
|
|
|
14
14
|
describe('TableColumnModel sorter settings', () => {
|
|
15
|
+
it('hides quick edit setting for relation path columns added from association groups', async () => {
|
|
16
|
+
const engine = new FlowEngine();
|
|
17
|
+
const model = new TableColumnModel({ uid: 'table-column-relation-path-quick-edit', flowEngine: engine } as any);
|
|
18
|
+
const quickEditStep = model.getFlow('tableColumnSettings')?.steps?.quickEdit as any;
|
|
19
|
+
|
|
20
|
+
const hidden = await quickEditStep.hideInSettings({
|
|
21
|
+
model: {
|
|
22
|
+
associationPathName: 'department',
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
expect(hidden).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('keeps quick edit disabled for relation path columns even when params enable it', () => {
|
|
30
|
+
const engine = new FlowEngine();
|
|
31
|
+
const model = new TableColumnModel({
|
|
32
|
+
uid: 'table-column-relation-path-disable-quick-edit',
|
|
33
|
+
flowEngine: engine,
|
|
34
|
+
} as any);
|
|
35
|
+
const quickEditStep = model.getFlow('tableColumnSettings')?.steps?.quickEdit as any;
|
|
36
|
+
const setProps = vi.fn();
|
|
37
|
+
|
|
38
|
+
quickEditStep.handler(
|
|
39
|
+
{
|
|
40
|
+
model: {
|
|
41
|
+
associationPathName: 'department',
|
|
42
|
+
setProps,
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
{ editable: true },
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
expect(setProps).toHaveBeenCalledWith('editable', false);
|
|
49
|
+
});
|
|
50
|
+
|
|
15
51
|
it('hides sortable setting for association fields', async () => {
|
|
16
52
|
const engine = new FlowEngine();
|
|
17
53
|
const model = new TableColumnModel({ uid: 'table-column-association-sorter', flowEngine: engine } as any);
|