@nocobase/client-v2 2.2.0-beta.5 → 2.2.0-beta.6
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/RouteRepository.d.ts +21 -6
- package/es/components/form/JsonTextArea.d.ts +2 -1
- package/es/components/form/VariableJsonTextArea.d.ts +19 -0
- package/es/components/form/index.d.ts +1 -0
- package/es/components/index.d.ts +1 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.d.ts +2 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.d.ts +2 -1
- package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +2 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.d.ts +3 -2
- package/es/flow/admin-shell/admin-layout/AdminLayoutSlotModels.d.ts +4 -0
- package/es/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.d.ts +6 -0
- package/es/flow/components/FieldAssignRulesEditor.d.ts +3 -1
- package/es/flow/models/base/GridModel.d.ts +1 -1
- package/es/flow/models/blocks/filter-form/FilterFormBlockModel.d.ts +5 -0
- package/es/flow/models/blocks/form/FormBlockModel.d.ts +4 -0
- package/es/flow/models/blocks/form/value-runtime/rules.d.ts +21 -0
- package/es/flow/models/blocks/form/value-runtime/runtime.d.ts +21 -0
- package/es/flow/models/blocks/form/value-runtime/types.d.ts +2 -2
- package/es/flow-compat/FieldValidation.d.ts +2 -1
- package/es/index.d.ts +2 -1
- package/es/index.mjs +143 -117
- package/lib/index.js +148 -122
- package/package.json +7 -7
- package/src/RouteRepository.ts +126 -24
- package/src/__tests__/RouteRepository.test.ts +216 -0
- package/src/__tests__/exports.test.ts +16 -0
- package/src/__tests__/settings-center.test.tsx +33 -0
- package/src/components/form/JsonTextArea.tsx +21 -11
- package/src/components/form/VariableJsonTextArea.tsx +175 -0
- package/src/components/form/__tests__/JsonTextArea.test.tsx +43 -0
- package/src/components/form/__tests__/VariableJsonTextArea.test.tsx +86 -0
- package/src/components/form/index.tsx +1 -0
- package/src/components/index.ts +1 -0
- package/src/flow/__tests__/FlowRoute.test.tsx +443 -5
- package/src/flow/actions/__tests__/fieldLinkageRules.scopeDepth.test.ts +150 -1
- package/src/flow/actions/__tests__/linkageAssignField.legacy.test.ts +135 -0
- package/src/flow/actions/__tests__/linkageRules.hiddenSync.restore.test.ts +19 -5
- package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +92 -0
- package/src/flow/actions/customVariable.tsx +1 -2
- package/src/flow/actions/linkageRules.tsx +122 -34
- package/src/flow/actions/runjs.tsx +2 -14
- package/src/flow/actions/validation.tsx +62 -30
- package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +33 -10
- package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.test.tsx +310 -0
- package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +44 -8
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.ts +5 -3
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +61 -9
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +71 -8
- package/src/flow/admin-shell/admin-layout/AdminLayoutModel.tsx +1 -1
- package/src/flow/admin-shell/admin-layout/AdminLayoutSlotModels.tsx +21 -8
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +188 -0
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +99 -0
- package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +166 -2
- package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.test.ts +20 -0
- package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.ts +33 -5
- package/src/flow/components/DefaultValue.tsx +1 -2
- package/src/flow/components/FieldAssignRulesEditor.tsx +62 -12
- package/src/flow/components/FlowRoute.tsx +56 -11
- package/src/flow/components/__tests__/FieldAssignRulesEditor.test.tsx +81 -4
- package/src/flow/components/code-editor/__tests__/useCodeRunner.test.tsx +2 -17
- package/src/flow/components/code-editor/hooks/useCodeRunner.ts +2 -14
- package/src/flow/components/code-editor/runjsDiagnostics.ts +8 -45
- package/src/flow/models/actions/JSActionModel.tsx +2 -7
- package/src/flow/models/actions/JSCollectionActionModel.tsx +2 -7
- package/src/flow/models/actions/JSItemActionModel.tsx +2 -7
- package/src/flow/models/actions/JSRecordActionModel.tsx +2 -7
- package/src/flow/models/base/ActionModel.tsx +12 -1
- package/src/flow/models/base/GridModel.tsx +38 -7
- package/src/flow/models/base/PageModel/PageModel.tsx +4 -1
- package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +28 -4
- package/src/flow/models/base/__tests__/ActionModel.test.ts +83 -0
- package/src/flow/models/base/__tests__/GridModel.dragSnapshotContainer.test.ts +239 -1
- package/src/flow/models/base/__tests__/transformRowsToSingleColumn.test.ts +48 -0
- package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +31 -3
- package/src/flow/models/blocks/filter-form/FilterFormJSActionModel.tsx +2 -7
- package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +77 -0
- package/src/flow/models/blocks/form/EditFormModel.tsx +1 -0
- package/src/flow/models/blocks/form/FormBlockModel.tsx +7 -0
- package/src/flow/models/blocks/form/JSFormActionModel.tsx +2 -7
- package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +17 -0
- package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +880 -102
- package/src/flow/models/blocks/form/value-runtime/rules.ts +445 -13
- package/src/flow/models/blocks/form/value-runtime/runtime.ts +257 -13
- package/src/flow/models/blocks/form/value-runtime/types.ts +2 -2
- package/src/flow/models/blocks/js-block/JSBlock.tsx +2 -7
- package/src/flow/models/blocks/table/JSColumnModel.tsx +1 -9
- package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +1 -1
- package/src/flow/models/blocks/table/TableBlockModel.tsx +13 -3
- package/src/flow/models/blocks/table/__tests__/TableActionsColumnModel.test.tsx +54 -0
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.mobileSettingsButtons.test.tsx +78 -0
- package/src/flow/models/fields/AssociationFieldModel/AssociationFieldModel.tsx +1 -1
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +29 -6
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +13 -1
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableFieldModel.reset.test.ts +180 -0
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +6 -4
- package/src/flow/models/fields/AssociationFieldModel/__tests__/AssociationFieldModel.updateAssociation.test.ts +72 -0
- package/src/flow/models/fields/InputFieldModel.tsx +14 -22
- package/src/flow/models/fields/JSEditableFieldModel.tsx +2 -11
- package/src/flow/models/fields/JSFieldModel.tsx +2 -7
- package/src/flow/models/fields/JSItemModel.tsx +2 -14
- package/src/flow/models/fields/__tests__/InputFieldModel.test.tsx +17 -0
- package/src/flow/models/topbar/TopbarActionModel.tsx +93 -10
- package/src/flow-compat/FieldValidation.tsx +122 -60
- package/src/flow-compat/Popover.tsx +43 -4
- package/src/flow-compat/__tests__/Popover.test.tsx +34 -0
- package/src/index.ts +8 -1
- package/src/layout-manager/__tests__/LayoutRoute.test.tsx +41 -1
- package/src/settings-center/AdminSettingsLayout.tsx +3 -0
- package/src/settings-center/plugin-manager/index.tsx +3 -0
|
@@ -28,6 +28,13 @@ type FlowRouteGuardState = {
|
|
|
28
28
|
|
|
29
29
|
export type LegacyPageBehavior = 'redirect' | 'notFound' | 'bridge';
|
|
30
30
|
|
|
31
|
+
type FlowRouteLayoutContext = {
|
|
32
|
+
authCheck?: boolean;
|
|
33
|
+
routePath?: string;
|
|
34
|
+
routeName?: string;
|
|
35
|
+
uid?: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
31
38
|
export type FlowRouteProps = {
|
|
32
39
|
pageUid?: string;
|
|
33
40
|
active?: boolean;
|
|
@@ -38,7 +45,7 @@ export type FlowRouteProps = {
|
|
|
38
45
|
const getDefaultAdminLayoutModel = (flowEngine: FlowEngine) =>
|
|
39
46
|
getAdminLayoutModel<AdminLayoutModel>(flowEngine, { required: true });
|
|
40
47
|
|
|
41
|
-
const getDefaultLayoutModel = (flowEngine: FlowEngine, contextLayout?:
|
|
48
|
+
const getDefaultLayoutModel = (flowEngine: FlowEngine, contextLayout?: FlowRouteLayoutContext) => {
|
|
42
49
|
const layout = contextLayout || flowEngine.context.layout;
|
|
43
50
|
|
|
44
51
|
if (layout?.uid) {
|
|
@@ -48,7 +55,10 @@ const getDefaultLayoutModel = (flowEngine: FlowEngine, contextLayout?: any) => {
|
|
|
48
55
|
return getDefaultAdminLayoutModel(flowEngine);
|
|
49
56
|
};
|
|
50
57
|
|
|
51
|
-
const getDefaultLegacyPageBehavior = (
|
|
58
|
+
const getDefaultLegacyPageBehavior = (
|
|
59
|
+
flowEngine: FlowEngine,
|
|
60
|
+
contextLayout?: FlowRouteLayoutContext,
|
|
61
|
+
): LegacyPageBehavior => {
|
|
52
62
|
const layout = contextLayout || flowEngine.context.layout;
|
|
53
63
|
|
|
54
64
|
if (layout?.routeName && layout.routeName !== 'admin') {
|
|
@@ -58,6 +68,8 @@ const getDefaultLegacyPageBehavior = (flowEngine: FlowEngine, contextLayout?: an
|
|
|
58
68
|
return 'redirect';
|
|
59
69
|
};
|
|
60
70
|
|
|
71
|
+
const shouldRequireAccessibleRoute = (contextLayout?: FlowRouteLayoutContext) => contextLayout?.authCheck !== false;
|
|
72
|
+
|
|
61
73
|
const hasFlowModel = async (flowEngine: FlowEngine, pageUid: string) => {
|
|
62
74
|
if (flowEngine.getModel(pageUid)) {
|
|
63
75
|
return true;
|
|
@@ -148,18 +160,46 @@ const BridgeFlowRoute = ({
|
|
|
148
160
|
* @throws {Error} 当缺少 `route.params.name` 时抛出异常
|
|
149
161
|
*/
|
|
150
162
|
const FlowRoute = (props: FlowRouteProps = {}) => {
|
|
163
|
+
const {
|
|
164
|
+
active,
|
|
165
|
+
getLayoutModel: getLayoutModelProp,
|
|
166
|
+
legacyPageBehavior: legacyPageBehaviorProp,
|
|
167
|
+
pageUid: pageUidProp,
|
|
168
|
+
} = props;
|
|
151
169
|
const flowEngine = useFlowEngine();
|
|
152
|
-
const flowContext = useFlowContext<
|
|
170
|
+
const flowContext = useFlowContext<{ layout?: FlowRouteLayoutContext }>();
|
|
153
171
|
const contextLayout = flowContext?.layout;
|
|
172
|
+
const propsLayoutModel = useMemo(() => getLayoutModelProp?.(flowEngine), [flowEngine, getLayoutModelProp]);
|
|
173
|
+
const rawRouteLayout = contextLayout || propsLayoutModel?.layout;
|
|
174
|
+
const routeLayoutUid = rawRouteLayout?.uid;
|
|
175
|
+
const routeLayoutRouteName = rawRouteLayout?.routeName;
|
|
176
|
+
const routeLayoutRoutePath = rawRouteLayout?.routePath;
|
|
177
|
+
const routeLayoutAuthCheck = rawRouteLayout?.authCheck;
|
|
178
|
+
const routeLayout = useMemo(() => {
|
|
179
|
+
if (
|
|
180
|
+
!routeLayoutUid &&
|
|
181
|
+
!routeLayoutRouteName &&
|
|
182
|
+
!routeLayoutRoutePath &&
|
|
183
|
+
typeof routeLayoutAuthCheck === 'undefined'
|
|
184
|
+
) {
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
authCheck: routeLayoutAuthCheck,
|
|
189
|
+
routeName: routeLayoutRouteName,
|
|
190
|
+
routePath: routeLayoutRoutePath,
|
|
191
|
+
uid: routeLayoutUid,
|
|
192
|
+
};
|
|
193
|
+
}, [routeLayoutAuthCheck, routeLayoutRouteName, routeLayoutRoutePath, routeLayoutUid]);
|
|
154
194
|
const getLayoutModel = useMemo(
|
|
155
|
-
() =>
|
|
156
|
-
[
|
|
195
|
+
() => getLayoutModelProp || ((engine: FlowEngine) => getDefaultLayoutModel(engine, routeLayout)),
|
|
196
|
+
[getLayoutModelProp, routeLayout],
|
|
157
197
|
);
|
|
158
|
-
const legacyPageBehavior =
|
|
198
|
+
const legacyPageBehavior = legacyPageBehaviorProp || getDefaultLegacyPageBehavior(flowEngine, routeLayout);
|
|
159
199
|
const app = useApp();
|
|
160
200
|
const routeRepository = flowEngine.context.routeRepository;
|
|
161
201
|
const params = useParams();
|
|
162
|
-
const pageUid =
|
|
202
|
+
const pageUid = pageUidProp || params?.name;
|
|
163
203
|
const skipRouteRepositoryCheck = !routeRepository;
|
|
164
204
|
const [guardState, setGuardState] = useState<FlowRouteGuardState>({
|
|
165
205
|
pending: true,
|
|
@@ -196,6 +236,11 @@ const FlowRoute = (props: FlowRouteProps = {}) => {
|
|
|
196
236
|
}
|
|
197
237
|
|
|
198
238
|
const route = skipRouteRepositoryCheck ? undefined : routeRepository?.getRouteBySchemaUid?.(pageUid);
|
|
239
|
+
if (!route && !skipRouteRepositoryCheck && shouldRequireAccessibleRoute(routeLayout)) {
|
|
240
|
+
setGuardState({ pending: false, allowBridge: false, notFound: true });
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
199
244
|
if (!route && legacyPageBehavior === 'notFound') {
|
|
200
245
|
const flowModelExists = await hasFlowModel(flowEngine, pageUid);
|
|
201
246
|
if (active && requestId === requestIdRef.current) {
|
|
@@ -245,12 +290,12 @@ const FlowRoute = (props: FlowRouteProps = {}) => {
|
|
|
245
290
|
}
|
|
246
291
|
};
|
|
247
292
|
|
|
248
|
-
|
|
293
|
+
run();
|
|
249
294
|
|
|
250
295
|
return () => {
|
|
251
296
|
active = false;
|
|
252
297
|
};
|
|
253
|
-
}, [app, flowEngine, legacyPageBehavior, pageUid, routeRepository, skipRouteRepositoryCheck]);
|
|
298
|
+
}, [app, flowEngine, legacyPageBehavior, pageUid, routeLayout, routeRepository, skipRouteRepositoryCheck]);
|
|
254
299
|
|
|
255
300
|
const content = useMemo(() => {
|
|
256
301
|
if (guardState.pending) {
|
|
@@ -265,8 +310,8 @@ const FlowRoute = (props: FlowRouteProps = {}) => {
|
|
|
265
310
|
return null;
|
|
266
311
|
}
|
|
267
312
|
|
|
268
|
-
return <BridgeFlowRoute pageUid={pageUid} active={
|
|
269
|
-
}, [getLayoutModel, guardState.allowBridge, guardState.notFound, guardState.pending, pageUid
|
|
313
|
+
return <BridgeFlowRoute pageUid={pageUid} active={active} getLayoutModel={getLayoutModel} />;
|
|
314
|
+
}, [active, getLayoutModel, guardState.allowBridge, guardState.notFound, guardState.pending, pageUid]);
|
|
270
315
|
|
|
271
316
|
return content;
|
|
272
317
|
};
|
|
@@ -795,15 +795,23 @@ describe('FieldAssignRulesEditor', () => {
|
|
|
795
795
|
);
|
|
796
796
|
|
|
797
797
|
expect(screen.getByLabelText('Default value')).toBeInTheDocument();
|
|
798
|
+
expect(screen.getByLabelText('Override value')).toBeInTheDocument();
|
|
798
799
|
expect(screen.getByLabelText('Fixed value')).toBeInTheDocument();
|
|
799
800
|
expect(screen.getByLabelText('Fixed value')).toBeChecked();
|
|
800
801
|
await userEvent.click(screen.getByLabelText('Default value'));
|
|
801
802
|
await waitFor(() => {
|
|
802
803
|
expect(onChange).toHaveBeenCalled();
|
|
803
804
|
});
|
|
804
|
-
|
|
805
|
-
const lastCall = onChange.mock.calls[onChange.mock.calls.length - 1]?.[0] as FieldAssignRuleItem[];
|
|
805
|
+
let lastCall = onChange.mock.calls[onChange.mock.calls.length - 1]?.[0] as FieldAssignRuleItem[];
|
|
806
806
|
expect(lastCall?.[0]?.mode).toBe('default');
|
|
807
|
+
|
|
808
|
+
onChange.mockClear();
|
|
809
|
+
await userEvent.click(screen.getByLabelText('Override value'));
|
|
810
|
+
await waitFor(() => {
|
|
811
|
+
expect(onChange).toHaveBeenCalled();
|
|
812
|
+
});
|
|
813
|
+
lastCall = onChange.mock.calls[onChange.mock.calls.length - 1]?.[0] as FieldAssignRuleItem[];
|
|
814
|
+
expect(lastCall?.[0]?.mode).toBe('override');
|
|
807
815
|
});
|
|
808
816
|
|
|
809
817
|
it('shows assignment mode tooltips for each option', async () => {
|
|
@@ -821,7 +829,7 @@ describe('FieldAssignRulesEditor', () => {
|
|
|
821
829
|
);
|
|
822
830
|
|
|
823
831
|
const questionIcons = Array.from(container.querySelectorAll('.anticon-question-circle'));
|
|
824
|
-
expect(questionIcons).toHaveLength(
|
|
832
|
+
expect(questionIcons).toHaveLength(3);
|
|
825
833
|
|
|
826
834
|
await userEvent.hover(questionIcons[0] as Element);
|
|
827
835
|
await waitFor(() => {
|
|
@@ -835,11 +843,80 @@ describe('FieldAssignRulesEditor', () => {
|
|
|
835
843
|
await userEvent.unhover(questionIcons[0] as Element);
|
|
836
844
|
|
|
837
845
|
await userEvent.hover(questionIcons[1] as Element);
|
|
846
|
+
await waitFor(() => {
|
|
847
|
+
expect(screen.getByText('Overrides existing values until the user edits it.')).toBeInTheDocument();
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
await userEvent.unhover(questionIcons[1] as Element);
|
|
851
|
+
|
|
852
|
+
await userEvent.hover(questionIcons[2] as Element);
|
|
838
853
|
await waitFor(() => {
|
|
839
854
|
expect(screen.getByText('A system-set value. Read-only.')).toBeInTheDocument();
|
|
840
855
|
});
|
|
841
856
|
});
|
|
842
857
|
|
|
858
|
+
it('honors allowed assignment modes', () => {
|
|
859
|
+
const value: FieldAssignRuleItem[] = [
|
|
860
|
+
{
|
|
861
|
+
key: '1',
|
|
862
|
+
enable: true,
|
|
863
|
+
targetPath: 'title',
|
|
864
|
+
mode: 'override',
|
|
865
|
+
},
|
|
866
|
+
];
|
|
867
|
+
|
|
868
|
+
render(
|
|
869
|
+
wrap(
|
|
870
|
+
<FieldAssignRulesEditor
|
|
871
|
+
t={t}
|
|
872
|
+
fieldOptions={[]}
|
|
873
|
+
value={value}
|
|
874
|
+
allowedModes={['default', 'assign']}
|
|
875
|
+
showCondition={false}
|
|
876
|
+
showEnable={false}
|
|
877
|
+
/>,
|
|
878
|
+
),
|
|
879
|
+
);
|
|
880
|
+
|
|
881
|
+
expect(screen.getByLabelText('Default value')).toBeInTheDocument();
|
|
882
|
+
expect(screen.getByLabelText('Fixed value')).toBeInTheDocument();
|
|
883
|
+
expect(screen.queryByLabelText('Override value')).toBeNull();
|
|
884
|
+
expect(screen.getByLabelText('Fixed value')).toBeChecked();
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
it('normalizes disallowed assignment mode before emitting changes', async () => {
|
|
888
|
+
const onChange = vi.fn();
|
|
889
|
+
const value: FieldAssignRuleItem[] = [
|
|
890
|
+
{
|
|
891
|
+
key: '1',
|
|
892
|
+
enable: true,
|
|
893
|
+
targetPath: 'title',
|
|
894
|
+
mode: 'override',
|
|
895
|
+
},
|
|
896
|
+
];
|
|
897
|
+
|
|
898
|
+
render(
|
|
899
|
+
wrap(
|
|
900
|
+
<FieldAssignRulesEditor
|
|
901
|
+
t={t}
|
|
902
|
+
fieldOptions={[]}
|
|
903
|
+
value={value}
|
|
904
|
+
onChange={onChange}
|
|
905
|
+
allowedModes={['default', 'assign']}
|
|
906
|
+
showCondition={false}
|
|
907
|
+
/>,
|
|
908
|
+
),
|
|
909
|
+
);
|
|
910
|
+
|
|
911
|
+
await userEvent.click(screen.getByRole('switch'));
|
|
912
|
+
|
|
913
|
+
await waitFor(() => {
|
|
914
|
+
expect(onChange).toHaveBeenCalled();
|
|
915
|
+
});
|
|
916
|
+
const lastCall = onChange.mock.calls[onChange.mock.calls.length - 1]?.[0] as FieldAssignRuleItem[];
|
|
917
|
+
expect(lastCall?.[0]?.mode).toBe('assign');
|
|
918
|
+
});
|
|
919
|
+
|
|
843
920
|
it('merges extra item tree into base item while keeping order', () => {
|
|
844
921
|
const base: MetaTreeNode[] = [
|
|
845
922
|
{ name: 'formValues', title: 'Current form', type: 'object', paths: ['formValues'] },
|
|
@@ -897,7 +974,7 @@ describe('FieldAssignRulesEditor', () => {
|
|
|
897
974
|
|
|
898
975
|
const itemNode = merged.find((node) => node.name === 'item');
|
|
899
976
|
expect(itemNode).toBeTruthy();
|
|
900
|
-
const parent = (itemNode
|
|
977
|
+
const parent = ((itemNode?.children as MetaTreeNode[]) || []).find((node) => node.name === 'parentItem');
|
|
901
978
|
expect(parent).toBeTruthy();
|
|
902
979
|
|
|
903
980
|
const nestedParent = ((parent?.children as MetaTreeNode[]) || []).find((node) => node.name === 'parentItem');
|
|
@@ -20,8 +20,6 @@ import {
|
|
|
20
20
|
FlowEngineProvider,
|
|
21
21
|
FlowModelRenderer,
|
|
22
22
|
ElementProxy,
|
|
23
|
-
createSafeWindow,
|
|
24
|
-
createSafeDocument,
|
|
25
23
|
createViewScopedEngine,
|
|
26
24
|
} from '@nocobase/flow-engine';
|
|
27
25
|
import { JSEditableFieldModel } from '../../../models/fields/JSEditableFieldModel';
|
|
@@ -167,11 +165,7 @@ describe('useCodeRunner (beforeRender)', () => {
|
|
|
167
165
|
const code = ctx?.inputArgs?.preview?.code || '';
|
|
168
166
|
ctx.onRefReady(ctx.ref, async (el) => {
|
|
169
167
|
ctx.defineProperty('element', { get: () => new ElementProxy(el as any) });
|
|
170
|
-
await ctx.runjs(
|
|
171
|
-
code,
|
|
172
|
-
{ window: createSafeWindow(), document: createSafeDocument() },
|
|
173
|
-
{ preprocessTemplates: true },
|
|
174
|
-
);
|
|
168
|
+
await ctx.runjs(code, undefined, { preprocessTemplates: true });
|
|
175
169
|
});
|
|
176
170
|
},
|
|
177
171
|
},
|
|
@@ -286,16 +280,7 @@ return currentUsername;
|
|
|
286
280
|
const code = ctx?.inputArgs?.preview?.code || '';
|
|
287
281
|
ctx.onRefReady(ctx.ref, async (el) => {
|
|
288
282
|
ctx.defineProperty('element', { get: () => new ElementProxy(el as any) });
|
|
289
|
-
|
|
290
|
-
await ctx.runjs(
|
|
291
|
-
code,
|
|
292
|
-
{
|
|
293
|
-
window: createSafeWindow({ navigator }),
|
|
294
|
-
document: createSafeDocument(),
|
|
295
|
-
navigator,
|
|
296
|
-
},
|
|
297
|
-
{ preprocessTemplates: true },
|
|
298
|
-
);
|
|
283
|
+
await ctx.runjs(code, undefined, { preprocessTemplates: true });
|
|
299
284
|
});
|
|
300
285
|
},
|
|
301
286
|
},
|
|
@@ -9,14 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { useCallback, useRef, useState } from 'react';
|
|
11
11
|
import { parseErrorLineColumn, WRAPPER_PRELUDE_LINES } from '../errorHelpers';
|
|
12
|
-
import {
|
|
13
|
-
FlowModelContext,
|
|
14
|
-
JSRunner,
|
|
15
|
-
createSafeWindow,
|
|
16
|
-
createSafeDocument,
|
|
17
|
-
createSafeNavigator,
|
|
18
|
-
prepareRunJsCode,
|
|
19
|
-
} from '@nocobase/flow-engine';
|
|
12
|
+
import { FlowModelContext, JSRunner, prepareRunJsCode } from '@nocobase/flow-engine';
|
|
20
13
|
|
|
21
14
|
export type RunLog = { level: 'log' | 'info' | 'warn' | 'error'; msg: string; line?: number; column?: number };
|
|
22
15
|
type RunResult = Awaited<ReturnType<JSRunner['run']>>;
|
|
@@ -286,12 +279,7 @@ export function useCodeRunner(hostCtx: FlowModelContext, version = 'v1') {
|
|
|
286
279
|
const eventName = typeof onDef === 'string' ? onDef : onDef?.eventName;
|
|
287
280
|
if (!flow) {
|
|
288
281
|
// 无可用流程(典型场景:联动规则里的 RunJS 预览),直接在当前上下文执行代码
|
|
289
|
-
|
|
290
|
-
await directRunCtx.runjs(
|
|
291
|
-
code,
|
|
292
|
-
{ window: createSafeWindow({ navigator }), document: createSafeDocument(), navigator },
|
|
293
|
-
{ version },
|
|
294
|
-
);
|
|
282
|
+
await directRunCtx.runjs(code, undefined, { version });
|
|
295
283
|
} else if (typeof eventName === 'string') {
|
|
296
284
|
await m.dispatchEvent(eventName, { preview: { code } }, { sequential: true, useCache: false });
|
|
297
285
|
} else if (isManual) {
|
|
@@ -20,10 +20,8 @@ import * as acornWalk from 'acorn-walk';
|
|
|
20
20
|
import {
|
|
21
21
|
JSRunner,
|
|
22
22
|
FlowContext,
|
|
23
|
-
createSafeDocument,
|
|
24
|
-
createSafeNavigator,
|
|
25
|
-
createSafeWindow,
|
|
26
23
|
prepareRunJsCode,
|
|
24
|
+
RUNJS_ALLOWED_BARE_GLOBAL_NAMES,
|
|
27
25
|
shouldPreprocessRunJSTemplates,
|
|
28
26
|
} from '@nocobase/flow-engine';
|
|
29
27
|
|
|
@@ -604,41 +602,7 @@ function collectHeuristicIssues(code: string): RunJSIssue[] {
|
|
|
604
602
|
return issues;
|
|
605
603
|
}
|
|
606
604
|
|
|
607
|
-
const declared = new Set<string>(
|
|
608
|
-
// Common globals / allowed runtime context roots
|
|
609
|
-
'ctx',
|
|
610
|
-
'console',
|
|
611
|
-
'window',
|
|
612
|
-
'document',
|
|
613
|
-
'navigator',
|
|
614
|
-
'Math',
|
|
615
|
-
'Date',
|
|
616
|
-
'Array',
|
|
617
|
-
'Object',
|
|
618
|
-
'Number',
|
|
619
|
-
'String',
|
|
620
|
-
'Boolean',
|
|
621
|
-
'Promise',
|
|
622
|
-
'RegExp',
|
|
623
|
-
'Set',
|
|
624
|
-
'Map',
|
|
625
|
-
'WeakSet',
|
|
626
|
-
'WeakMap',
|
|
627
|
-
'JSON',
|
|
628
|
-
'Intl',
|
|
629
|
-
'URL',
|
|
630
|
-
'Error',
|
|
631
|
-
'TypeError',
|
|
632
|
-
'encodeURIComponent',
|
|
633
|
-
'decodeURIComponent',
|
|
634
|
-
'parseInt',
|
|
635
|
-
'parseFloat',
|
|
636
|
-
'isNaN',
|
|
637
|
-
'isFinite',
|
|
638
|
-
'undefined',
|
|
639
|
-
'NaN',
|
|
640
|
-
'Infinity',
|
|
641
|
-
]);
|
|
605
|
+
const declared = new Set<string>(RUNJS_ALLOWED_BARE_GLOBAL_NAMES);
|
|
642
606
|
|
|
643
607
|
const addId = (id: any) => {
|
|
644
608
|
if (id && typeof id.name === 'string') declared.add(id.name);
|
|
@@ -1036,15 +1000,14 @@ export async function diagnoseRunJS(
|
|
|
1036
1000
|
const { consoleCapture, loggerCapture } = createLogCollectors(logs);
|
|
1037
1001
|
|
|
1038
1002
|
const baseGlobals: Record<string, any> = { console: consoleCapture };
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1003
|
+
if (typeof window !== 'undefined') {
|
|
1004
|
+
baseGlobals.window = window;
|
|
1005
|
+
if (typeof navigator !== 'undefined') {
|
|
1042
1006
|
baseGlobals.navigator = navigator;
|
|
1043
|
-
baseGlobals.window = createSafeWindow({ navigator });
|
|
1044
|
-
baseGlobals.document = createSafeDocument();
|
|
1045
1007
|
}
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1008
|
+
}
|
|
1009
|
+
if (typeof document !== 'undefined') {
|
|
1010
|
+
baseGlobals.document = document;
|
|
1048
1011
|
}
|
|
1049
1012
|
|
|
1050
1013
|
let prepared = src;
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import { tExpr } from '@nocobase/flow-engine';
|
|
11
11
|
import type { ButtonProps } from 'antd/es/button';
|
|
12
12
|
import { CodeEditor } from '../../components/code-editor';
|
|
13
13
|
import { ActionModel } from '../base';
|
|
@@ -72,12 +72,7 @@ ctx.message.info('Hello JS action.');
|
|
|
72
72
|
},
|
|
73
73
|
async handler(ctx, params) {
|
|
74
74
|
const { code, version } = resolveRunJsParams(ctx, params);
|
|
75
|
-
|
|
76
|
-
await ctx.runjs(
|
|
77
|
-
code,
|
|
78
|
-
{ window: createSafeWindow({ navigator }), document: createSafeDocument(), navigator },
|
|
79
|
-
{ version },
|
|
80
|
-
);
|
|
75
|
+
await ctx.runjs(code, undefined, { version });
|
|
81
76
|
},
|
|
82
77
|
},
|
|
83
78
|
},
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import { tExpr } from '@nocobase/flow-engine';
|
|
11
11
|
import type { ButtonProps } from 'antd/es/button';
|
|
12
12
|
import { CodeEditor } from '../../components/code-editor';
|
|
13
13
|
import { ActionModel, ActionSceneEnum } from '../base';
|
|
@@ -78,12 +78,7 @@ if (!rows.length) {
|
|
|
78
78
|
},
|
|
79
79
|
async handler(ctx, params) {
|
|
80
80
|
const { code, version } = resolveRunJsParams(ctx, params);
|
|
81
|
-
|
|
82
|
-
await ctx.runjs(
|
|
83
|
-
code,
|
|
84
|
-
{ window: createSafeWindow({ navigator }), document: createSafeDocument(), navigator },
|
|
85
|
-
{ version },
|
|
86
|
-
);
|
|
81
|
+
await ctx.runjs(code, undefined, { version });
|
|
87
82
|
},
|
|
88
83
|
},
|
|
89
84
|
},
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { ElementProxy,
|
|
10
|
+
import { ElementProxy, tExpr } from '@nocobase/flow-engine';
|
|
11
11
|
import React from 'react';
|
|
12
12
|
import { CodeEditor } from '../../components/code-editor';
|
|
13
13
|
import { ActionModel, ActionSceneEnum, CollectionActionGroupModel, RecordActionGroupModel } from '../base';
|
|
@@ -137,12 +137,7 @@ JSItemActionModel.registerFlow({
|
|
|
137
137
|
ctx.defineProperty('element', {
|
|
138
138
|
get: () => new ElementProxy(element),
|
|
139
139
|
});
|
|
140
|
-
|
|
141
|
-
await ctx.runjs(
|
|
142
|
-
code,
|
|
143
|
-
{ window: createSafeWindow({ navigator }), document: createSafeDocument(), navigator },
|
|
144
|
-
{ version },
|
|
145
|
-
);
|
|
140
|
+
await ctx.runjs(code, undefined, { version });
|
|
146
141
|
});
|
|
147
142
|
},
|
|
148
143
|
},
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import { tExpr } from '@nocobase/flow-engine';
|
|
11
11
|
import type { ButtonProps } from 'antd/es/button';
|
|
12
12
|
import { CodeEditor } from '../../components/code-editor';
|
|
13
13
|
import { ActionSceneEnum, RecordActionModel } from '../base';
|
|
@@ -78,12 +78,7 @@ if (!ctx.record) {
|
|
|
78
78
|
},
|
|
79
79
|
async handler(ctx, params) {
|
|
80
80
|
const { code, version } = resolveRunJsParams(ctx, params);
|
|
81
|
-
|
|
82
|
-
await ctx.runjs(
|
|
83
|
-
code,
|
|
84
|
-
{ window: createSafeWindow({ navigator }), document: createSafeDocument(), navigator },
|
|
85
|
-
{ version },
|
|
86
|
-
);
|
|
81
|
+
await ctx.runjs(code, undefined, { version });
|
|
87
82
|
},
|
|
88
83
|
},
|
|
89
84
|
},
|
|
@@ -18,6 +18,17 @@ import { commonConditionHandler, ConditionBuilder } from '../../components/Condi
|
|
|
18
18
|
export { ActionModel, ActionSceneEnum, ActionWithoutPermission, type ActionSceneType } from './ActionModelCore';
|
|
19
19
|
import { ActionModel } from './ActionModelCore';
|
|
20
20
|
|
|
21
|
+
function getMobileDefaultProps(ctx) {
|
|
22
|
+
const defaultProps = ctx.model.defaultProps || {};
|
|
23
|
+
if (ctx.isMobileLayout && defaultProps.icon) {
|
|
24
|
+
return {
|
|
25
|
+
...defaultProps,
|
|
26
|
+
iconOnly: true,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
return defaultProps;
|
|
30
|
+
}
|
|
31
|
+
|
|
21
32
|
ActionModel.registerFlow({
|
|
22
33
|
key: 'buttonSettings',
|
|
23
34
|
title: tExpr('Button settings'),
|
|
@@ -80,7 +91,7 @@ ActionModel.registerFlow({
|
|
|
80
91
|
};
|
|
81
92
|
},
|
|
82
93
|
defaultParams(ctx) {
|
|
83
|
-
const defaultProps = ctx
|
|
94
|
+
const defaultProps = getMobileDefaultProps(ctx);
|
|
84
95
|
if (!ctx.model.enableEditColor) {
|
|
85
96
|
return defaultProps;
|
|
86
97
|
}
|
|
@@ -75,6 +75,12 @@ interface DragState {
|
|
|
75
75
|
generatedIds: Map<string, string>;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
const MOBILE_GRID_DISABLED_DROP_SLOT_TYPES = new Set<LayoutSlot['type']>(['column-edge', 'item-edge', 'empty-column']);
|
|
79
|
+
|
|
80
|
+
const filterMobileGridDragSlots = (slots: LayoutSlot[]) => {
|
|
81
|
+
return slots.filter((slot) => !MOBILE_GRID_DISABLED_DROP_SLOT_TYPES.has(slot.type));
|
|
82
|
+
};
|
|
83
|
+
|
|
78
84
|
const getClientPoint = (event: any): { x: number; y: number } | null => {
|
|
79
85
|
if (!event) {
|
|
80
86
|
return null;
|
|
@@ -625,7 +631,7 @@ export class GridModel<T extends { subModels: { items: FlowModel[] } } = Default
|
|
|
625
631
|
return;
|
|
626
632
|
}
|
|
627
633
|
const snapshot = buildLayoutSnapshot({ container: this.getDragContainer() });
|
|
628
|
-
this.dragState.slots = snapshot.slots;
|
|
634
|
+
this.dragState.slots = this.context.isMobileLayout ? filterMobileGridDragSlots(snapshot.slots) : snapshot.slots;
|
|
629
635
|
this.dragState.containerRect = snapshot.containerRect;
|
|
630
636
|
}
|
|
631
637
|
|
|
@@ -1135,7 +1141,7 @@ GridModel.registerFlow({
|
|
|
1135
1141
|
/**
|
|
1136
1142
|
* 将多列栅格 rows 转换为移动端单列 rows。
|
|
1137
1143
|
* 遍历原 rows 的行与列顺序,把每个原列(过滤空白列后)变成一个新行(仅一列)。
|
|
1138
|
-
* 行 key
|
|
1144
|
+
* 行 key 使用原 rowId 与 columnIndex 生成,避免每次渲染产生随机 key 导致移动端滚动位置丢失。
|
|
1139
1145
|
* @param rows 原始多列 rows
|
|
1140
1146
|
* @returns 单列 rows
|
|
1141
1147
|
*/
|
|
@@ -1145,13 +1151,38 @@ export function transformRowsToSingleColumn(
|
|
|
1145
1151
|
): Record<string, string[][]> {
|
|
1146
1152
|
const emptyColumnUid = options?.emptyColumnUid ?? EMPTY_COLUMN_UID;
|
|
1147
1153
|
const singleColumnRows: Record<string, string[][]> = {};
|
|
1154
|
+
|
|
1155
|
+
const getAvailableRowId = (baseRowId: string) => {
|
|
1156
|
+
if (!singleColumnRows[baseRowId]) {
|
|
1157
|
+
return baseRowId;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
let duplicateIndex = 1;
|
|
1161
|
+
let rowId = `${baseRowId}:duplicate:${duplicateIndex}`;
|
|
1162
|
+
while (singleColumnRows[rowId]) {
|
|
1163
|
+
duplicateIndex += 1;
|
|
1164
|
+
rowId = `${baseRowId}:duplicate:${duplicateIndex}`;
|
|
1165
|
+
}
|
|
1166
|
+
return rowId;
|
|
1167
|
+
};
|
|
1168
|
+
|
|
1148
1169
|
Object.keys(rows).forEach((rowId) => {
|
|
1149
1170
|
const columns = rows[rowId];
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
}
|
|
1171
|
+
const effectiveColumns = columns
|
|
1172
|
+
.map((column, columnIndex) => ({
|
|
1173
|
+
columnIndex,
|
|
1174
|
+
items: column.filter((id) => id !== emptyColumnUid),
|
|
1175
|
+
}))
|
|
1176
|
+
.filter((column) => column.items.length > 0);
|
|
1177
|
+
|
|
1178
|
+
if (effectiveColumns.length === 1 && effectiveColumns[0].columnIndex === 0) {
|
|
1179
|
+
singleColumnRows[getAvailableRowId(rowId)] = [effectiveColumns[0].items];
|
|
1180
|
+
return;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
effectiveColumns.forEach((column) => {
|
|
1184
|
+
const generatedRowId = `mobile:${encodeURIComponent(rowId)}:${column.columnIndex}`;
|
|
1185
|
+
singleColumnRows[getAvailableRowId(generatedRowId)] = [column.items];
|
|
1155
1186
|
});
|
|
1156
1187
|
});
|
|
1157
1188
|
return singleColumnRows;
|
|
@@ -375,12 +375,15 @@ export class PageModel extends FlowModel<PageModelStructure> {
|
|
|
375
375
|
tabBarStyle={this.props.tabBarStyle}
|
|
376
376
|
items={this.mapTabs()}
|
|
377
377
|
onChange={(activeKey) => {
|
|
378
|
+
const previousActiveKey = this.props.tabActiveKey || this.getActiveTabKey();
|
|
378
379
|
this.context.view.navigation?.changeTo?.({
|
|
379
380
|
tabUid: activeKey,
|
|
380
381
|
});
|
|
381
382
|
|
|
382
383
|
this.invokeTabModelLifecycleMethod(activeKey, 'onActive');
|
|
383
|
-
|
|
384
|
+
if (previousActiveKey && previousActiveKey !== activeKey) {
|
|
385
|
+
this.invokeTabModelLifecycleMethod(previousActiveKey, 'onInactive');
|
|
386
|
+
}
|
|
384
387
|
this.setProps('tabActiveKey', activeKey);
|
|
385
388
|
}}
|
|
386
389
|
// destroyInactiveTabPane
|
|
@@ -140,13 +140,9 @@ vi.mock('@nocobase/flow-engine', () => {
|
|
|
140
140
|
jioToJoiSchema: () => ({
|
|
141
141
|
validate: (value: any) => ({ value }),
|
|
142
142
|
}),
|
|
143
|
-
createSafeWindow: () => window,
|
|
144
|
-
createSafeDocument: () => document,
|
|
145
|
-
createSafeNavigator: () => navigator,
|
|
146
143
|
isRunJSValue: () => false,
|
|
147
144
|
isVariableExpression: () => false,
|
|
148
145
|
normalizeRunJSValue: (value: any) => value,
|
|
149
|
-
runjsWithSafeGlobals: async (handler: any, ...args: any[]) => handler?.(...args),
|
|
150
146
|
parseCtxDateExpression: (value: any) => value,
|
|
151
147
|
setupRunJSContexts: () => undefined,
|
|
152
148
|
getRunJSScenesForContext: () => [],
|
|
@@ -452,6 +448,34 @@ describe('PageModel', () => {
|
|
|
452
448
|
marginInlineEnd: 24,
|
|
453
449
|
});
|
|
454
450
|
});
|
|
451
|
+
|
|
452
|
+
it('should deactivate the previous tab when navigation updates active tab synchronously', () => {
|
|
453
|
+
pageModel.props = { tabActiveKey: 'tab-old' } as any;
|
|
454
|
+
const invokeSpy = vi.spyOn(pageModel as any, 'invokeTabModelLifecycleMethod').mockImplementation(() => undefined);
|
|
455
|
+
const changeTo = vi.fn((params: { tabUid: string }) => {
|
|
456
|
+
pageModel.props.tabActiveKey = params.tabUid;
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
pageModel.context.view = {
|
|
460
|
+
navigation: {
|
|
461
|
+
viewParams: {
|
|
462
|
+
tabUid: 'tab-old',
|
|
463
|
+
},
|
|
464
|
+
changeTo,
|
|
465
|
+
},
|
|
466
|
+
} as any;
|
|
467
|
+
|
|
468
|
+
const result = pageModel.renderTabs() as any;
|
|
469
|
+
const tabsElement = result.props.children;
|
|
470
|
+
|
|
471
|
+
tabsElement.props.onChange('tab-new');
|
|
472
|
+
|
|
473
|
+
expect(changeTo).toHaveBeenCalledWith({ tabUid: 'tab-new' });
|
|
474
|
+
expect(invokeSpy).toHaveBeenNthCalledWith(1, 'tab-new', 'onActive');
|
|
475
|
+
expect(invokeSpy).toHaveBeenNthCalledWith(2, 'tab-old', 'onInactive');
|
|
476
|
+
expect(invokeSpy).not.toHaveBeenCalledWith('tab-new', 'onInactive');
|
|
477
|
+
expect(pageModel.props.tabActiveKey).toBe('tab-new');
|
|
478
|
+
});
|
|
455
479
|
});
|
|
456
480
|
|
|
457
481
|
describe('render header spacing with tabs', () => {
|