@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
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { uid } from '@formily/shared';
|
|
10
11
|
import { defineAction, tExpr } from '@nocobase/flow-engine';
|
|
11
12
|
import { DetailsItemModel } from '../models/blocks/details/DetailsItemModel';
|
|
12
13
|
import { getFieldBindingUse, rebuildFieldSubModel } from '../internal/utils/rebuildFieldSubModel';
|
|
@@ -19,6 +20,14 @@ type PatternAwareFieldModel = {
|
|
|
19
20
|
scheduleApplyJsSettings?: () => void;
|
|
20
21
|
};
|
|
21
22
|
|
|
23
|
+
function resolveAssociationTitleField(ctx: any) {
|
|
24
|
+
return (
|
|
25
|
+
ctx.model.subModels?.field?.props?.fieldNames?.label ||
|
|
26
|
+
ctx.model.props.titleField ||
|
|
27
|
+
ctx.collectionField?.targetCollectionTitleFieldName
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
22
31
|
function shouldPreserveFieldModelOnPatternChange(ctx: any) {
|
|
23
32
|
const fieldModel = ctx.model.subModels.field;
|
|
24
33
|
const fieldUse = getFieldBindingUse(fieldModel) ?? fieldModel?.use;
|
|
@@ -27,6 +36,22 @@ function shouldPreserveFieldModelOnPatternChange(ctx: any) {
|
|
|
27
36
|
return ((ModelClass?.meta as PatternAwareFieldModelMeta | undefined)?.preserveOnPatternChange ?? false) === true;
|
|
28
37
|
}
|
|
29
38
|
|
|
39
|
+
async function refreshPatternRuntime(ctx: any) {
|
|
40
|
+
ctx.model.invalidateFlowCache?.('beforeRender', true);
|
|
41
|
+
await ctx.model.rerender?.();
|
|
42
|
+
|
|
43
|
+
const parent = ctx.model.parent;
|
|
44
|
+
if (!parent) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
parent.invalidateFlowCache?.('beforeRender', true);
|
|
49
|
+
parent.setProps?.({
|
|
50
|
+
__patternRefreshKey: uid(),
|
|
51
|
+
});
|
|
52
|
+
await parent.rerender?.();
|
|
53
|
+
}
|
|
54
|
+
|
|
30
55
|
export const pattern = defineAction({
|
|
31
56
|
name: 'pattern',
|
|
32
57
|
title: tExpr('Display mode'),
|
|
@@ -76,17 +101,25 @@ export const pattern = defineAction({
|
|
|
76
101
|
if (params.pattern !== previousParams.pattern) {
|
|
77
102
|
(ctx.model.subModels.field as PatternAwareFieldModel | undefined)?.scheduleApplyJsSettings?.();
|
|
78
103
|
}
|
|
104
|
+
await refreshPatternRuntime(ctx);
|
|
79
105
|
return;
|
|
80
106
|
}
|
|
81
107
|
|
|
82
108
|
const targetCollection = ctx.collectionField.targetCollection;
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
);
|
|
109
|
+
const associationTitleField = resolveAssociationTitleField(ctx);
|
|
110
|
+
const targetCollectionTitleField = targetCollection?.getField(associationTitleField);
|
|
86
111
|
const { model } = ctx;
|
|
87
112
|
const resolveDefaultProps = (binding, field = ctx.collectionField) => {
|
|
88
113
|
if (!binding) return undefined;
|
|
89
|
-
|
|
114
|
+
const defaultProps =
|
|
115
|
+
typeof binding.defaultProps === 'function' ? binding.defaultProps(ctx, field) : binding.defaultProps;
|
|
116
|
+
if (!ctx.collectionField?.isAssociationField?.() || !associationTitleField) {
|
|
117
|
+
return defaultProps;
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
...(defaultProps || {}),
|
|
121
|
+
titleField: associationTitleField,
|
|
122
|
+
};
|
|
90
123
|
};
|
|
91
124
|
if (params.pattern === 'readPretty') {
|
|
92
125
|
const binding = DetailsItemModel.getDefaultBindingByField(ctx, ctx.collectionField, {
|
|
@@ -111,17 +144,19 @@ export const pattern = defineAction({
|
|
|
111
144
|
});
|
|
112
145
|
}
|
|
113
146
|
}
|
|
147
|
+
await refreshPatternRuntime(ctx);
|
|
114
148
|
},
|
|
115
149
|
async beforeParamsSave(ctx, params, previousParams) {
|
|
116
150
|
if (params.pattern === 'readPretty') {
|
|
151
|
+
const titleField = resolveAssociationTitleField(ctx);
|
|
117
152
|
ctx.model.setProps({
|
|
118
153
|
pattern: 'readPretty',
|
|
119
154
|
disabled: false,
|
|
120
|
-
titleField
|
|
155
|
+
titleField,
|
|
121
156
|
});
|
|
122
157
|
if (ctx.collectionField.isAssociationField())
|
|
123
158
|
await ctx.model.setStepParams('editItemSettings', 'titleField', {
|
|
124
|
-
titleField
|
|
159
|
+
titleField,
|
|
125
160
|
});
|
|
126
161
|
} else {
|
|
127
162
|
ctx.model.setProps({
|
|
@@ -43,7 +43,9 @@ export const titleField = defineAction({
|
|
|
43
43
|
const options = targetFields
|
|
44
44
|
.filter((field) =>
|
|
45
45
|
isTitleFieldInterface(
|
|
46
|
-
|
|
46
|
+
typeof field.getInterfaceOptions === 'function'
|
|
47
|
+
? field.getInterfaceOptions()
|
|
48
|
+
: getFlowFieldInterfaceOptions(field.options?.interface || field.interface, dataSourceManager),
|
|
47
49
|
),
|
|
48
50
|
)
|
|
49
51
|
.map((field) => ({
|
|
@@ -59,7 +61,7 @@ export const titleField = defineAction({
|
|
|
59
61
|
};
|
|
60
62
|
},
|
|
61
63
|
defaultParams: (ctx: any) => {
|
|
62
|
-
const titleField = ctx.model
|
|
64
|
+
const titleField = ctx.model?.context?.collectionField?.targetCollectionTitleFieldName;
|
|
63
65
|
return {
|
|
64
66
|
label: ctx.model.parent?.props?.titleField || ctx.model.props.titleField || titleField,
|
|
65
67
|
};
|
|
@@ -628,10 +628,6 @@ export class AdminLayoutMenuItemModel extends FlowModel<AdminLayoutMenuItemStruc
|
|
|
628
628
|
)
|
|
629
629
|
.filter(Boolean) || [];
|
|
630
630
|
|
|
631
|
-
if (isV2AdminRuntime(this.context.app) && children.length === 0) {
|
|
632
|
-
return null;
|
|
633
|
-
}
|
|
634
|
-
|
|
635
631
|
if (options.designable && depth === 0) {
|
|
636
632
|
children.push(getAdminLayoutMenuInitializerButton('schema-initializer-Menu-side', this, route));
|
|
637
633
|
}
|
|
@@ -685,6 +681,7 @@ export class AdminLayoutMenuItemModel extends FlowModel<AdminLayoutMenuItemStruc
|
|
|
685
681
|
AdminLayoutMenuItemModel.registerFlow({
|
|
686
682
|
key: 'menuCreation',
|
|
687
683
|
title: 'Add menu item',
|
|
684
|
+
manual: true,
|
|
688
685
|
steps: {
|
|
689
686
|
basic: {
|
|
690
687
|
title: 'Add menu item',
|
|
@@ -701,6 +698,7 @@ AdminLayoutMenuItemModel.registerFlow({
|
|
|
701
698
|
AdminLayoutMenuItemModel.registerFlow({
|
|
702
699
|
key: 'menuSettings',
|
|
703
700
|
title: 'Menu settings',
|
|
701
|
+
manual: true,
|
|
704
702
|
steps: {
|
|
705
703
|
edit: {
|
|
706
704
|
title: 'Edit',
|
|
@@ -311,7 +311,7 @@ describe('AdminLayoutModel menu items', () => {
|
|
|
311
311
|
expect(route.children[1]._model).toBe(adminLayoutModel.subModels.menuItems?.[1]);
|
|
312
312
|
});
|
|
313
313
|
|
|
314
|
-
it('should filter legacy page menu routes in v2 admin layout', () => {
|
|
314
|
+
it('should filter legacy page menu routes but keep empty groups in v2 admin layout', () => {
|
|
315
315
|
const adminLayoutModel = engine.createModel<AdminLayoutModel>({
|
|
316
316
|
uid: 'admin-layout-model',
|
|
317
317
|
use: AdminLayoutModel,
|
|
@@ -369,22 +369,30 @@ describe('AdminLayoutModel menu items', () => {
|
|
|
369
369
|
t: (title) => title,
|
|
370
370
|
});
|
|
371
371
|
|
|
372
|
-
expect(route.children).toHaveLength(
|
|
372
|
+
expect(route.children).toHaveLength(3);
|
|
373
373
|
expect(route.children[0]).toMatchObject({
|
|
374
|
+
path: '/admin/2',
|
|
375
|
+
redirect: '/admin/2',
|
|
376
|
+
_runtimePath: null,
|
|
377
|
+
_navigationMode: 'spa',
|
|
378
|
+
_isLegacy: false,
|
|
379
|
+
});
|
|
380
|
+
expect(route.children[0].routes).toBeUndefined();
|
|
381
|
+
expect(route.children[1]).toMatchObject({
|
|
374
382
|
path: '/admin/3',
|
|
375
383
|
redirect: '/admin/nested-flow-page',
|
|
376
384
|
_runtimePath: '/apps/demo/v2/admin/nested-flow-page',
|
|
377
385
|
_navigationMode: 'spa',
|
|
378
386
|
_isLegacy: false,
|
|
379
387
|
});
|
|
380
|
-
expect(route.children[
|
|
381
|
-
expect(route.children[
|
|
388
|
+
expect(route.children[1].routes).toHaveLength(1);
|
|
389
|
+
expect(route.children[1].routes?.[0]).toMatchObject({
|
|
382
390
|
path: '/admin/nested-flow-page',
|
|
383
391
|
_runtimePath: '/apps/demo/v2/admin/nested-flow-page',
|
|
384
392
|
_navigationMode: 'spa',
|
|
385
393
|
_isLegacy: false,
|
|
386
394
|
});
|
|
387
|
-
expect(route.children[
|
|
395
|
+
expect(route.children[2]).toMatchObject({
|
|
388
396
|
path: '/admin/__admin_layout__/link/4',
|
|
389
397
|
});
|
|
390
398
|
});
|
|
@@ -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) {
|
|
@@ -28,6 +28,9 @@ import {
|
|
|
28
28
|
GLOBAL_EMBED_CONTAINER_ID,
|
|
29
29
|
EMBED_REPLACING_DATA_KEY,
|
|
30
30
|
shouldHideEventInSettings,
|
|
31
|
+
DetachedFlowRegistry,
|
|
32
|
+
replaceFlowRegistry,
|
|
33
|
+
serializeFlowRegistry,
|
|
31
34
|
} from '@nocobase/flow-engine';
|
|
32
35
|
import { Collapse, Input, Button, Space, Tooltip, Empty, Dropdown, Select } from 'antd';
|
|
33
36
|
import { uid } from '@formily/shared';
|
|
@@ -35,6 +38,9 @@ import { useUpdate } from 'ahooks';
|
|
|
35
38
|
import _ from 'lodash';
|
|
36
39
|
|
|
37
40
|
type FlowOnObject = Exclude<FlowDefinition['on'], string | undefined>;
|
|
41
|
+
type FlowRegistryAvailability = {
|
|
42
|
+
hasFlow(flowKey: string): boolean;
|
|
43
|
+
};
|
|
38
44
|
|
|
39
45
|
function isFlowOnObject(on: FlowDefinition['on']): on is FlowOnObject {
|
|
40
46
|
return !!on && typeof on === 'object';
|
|
@@ -93,7 +99,7 @@ function validateFlowOnPhase(onObj: FlowOnObject): 'flowKey' | 'stepKey' | undef
|
|
|
93
99
|
|
|
94
100
|
export const DynamicFlowsIcon: React.FC<{ model: FlowModel }> = (props) => {
|
|
95
101
|
const { model } = props;
|
|
96
|
-
const t = model.translate.bind(model);
|
|
102
|
+
const t = React.useMemo(() => model.translate.bind(model), [model]);
|
|
97
103
|
|
|
98
104
|
const handleClick = () => {
|
|
99
105
|
const target = document.querySelector<HTMLDivElement>(`#${GLOBAL_EMBED_CONTAINER_ID}`);
|
|
@@ -190,7 +196,17 @@ const FieldLabel = ({
|
|
|
190
196
|
|
|
191
197
|
// 事件配置组件 - 独立的 observer 组件确保响应式更新
|
|
192
198
|
const EventConfigSection = observer(
|
|
193
|
-
({
|
|
199
|
+
({
|
|
200
|
+
flow,
|
|
201
|
+
model,
|
|
202
|
+
flowEngine,
|
|
203
|
+
flowRegistry,
|
|
204
|
+
}: {
|
|
205
|
+
flow: FlowDefinition;
|
|
206
|
+
model: FlowModel;
|
|
207
|
+
flowEngine: any;
|
|
208
|
+
flowRegistry: FlowRegistryAvailability;
|
|
209
|
+
}) => {
|
|
194
210
|
const ctx = useFlowContext<FlowEngineContext>();
|
|
195
211
|
const t = model.translate.bind(model);
|
|
196
212
|
const refresh = useUpdate();
|
|
@@ -270,9 +286,9 @@ const EventConfigSection = observer(
|
|
|
270
286
|
return staticFlows.map((f) => ({
|
|
271
287
|
value: f.key,
|
|
272
288
|
label: formatKeyWithTitle(String(f.key), f.title),
|
|
273
|
-
disabled:
|
|
289
|
+
disabled: flowRegistry.hasFlow(f.key),
|
|
274
290
|
}));
|
|
275
|
-
}, [
|
|
291
|
+
}, [flowRegistry, formatKeyWithTitle, staticFlows]);
|
|
276
292
|
|
|
277
293
|
const stepOptions = React.useMemo(() => {
|
|
278
294
|
if (!flowKeyValue) return [];
|
|
@@ -446,12 +462,58 @@ const DynamicFlowsEditor = observer((props: { model: FlowModel }) => {
|
|
|
446
462
|
const { model } = props;
|
|
447
463
|
const ctx = useFlowContext<FlowEngineContext>();
|
|
448
464
|
const flowEngine = model.flowEngine;
|
|
465
|
+
const latestFlows = serializeFlowRegistry(model.flowRegistry);
|
|
466
|
+
const initialFlowsRef = React.useRef(latestFlows);
|
|
467
|
+
const [draftFlowRegistry] = React.useState(() => new DetachedFlowRegistry(latestFlows));
|
|
449
468
|
const [submitLoading, setSubmitLoading] = React.useState(false);
|
|
450
|
-
const t = model.translate.bind(model);
|
|
469
|
+
const t = React.useMemo(() => model.translate.bind(model), [model]);
|
|
470
|
+
const hasUnsavedChanges = React.useCallback(() => {
|
|
471
|
+
return !_.isEqual(initialFlowsRef.current, serializeFlowRegistry(draftFlowRegistry));
|
|
472
|
+
}, [draftFlowRegistry]);
|
|
473
|
+
|
|
474
|
+
React.useEffect(() => {
|
|
475
|
+
if (_.isEqual(initialFlowsRef.current, latestFlows) || hasUnsavedChanges()) {
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
replaceFlowRegistry(draftFlowRegistry, latestFlows);
|
|
480
|
+
initialFlowsRef.current = latestFlows;
|
|
481
|
+
}, [draftFlowRegistry, hasUnsavedChanges, latestFlows]);
|
|
482
|
+
|
|
483
|
+
React.useEffect(() => {
|
|
484
|
+
const view = ctx.view;
|
|
485
|
+
const previousBeforeClose = view.beforeClose;
|
|
486
|
+
const beforeClose = async (payload) => {
|
|
487
|
+
if (hasUnsavedChanges()) {
|
|
488
|
+
const confirmed =
|
|
489
|
+
(await ctx.modal?.confirm?.({
|
|
490
|
+
title: t('Unsaved changes'),
|
|
491
|
+
content: t("Are you sure you don't want to save?"),
|
|
492
|
+
okText: t('Confirm'),
|
|
493
|
+
cancelText: t('Cancel'),
|
|
494
|
+
})) ?? true;
|
|
495
|
+
|
|
496
|
+
if (!confirmed) {
|
|
497
|
+
return false;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const result = await previousBeforeClose?.(payload);
|
|
502
|
+
return result !== false;
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
view.beforeClose = beforeClose;
|
|
506
|
+
|
|
507
|
+
return () => {
|
|
508
|
+
if (view.beforeClose === beforeClose) {
|
|
509
|
+
view.beforeClose = previousBeforeClose;
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
}, [ctx, hasUnsavedChanges, t]);
|
|
451
513
|
|
|
452
514
|
// 添加新流
|
|
453
515
|
const handleAddFlow = () => {
|
|
454
|
-
|
|
516
|
+
draftFlowRegistry.addFlow(uid(), {
|
|
455
517
|
title: t('Event flow'),
|
|
456
518
|
steps: {},
|
|
457
519
|
});
|
|
@@ -516,7 +578,7 @@ const DynamicFlowsEditor = observer((props: { model: FlowModel }) => {
|
|
|
516
578
|
);
|
|
517
579
|
|
|
518
580
|
// 生成折叠面板项
|
|
519
|
-
const collapseItems =
|
|
581
|
+
const collapseItems = draftFlowRegistry.mapFlows((flow) => {
|
|
520
582
|
return {
|
|
521
583
|
key: flow.key,
|
|
522
584
|
label: renderPanelHeader(flow),
|
|
@@ -529,7 +591,7 @@ const DynamicFlowsEditor = observer((props: { model: FlowModel }) => {
|
|
|
529
591
|
children: (
|
|
530
592
|
<div>
|
|
531
593
|
{/* 事件部分 */}
|
|
532
|
-
<EventConfigSection flow={flow} model={model} flowEngine={flowEngine} />
|
|
594
|
+
<EventConfigSection flow={flow} model={model} flowEngine={flowEngine} flowRegistry={draftFlowRegistry} />
|
|
533
595
|
|
|
534
596
|
{/* 步骤部分 */}
|
|
535
597
|
<div>
|
|
@@ -670,13 +732,13 @@ const DynamicFlowsEditor = observer((props: { model: FlowModel }) => {
|
|
|
670
732
|
flexShrink: 0,
|
|
671
733
|
}}
|
|
672
734
|
>
|
|
673
|
-
<Button onClick={() => ctx.view.
|
|
735
|
+
<Button onClick={() => ctx.view.close()}>{t('Cancel')}</Button>
|
|
674
736
|
<Button
|
|
675
737
|
type="primary"
|
|
676
738
|
loading={submitLoading}
|
|
677
739
|
onClick={async () => {
|
|
678
740
|
setSubmitLoading(true);
|
|
679
|
-
const invalid =
|
|
741
|
+
const invalid = draftFlowRegistry
|
|
680
742
|
.mapFlows((flow) => {
|
|
681
743
|
if (!isFlowOnObject(flow.on)) return;
|
|
682
744
|
normalizeFlowOnPhase(flow.on);
|
|
@@ -695,10 +757,11 @@ const DynamicFlowsEditor = observer((props: { model: FlowModel }) => {
|
|
|
695
757
|
setSubmitLoading(false);
|
|
696
758
|
return;
|
|
697
759
|
}
|
|
698
|
-
|
|
699
|
-
|
|
760
|
+
const previousFlows = serializeFlowRegistry(model.flowRegistry);
|
|
761
|
+
const afterSaves: Array<() => Promise<void>> = [];
|
|
700
762
|
|
|
701
|
-
|
|
763
|
+
try {
|
|
764
|
+
for (const flow of draftFlowRegistry.mapFlows((it) => it)) {
|
|
702
765
|
for (const step of flow.mapSteps((it) => it)) {
|
|
703
766
|
const serialized = step.serialize();
|
|
704
767
|
const actionDef = step.use ? model.getAction(step.use) : undefined;
|
|
@@ -731,8 +794,19 @@ const DynamicFlowsEditor = observer((props: { model: FlowModel }) => {
|
|
|
731
794
|
}
|
|
732
795
|
}
|
|
733
796
|
|
|
797
|
+
replaceFlowRegistry(model.flowRegistry, serializeFlowRegistry(draftFlowRegistry));
|
|
734
798
|
await model.flowRegistry.save();
|
|
799
|
+
} catch (error) {
|
|
800
|
+
replaceFlowRegistry(model.flowRegistry, previousFlows);
|
|
801
|
+
setSubmitLoading(false);
|
|
802
|
+
model.context?.message?.error?.('Steps post-save hooks failed to run');
|
|
803
|
+
console.error(error);
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
735
806
|
|
|
807
|
+
initialFlowsRef.current = serializeFlowRegistry(model.flowRegistry);
|
|
808
|
+
|
|
809
|
+
try {
|
|
736
810
|
for (const runAfterSave of afterSaves) {
|
|
737
811
|
await runAfterSave();
|
|
738
812
|
}
|
|
@@ -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' }}
|