@nocobase/client-v2 2.1.0-beta.33 → 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/models/blocks/table/dragSort/dragSortComponents.d.ts +1 -6
- package/es/flow/models/blocks/table/dragSort/dragSortHooks.d.ts +5 -1
- package/es/flow-compat/passwordUtils.d.ts +1 -1
- package/es/index.d.ts +1 -0
- package/es/index.mjs +145 -78
- package/es/theme/globalStyles.d.ts +9 -0
- package/es/theme/index.d.ts +1 -0
- package/lib/index.js +161 -94
- package/package.json +8 -6
- 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 +2 -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/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +2 -0
- 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/TextAreaWithContextSelector.tsx +30 -6
- 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/models/blocks/table/dragSort/dragSortComponents.tsx +1 -81
- package/src/flow/models/fields/JSEditableFieldModel.tsx +107 -7
- 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 +5 -1
|
@@ -0,0 +1,75 @@
|
|
|
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 React from 'react';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Same shape as antd Table's `rowKey` prop — either a record key name or a
|
|
14
|
+
* function. Hoisted here so utilities and `Table.tsx` agree on the contract.
|
|
15
|
+
*/
|
|
16
|
+
export type RowKey<RecordType extends object> =
|
|
17
|
+
| (keyof RecordType & (string | number))
|
|
18
|
+
| ((record: RecordType, index?: number) => React.Key);
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Read a stable row id off a record. Mirrors antd Table's rowKey resolution
|
|
22
|
+
* but normalises non-primitive ids to strings so `data-row-key` attributes
|
|
23
|
+
* and `useSortable({ id })` agree on equality.
|
|
24
|
+
*/
|
|
25
|
+
export function readRowKey<RecordType extends object>(
|
|
26
|
+
record: RecordType,
|
|
27
|
+
rowKey: RowKey<RecordType>,
|
|
28
|
+
index?: number,
|
|
29
|
+
): React.Key | undefined {
|
|
30
|
+
if (typeof rowKey === 'function') {
|
|
31
|
+
return rowKey(record, index);
|
|
32
|
+
}
|
|
33
|
+
const value = record[rowKey] as unknown;
|
|
34
|
+
if (value == null) return undefined;
|
|
35
|
+
return typeof value === 'string' || typeof value === 'number' ? value : String(value);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Pixel-perfect snapshot of a rendered `<tr>` for the drag overlay clone.
|
|
40
|
+
* Contains everything needed to rebuild a visually identical floating row
|
|
41
|
+
* without re-running antd's column layout pass.
|
|
42
|
+
*/
|
|
43
|
+
export type RowSnapshot = {
|
|
44
|
+
/** outerHTML of the source `<tr>`, captured at drag start. */
|
|
45
|
+
html: string;
|
|
46
|
+
/** Per-cell pixel widths (in DOM order) so the clone can fix them via `<col>`. */
|
|
47
|
+
cellWidths: number[];
|
|
48
|
+
/** Total row width — used as the wrapper width so the clone matches source horizontally. */
|
|
49
|
+
totalWidth: number;
|
|
50
|
+
/** Total row height — applied to the clone so cell padding matches the source row exactly. */
|
|
51
|
+
totalHeight: number;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Snapshot the source `<tr>` so the drag overlay can render a pixel-accurate
|
|
56
|
+
* floating clone. We can't reliably recompute the layout from `columns` alone
|
|
57
|
+
* — antd auto-sizes columns at runtime based on content + the surrounding
|
|
58
|
+
* container — so we read the rendered widths off the DOM at drag start. The
|
|
59
|
+
* row height is captured too because antd's cell padding rules are scoped to
|
|
60
|
+
* a selector chain we strip in the clone.
|
|
61
|
+
*/
|
|
62
|
+
export function snapshotSourceRow(rowKey: string): RowSnapshot | null {
|
|
63
|
+
if (typeof document === 'undefined') return null;
|
|
64
|
+
// `CSS.escape` is in lib.dom and shipped in every browser we target; the
|
|
65
|
+
// guard is purely a belt-and-suspenders against exotic test environments
|
|
66
|
+
// where `window.CSS` may be absent.
|
|
67
|
+
const cssGlobal: { escape?: (value: string) => string } | undefined =
|
|
68
|
+
typeof window !== 'undefined' ? window.CSS : undefined;
|
|
69
|
+
const escaped = cssGlobal?.escape ? cssGlobal.escape(rowKey) : rowKey;
|
|
70
|
+
const sourceRow = document.querySelector(`tr[data-row-key="${escaped}"]`) as HTMLTableRowElement | null;
|
|
71
|
+
if (!sourceRow) return null;
|
|
72
|
+
const cellWidths = Array.from(sourceRow.cells).map((cell) => cell.getBoundingClientRect().width);
|
|
73
|
+
const rect = sourceRow.getBoundingClientRect();
|
|
74
|
+
return { html: sourceRow.outerHTML, cellWidths, totalWidth: rect.width, totalHeight: rect.height };
|
|
75
|
+
}
|
package/src/components/index.ts
CHANGED
|
@@ -681,6 +681,7 @@ export class AdminLayoutMenuItemModel extends FlowModel<AdminLayoutMenuItemStruc
|
|
|
681
681
|
AdminLayoutMenuItemModel.registerFlow({
|
|
682
682
|
key: 'menuCreation',
|
|
683
683
|
title: 'Add menu item',
|
|
684
|
+
manual: true,
|
|
684
685
|
steps: {
|
|
685
686
|
basic: {
|
|
686
687
|
title: 'Add menu item',
|
|
@@ -697,6 +698,7 @@ AdminLayoutMenuItemModel.registerFlow({
|
|
|
697
698
|
AdminLayoutMenuItemModel.registerFlow({
|
|
698
699
|
key: 'menuSettings',
|
|
699
700
|
title: 'Menu settings',
|
|
701
|
+
manual: true,
|
|
700
702
|
steps: {
|
|
701
703
|
edit: {
|
|
702
704
|
title: 'Edit',
|
|
@@ -249,4 +249,115 @@ describe('resolveAdminRouteRuntimeTarget', () => {
|
|
|
249
249
|
expect(toRouterNavigationPath('/nocobase/v2/admin/page-1', '/nocobase/v2')).toBe('/admin/page-1');
|
|
250
250
|
expect(toRouterNavigationPath('/admin/page-1', '/nocobase/v2')).toBe('/admin/page-1');
|
|
251
251
|
});
|
|
252
|
+
|
|
253
|
+
describe('v2 sub-app context (router basename contains /apps/<id>/)', () => {
|
|
254
|
+
const subApp = {
|
|
255
|
+
getPublicPath: () => '/nocobase/v2/',
|
|
256
|
+
router: {
|
|
257
|
+
getBasename: () => '/nocobase/v2/apps/test-app/',
|
|
258
|
+
},
|
|
259
|
+
} as any;
|
|
260
|
+
|
|
261
|
+
it('should resolve flowPage runtime path under sub-app basename', () => {
|
|
262
|
+
expect(
|
|
263
|
+
resolveAdminRouteRuntimeTarget({
|
|
264
|
+
app: subApp,
|
|
265
|
+
route: {
|
|
266
|
+
type: NocoBaseDesktopRouteType.flowPage,
|
|
267
|
+
schemaUid: 'fp1',
|
|
268
|
+
},
|
|
269
|
+
}),
|
|
270
|
+
).toEqual({
|
|
271
|
+
runtimePath: '/nocobase/v2/apps/test-app/admin/fp1',
|
|
272
|
+
navigationMode: 'spa',
|
|
273
|
+
isLegacy: false,
|
|
274
|
+
reason: 'ok',
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('should resolve group DFS to first flowPage under sub-app basename', () => {
|
|
279
|
+
const route: NocoBaseDesktopRoute = {
|
|
280
|
+
type: NocoBaseDesktopRouteType.group,
|
|
281
|
+
children: [
|
|
282
|
+
{
|
|
283
|
+
type: NocoBaseDesktopRouteType.tabs,
|
|
284
|
+
schemaUid: 'tabs-1',
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
type: NocoBaseDesktopRouteType.group,
|
|
288
|
+
children: [
|
|
289
|
+
{
|
|
290
|
+
type: NocoBaseDesktopRouteType.page,
|
|
291
|
+
schemaUid: 'legacy-2',
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
type: NocoBaseDesktopRouteType.flowPage,
|
|
295
|
+
schemaUid: 'nested-fp',
|
|
296
|
+
},
|
|
297
|
+
],
|
|
298
|
+
},
|
|
299
|
+
],
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
expect(resolveAdminRouteRuntimeTarget({ app: subApp, route })).toEqual({
|
|
303
|
+
runtimePath: '/nocobase/v2/apps/test-app/admin/nested-fp',
|
|
304
|
+
navigationMode: 'spa',
|
|
305
|
+
isLegacy: false,
|
|
306
|
+
reason: 'ok',
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('should strip sub-app basename when converting to router internal path', () => {
|
|
311
|
+
expect(toRouterNavigationPath('/nocobase/v2/apps/test-app/admin/page-1', '/nocobase/v2/apps/test-app')).toBe(
|
|
312
|
+
'/admin/page-1',
|
|
313
|
+
);
|
|
314
|
+
expect(toRouterNavigationPath('/nocobase/v2/apps/test-app/admin/page-1', '/nocobase/v2/apps/test-app/')).toBe(
|
|
315
|
+
'/admin/page-1',
|
|
316
|
+
);
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
describe('fallback when router basename is missing', () => {
|
|
321
|
+
it('should fall back to publicPath when router is undefined', () => {
|
|
322
|
+
const appNoRouter = {
|
|
323
|
+
getPublicPath: () => '/nocobase/v2/',
|
|
324
|
+
router: undefined,
|
|
325
|
+
} as any;
|
|
326
|
+
|
|
327
|
+
expect(
|
|
328
|
+
resolveAdminRouteRuntimeTarget({
|
|
329
|
+
app: appNoRouter,
|
|
330
|
+
route: {
|
|
331
|
+
type: NocoBaseDesktopRouteType.flowPage,
|
|
332
|
+
schemaUid: 'fp1',
|
|
333
|
+
},
|
|
334
|
+
}),
|
|
335
|
+
).toMatchObject({
|
|
336
|
+
runtimePath: '/nocobase/v2/admin/fp1',
|
|
337
|
+
reason: 'ok',
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it('should fall back to publicPath when getBasename returns undefined', () => {
|
|
342
|
+
const appNoBasename = {
|
|
343
|
+
getPublicPath: () => '/nocobase/v2/',
|
|
344
|
+
router: {
|
|
345
|
+
getBasename: () => undefined,
|
|
346
|
+
},
|
|
347
|
+
} as any;
|
|
348
|
+
|
|
349
|
+
expect(
|
|
350
|
+
resolveAdminRouteRuntimeTarget({
|
|
351
|
+
app: appNoBasename,
|
|
352
|
+
route: {
|
|
353
|
+
type: NocoBaseDesktopRouteType.flowPage,
|
|
354
|
+
schemaUid: 'fp1',
|
|
355
|
+
},
|
|
356
|
+
}),
|
|
357
|
+
).toMatchObject({
|
|
358
|
+
runtimePath: '/nocobase/v2/admin/fp1',
|
|
359
|
+
reason: 'ok',
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
});
|
|
252
363
|
});
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { getV2EffectiveBasePath } from '../../../authRedirect';
|
|
10
11
|
import type { BaseApplication } from '../../../BaseApplication';
|
|
11
12
|
import { NocoBaseDesktopRouteType, type NocoBaseDesktopRoute } from '../../../flow-compat';
|
|
12
13
|
|
|
@@ -106,7 +107,7 @@ function joinRootRelativePath(basePath: string, pathname: string) {
|
|
|
106
107
|
}
|
|
107
108
|
|
|
108
109
|
function getV2AdminPath(app: ResolveAdminRouteRuntimeTargetOptions['app'], schemaUid: string) {
|
|
109
|
-
return joinRootRelativePath(app
|
|
110
|
+
return joinRootRelativePath(getV2EffectiveBasePath(app), `/admin/${schemaUid}`);
|
|
110
111
|
}
|
|
111
112
|
|
|
112
113
|
function appendLocationState(pathname: string, location?: LocationLike) {
|
|
@@ -20,7 +20,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
|
20
20
|
import { Button, Input } from 'antd';
|
|
21
21
|
import { css } from '@emotion/css';
|
|
22
22
|
import type { TextAreaRef } from 'antd/es/input/TextArea';
|
|
23
|
-
import { FlowContextSelector, useFlowContext } from '@nocobase/flow-engine';
|
|
23
|
+
import { FlowContextSelector, useFlowContext, type MetaTreeNode } from '@nocobase/flow-engine';
|
|
24
24
|
|
|
25
25
|
export interface TextAreaWithContextSelectorProps {
|
|
26
26
|
value?: string;
|
|
@@ -29,6 +29,20 @@ export interface TextAreaWithContextSelectorProps {
|
|
|
29
29
|
rows?: number;
|
|
30
30
|
maxRows?: number;
|
|
31
31
|
style?: React.CSSProperties;
|
|
32
|
+
disabled?: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Custom meta tree for the variable picker. Accepts an array, a sync getter,
|
|
35
|
+
* or an async getter — same shape as `FlowContextSelector`'s `metaTree`. If
|
|
36
|
+
* omitted, the full `ctx.getPropertyMetaTree()` is used (legacy default).
|
|
37
|
+
*/
|
|
38
|
+
metaTree?: MetaTreeNode[] | (() => MetaTreeNode[] | Promise<MetaTreeNode[]>);
|
|
39
|
+
/**
|
|
40
|
+
* Format a picked meta node into the string inserted at the caret. When
|
|
41
|
+
* omitted, the FlowContextSelector default (`{{ ctx.X.Y }}`) is used.
|
|
42
|
+
* Override to match a different storage convention — e.g. NocoBase server
|
|
43
|
+
* templates use `{{$X.Y}}` without the `ctx.` prefix.
|
|
44
|
+
*/
|
|
45
|
+
formatPathToValue?: (meta: MetaTreeNode) => string;
|
|
32
46
|
}
|
|
33
47
|
|
|
34
48
|
/**
|
|
@@ -41,6 +55,9 @@ export const TextAreaWithContextSelector: React.FC<TextAreaWithContextSelectorPr
|
|
|
41
55
|
rows = 3,
|
|
42
56
|
maxRows = 24,
|
|
43
57
|
style,
|
|
58
|
+
disabled,
|
|
59
|
+
metaTree,
|
|
60
|
+
formatPathToValue,
|
|
44
61
|
}) => {
|
|
45
62
|
const flowCtx = useFlowContext();
|
|
46
63
|
const [innerValue, setInnerValue] = useState<string>(value || '');
|
|
@@ -76,10 +93,11 @@ export const TextAreaWithContextSelector: React.FC<TextAreaWithContextSelectorPr
|
|
|
76
93
|
const next = prev.slice(0, start) + toInsert + prev.slice(end);
|
|
77
94
|
setInnerValue(next);
|
|
78
95
|
onChange?.(next);
|
|
79
|
-
//
|
|
96
|
+
// 插入后选中刚插入的变量文本,与 v1 RawTextArea 行为一致:
|
|
97
|
+
// 用户可立即按删除键移除整段变量,或继续输入直接替换。
|
|
80
98
|
requestAnimationFrame(() => {
|
|
81
99
|
const pos = start + (toInsert?.length || 0);
|
|
82
|
-
el.setSelectionRange(
|
|
100
|
+
el.setSelectionRange(start, pos);
|
|
83
101
|
el.focus();
|
|
84
102
|
});
|
|
85
103
|
},
|
|
@@ -94,8 +112,8 @@ export const TextAreaWithContextSelector: React.FC<TextAreaWithContextSelectorPr
|
|
|
94
112
|
[insertAtCaret],
|
|
95
113
|
);
|
|
96
114
|
|
|
97
|
-
//
|
|
98
|
-
const
|
|
115
|
+
// 使用函数形式提供变量树,保证与运行时上下文一致;当外部传入则尊重外部值。
|
|
116
|
+
const resolvedMetaTree = useMemo(() => metaTree ?? (() => flowCtx.getPropertyMetaTree?.()), [flowCtx, metaTree]);
|
|
99
117
|
|
|
100
118
|
return (
|
|
101
119
|
<div style={{ position: 'relative', width: '100%', ...style }}>
|
|
@@ -105,6 +123,7 @@ export const TextAreaWithContextSelector: React.FC<TextAreaWithContextSelectorPr
|
|
|
105
123
|
onChange={handleTextChange}
|
|
106
124
|
autoSize={{ minRows: rows, maxRows }}
|
|
107
125
|
placeholder={placeholder}
|
|
126
|
+
disabled={disabled}
|
|
108
127
|
style={{ width: '100%' }}
|
|
109
128
|
/>
|
|
110
129
|
<div
|
|
@@ -116,7 +135,12 @@ export const TextAreaWithContextSelector: React.FC<TextAreaWithContextSelectorPr
|
|
|
116
135
|
lineHeight: 0,
|
|
117
136
|
}}
|
|
118
137
|
>
|
|
119
|
-
<FlowContextSelector
|
|
138
|
+
<FlowContextSelector
|
|
139
|
+
metaTree={resolvedMetaTree}
|
|
140
|
+
disabled={disabled}
|
|
141
|
+
formatPathToValue={formatPathToValue}
|
|
142
|
+
onChange={(val) => handleVariableSelected(val)}
|
|
143
|
+
>
|
|
120
144
|
<Button
|
|
121
145
|
type="default"
|
|
122
146
|
style={{ fontStyle: 'italic', fontFamily: 'New York, Times New Roman, Times, serif' }}
|
|
@@ -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 },
|
|
@@ -7,84 +7,4 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
import { TinyColor } from '@ctrl/tinycolor';
|
|
12
|
-
import { useSortable } from '@dnd-kit/sortable';
|
|
13
|
-
import { css } from '@emotion/css';
|
|
14
|
-
import { theme } from 'antd';
|
|
15
|
-
import classNames from 'classnames';
|
|
16
|
-
import React, { useMemo } from 'react';
|
|
17
|
-
|
|
18
|
-
type DragSortRowContextValue = {
|
|
19
|
-
attributes?: Record<string, unknown>;
|
|
20
|
-
listeners?: Record<string, unknown>;
|
|
21
|
-
setActivatorNodeRef?: (node: HTMLElement | null) => void;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
const DragSortRowContext = React.createContext<DragSortRowContextValue | null>(null);
|
|
25
|
-
|
|
26
|
-
const sortHandleClass = css`
|
|
27
|
-
display: inline-flex;
|
|
28
|
-
align-items: center;
|
|
29
|
-
justify-content: center;
|
|
30
|
-
cursor: grab;
|
|
31
|
-
`;
|
|
32
|
-
|
|
33
|
-
export const SortHandle: React.FC<{ id: string | number; style?: React.CSSProperties }> = (props) => {
|
|
34
|
-
const { id: _id, ...otherProps } = props;
|
|
35
|
-
const dragSortContext = React.useContext(DragSortRowContext);
|
|
36
|
-
// return <MenuOutlined ref={setNodeRef} {...otherProps} {...listeners} style={{ cursor: 'grab' }} />;
|
|
37
|
-
return (
|
|
38
|
-
<span
|
|
39
|
-
ref={dragSortContext?.setActivatorNodeRef}
|
|
40
|
-
{...dragSortContext?.attributes}
|
|
41
|
-
{...dragSortContext?.listeners}
|
|
42
|
-
{...otherProps}
|
|
43
|
-
className={classNames(sortHandleClass)}
|
|
44
|
-
>
|
|
45
|
-
<MenuOutlined />
|
|
46
|
-
</span>
|
|
47
|
-
);
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
export const SortableRow = (props) => {
|
|
51
|
-
const { token }: any = theme.useToken();
|
|
52
|
-
const id = props['data-row-key']?.toString();
|
|
53
|
-
const { setNodeRef, setActivatorNodeRef, attributes, listeners, active, over } = useSortable({
|
|
54
|
-
id,
|
|
55
|
-
});
|
|
56
|
-
const { rowIndex, ...others } = props;
|
|
57
|
-
const isOver = over?.id === id;
|
|
58
|
-
const classObj = useMemo(() => {
|
|
59
|
-
const borderColor = new TinyColor(token.colorPrimary).setAlpha(0.6).toHex8String();
|
|
60
|
-
return {
|
|
61
|
-
topActiveClass: css`
|
|
62
|
-
& > td {
|
|
63
|
-
border-top: 2px solid ${borderColor} !important;
|
|
64
|
-
}
|
|
65
|
-
`,
|
|
66
|
-
bottomActiveClass: css`
|
|
67
|
-
& > td {
|
|
68
|
-
border-bottom: 2px solid ${borderColor} !important;
|
|
69
|
-
}
|
|
70
|
-
`,
|
|
71
|
-
};
|
|
72
|
-
}, [token.colorPrimary]);
|
|
73
|
-
|
|
74
|
-
const className =
|
|
75
|
-
(active?.data.current?.sortable.index ?? -1) > rowIndex ? classObj.topActiveClass : classObj.bottomActiveClass;
|
|
76
|
-
|
|
77
|
-
const row = (
|
|
78
|
-
<DragSortRowContext.Provider value={{ listeners, setActivatorNodeRef }}>
|
|
79
|
-
<tr
|
|
80
|
-
ref={(node) => {
|
|
81
|
-
setNodeRef(node);
|
|
82
|
-
}}
|
|
83
|
-
{...others}
|
|
84
|
-
className={classNames(props.className, { [className]: active && isOver })}
|
|
85
|
-
/>
|
|
86
|
-
</DragSortRowContext.Provider>
|
|
87
|
-
);
|
|
88
|
-
|
|
89
|
-
return row;
|
|
90
|
-
};
|
|
10
|
+
export { DragSortRowContext, SortHandle, SortableRow } from '../../../../../components/form/table/dnd/SortableRow';
|