@nocobase/client-v2 2.2.0-alpha.6 → 2.2.0-alpha.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/es/BaseApplication.d.ts +1 -0
  2. package/es/collection-field-interface/CollectionFieldInterface.d.ts +1 -0
  3. package/es/collection-field-interface/CollectionFieldInterfaceManager.d.ts +1 -0
  4. package/es/flow/components/FieldAssignExactDatePicker.d.ts +1 -0
  5. package/es/flow/components/FieldAssignValueInput.d.ts +1 -0
  6. package/es/flow/components/RunJSValueEditor.d.ts +1 -0
  7. package/es/flow/models/blocks/form/QuickEditFormModel.d.ts +17 -2
  8. package/es/index.mjs +86 -86
  9. package/lib/index.js +95 -95
  10. package/package.json +8 -7
  11. package/src/BaseApplication.tsx +9 -5
  12. package/src/__tests__/app.test.tsx +26 -0
  13. package/src/collection-field-interface/CollectionFieldInterface.ts +1 -0
  14. package/src/collection-field-interface/CollectionFieldInterfaceManager.ts +1 -0
  15. package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +145 -2
  16. package/src/components/form/ScanInput/useCodeScanner.ts +154 -2
  17. package/src/flow/FlowPage.tsx +9 -1
  18. package/src/flow/__tests__/FlowPage.test.tsx +50 -3
  19. package/src/flow/__tests__/FlowRoute.test.tsx +2 -2
  20. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +0 -1
  21. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +253 -6
  22. package/src/flow/components/FieldAssignExactDatePicker.tsx +25 -11
  23. package/src/flow/components/FieldAssignValueInput.tsx +60 -15
  24. package/src/flow/components/FlowRoute.tsx +7 -3
  25. package/src/flow/components/RunJSValueEditor.tsx +9 -1
  26. package/src/flow/components/__tests__/FieldAssignValueInput.context.test.tsx +134 -0
  27. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +1 -0
  28. package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +27 -12
  29. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +36 -0
  30. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +34 -0
  31. package/src/flow/models/blocks/form/QuickEditFormModel.tsx +189 -36
  32. package/src/flow/models/blocks/form/__tests__/QuickEditFormModel.quickEdit.test.ts +350 -2
  33. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +1 -0
  34. package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +20 -10
  35. package/src/flow/models/fields/mobile-components/MobileSelect.tsx +24 -12
@@ -11,23 +11,58 @@ import { FlowEngine, FlowEngineProvider } from '@nocobase/flow-engine';
11
11
  import { act, render, screen, waitFor } from '@testing-library/react';
12
12
  import React, { useEffect } from 'react';
13
13
  import { createMemoryRouter, RouterProvider, useParams } from 'react-router-dom';
14
- import { describe, expect, it, vi } from 'vitest';
14
+ import { afterEach, describe, expect, it, vi } from 'vitest';
15
+ import { NocoBaseDesktopRouteType, type NocoBaseDesktopRoute } from '../../../../flow-compat';
15
16
  import type { LayoutDefinition } from '../../../../layout-manager/types';
16
17
  import { AdminLayoutComponent } from '../AdminLayoutComponent';
17
18
  import { AdminLayoutModel } from '../AdminLayoutModel';
19
+ import { AdminLayoutMenuItemModel } from '../AdminLayoutMenuModels';
18
20
  import { AdminLayoutContent } from '../AdminLayoutSlotModels';
19
21
 
22
+ const proLayoutLifecycle = vi.hoisted(() => ({
23
+ events: [] as string[],
24
+ routes: [] as Array<{ children?: Array<{ name?: React.ReactNode }> }>,
25
+ }));
26
+
20
27
  vi.mock('@ant-design/pro-layout', async () => {
21
28
  const ReactModule = await import('react');
22
29
  const RouteContext = ReactModule.createContext({ isMobile: false });
30
+ const ProLayoutMock = (props: {
31
+ children?: React.ReactNode;
32
+ route?: { children?: Array<{ name?: React.ReactNode }> };
33
+ }) => {
34
+ ReactModule.useEffect(() => {
35
+ proLayoutLifecycle.events.push('pro-layout:mount');
36
+ return () => {
37
+ proLayoutLifecycle.events.push('pro-layout:unmount');
38
+ };
39
+ }, []);
40
+ proLayoutLifecycle.routes.push(props.route || {});
23
41
 
24
- return {
25
- default: (props: { children?: React.ReactNode }) =>
42
+ return ReactModule.createElement(
43
+ RouteContext.Provider,
44
+ { value: { isMobile: false } },
26
45
  ReactModule.createElement(
27
- RouteContext.Provider,
28
- { value: { isMobile: false } },
29
- ReactModule.createElement('div', { 'data-testid': 'pro-layout' }, props.children),
46
+ 'div',
47
+ { 'data-testid': 'pro-layout' },
48
+ ReactModule.createElement(
49
+ 'nav',
50
+ { 'data-testid': 'pro-layout-menu' },
51
+ (props.route?.children || []).map((item) =>
52
+ ReactModule.createElement(
53
+ 'span',
54
+ { key: String(item.name), 'data-testid': 'pro-layout-menu-item' },
55
+ item.name,
56
+ ),
57
+ ),
58
+ ),
59
+ props.children,
30
60
  ),
61
+ );
62
+ };
63
+
64
+ return {
65
+ default: ProLayoutMock,
31
66
  RouteContext,
32
67
  };
33
68
  });
@@ -53,6 +88,10 @@ vi.mock('../AppListRender', () => ({
53
88
  useAppListRender: () => undefined,
54
89
  }));
55
90
 
91
+ afterEach(() => {
92
+ delete (window as Window & { __nocobase_modern_client_prefix__?: string }).__nocobase_modern_client_prefix__;
93
+ });
94
+
56
95
  describe('AdminLayoutComponent', () => {
57
96
  it('keeps custom admin layout pages alive by layout route name', async () => {
58
97
  const layout: LayoutDefinition = {
@@ -185,4 +224,212 @@ describe('AdminLayoutComponent', () => {
185
224
 
186
225
  expect(deactivateLayout).toHaveBeenCalledTimes(1);
187
226
  });
227
+
228
+ it('keeps layout content mounted when the menu route tree refreshes', async () => {
229
+ proLayoutLifecycle.events = [];
230
+ proLayoutLifecycle.routes = [];
231
+ const layout: LayoutDefinition = {
232
+ routeName: 'admin',
233
+ routePath: '/admin',
234
+ rootRouteName: 'admin',
235
+ uid: 'admin-layout-model',
236
+ layoutModelClass: 'AdminLayoutModel',
237
+ rootPageModelClass: 'RootPageModel',
238
+ childPageModelClass: 'ChildPageModel',
239
+ authCheck: true,
240
+ };
241
+ const events: string[] = [];
242
+ const engine = new FlowEngine();
243
+ engine.context.defineProperty('routeRepository', {
244
+ value: {
245
+ activateLayout: vi.fn(() => vi.fn()),
246
+ ensureAccessibleLoaded: vi.fn(async () => []),
247
+ listAccessible: () => [],
248
+ subscribe: vi.fn(),
249
+ unsubscribe: vi.fn(),
250
+ },
251
+ });
252
+ engine.context.defineProperty('t', {
253
+ value: (key: string) => key,
254
+ });
255
+ const model = engine.createModel<AdminLayoutModel>({
256
+ uid: layout.uid,
257
+ use: AdminLayoutModel,
258
+ props: {
259
+ layout,
260
+ },
261
+ });
262
+
263
+ const Page = () => {
264
+ useEffect(() => {
265
+ events.push('page:mount');
266
+ return () => {
267
+ events.push('page:unmount');
268
+ };
269
+ }, []);
270
+
271
+ return <div data-testid="custom-admin-route">Workflow tasks</div>;
272
+ };
273
+
274
+ const router = createMemoryRouter(
275
+ [
276
+ {
277
+ id: layout.routeName,
278
+ path: layout.routePath,
279
+ element: <AdminLayoutComponent model={model} />,
280
+ children: [
281
+ {
282
+ id: 'admin.workflow.tasks',
283
+ path: 'workflow/tasks/:taskType?/:status?/:popupId?',
284
+ element: <Page />,
285
+ },
286
+ ],
287
+ },
288
+ ],
289
+ {
290
+ initialEntries: ['/admin/workflow/tasks/approval-apply/pending'],
291
+ },
292
+ );
293
+
294
+ render(
295
+ <FlowEngineProvider engine={engine}>
296
+ <RouterProvider router={router} />
297
+ </FlowEngineProvider>,
298
+ );
299
+
300
+ expect(await screen.findByTestId('custom-admin-route')).toBeInTheDocument();
301
+ expect(proLayoutLifecycle.events).toEqual(['pro-layout:mount']);
302
+
303
+ act(() => {
304
+ model.refreshMenuRouteTree();
305
+ });
306
+
307
+ await act(async () => {
308
+ await router.navigate('/admin/workflow/tasks/approval-apply/pending/1');
309
+ });
310
+
311
+ expect(screen.getByTestId('custom-admin-route')).toBeInTheDocument();
312
+ expect(proLayoutLifecycle.events).toEqual(['pro-layout:mount']);
313
+ expect(events).toEqual(['page:mount']);
314
+ });
315
+
316
+ it('updates ProLayout menu routes after accessible routes change without remounting the layout', async () => {
317
+ proLayoutLifecycle.events = [];
318
+ proLayoutLifecycle.routes = [];
319
+ (window as Window & { __nocobase_modern_client_prefix__?: string }).__nocobase_modern_client_prefix__ = 'v2';
320
+ const layout: LayoutDefinition = {
321
+ routeName: 'admin',
322
+ routePath: '/admin',
323
+ rootRouteName: 'admin',
324
+ uid: 'admin-layout-model',
325
+ layoutModelClass: 'AdminLayoutModel',
326
+ rootPageModelClass: 'RootPageModel',
327
+ childPageModelClass: 'ChildPageModel',
328
+ authCheck: true,
329
+ };
330
+ let accessibleRoutes: NocoBaseDesktopRoute[] = [
331
+ {
332
+ id: 1,
333
+ title: 'Visible page',
334
+ schemaUid: 'visible-page',
335
+ type: NocoBaseDesktopRouteType.flowPage,
336
+ },
337
+ ];
338
+ let routeRepositorySubscriber: (() => void) | undefined;
339
+ const events: string[] = [];
340
+ const engine = new FlowEngine();
341
+ engine.registerModels({
342
+ AdminLayoutMenuItemModel,
343
+ });
344
+ engine.context.defineProperty('routeRepository', {
345
+ value: {
346
+ activateLayout: vi.fn(() => vi.fn()),
347
+ ensureAccessibleLoaded: vi.fn(async () => accessibleRoutes),
348
+ listAccessible: () => accessibleRoutes,
349
+ subscribe: vi.fn((subscriber: () => void) => {
350
+ routeRepositorySubscriber = subscriber;
351
+ }),
352
+ unsubscribe: vi.fn(),
353
+ },
354
+ });
355
+ engine.context.defineProperty('app', {
356
+ value: {
357
+ getPublicPath: () => '/v/',
358
+ router: {
359
+ getBasename: () => '/v',
360
+ },
361
+ },
362
+ });
363
+ engine.context.defineProperty('t', {
364
+ value: (key: string) => key,
365
+ });
366
+ const model = engine.createModel<AdminLayoutModel>({
367
+ uid: layout.uid,
368
+ use: AdminLayoutModel,
369
+ props: {
370
+ layout,
371
+ },
372
+ });
373
+
374
+ const Page = () => {
375
+ useEffect(() => {
376
+ events.push('page:mount');
377
+ return () => {
378
+ events.push('page:unmount');
379
+ };
380
+ }, []);
381
+
382
+ return <div data-testid="custom-admin-route">Workflow tasks</div>;
383
+ };
384
+
385
+ const router = createMemoryRouter(
386
+ [
387
+ {
388
+ id: layout.routeName,
389
+ path: layout.routePath,
390
+ element: <AdminLayoutComponent model={model} />,
391
+ children: [
392
+ {
393
+ id: 'admin.workflow.tasks',
394
+ path: 'workflow/tasks/:taskType?/:status?/:popupId?',
395
+ element: <Page />,
396
+ },
397
+ ],
398
+ },
399
+ ],
400
+ {
401
+ initialEntries: ['/admin/workflow/tasks/approval-apply/pending'],
402
+ },
403
+ );
404
+
405
+ render(
406
+ <FlowEngineProvider engine={engine}>
407
+ <RouterProvider router={router} />
408
+ </FlowEngineProvider>,
409
+ );
410
+
411
+ expect(await screen.findByText('Visible page')).toBeInTheDocument();
412
+ expect(proLayoutLifecycle.events).toEqual(['pro-layout:mount']);
413
+
414
+ act(() => {
415
+ accessibleRoutes = [];
416
+ routeRepositorySubscriber?.();
417
+ });
418
+
419
+ await waitFor(() => {
420
+ expect(screen.queryByText('Visible page')).not.toBeInTheDocument();
421
+ });
422
+
423
+ await act(async () => {
424
+ await router.navigate('/admin/workflow/tasks/approval-apply/pending/1');
425
+ });
426
+
427
+ expect(screen.getByTestId('custom-admin-route')).toBeInTheDocument();
428
+ expect(proLayoutLifecycle.events).toEqual(['pro-layout:mount']);
429
+ expect(events).toEqual(['page:mount']);
430
+ expect(
431
+ proLayoutLifecycle.routes.some((route) => route.children?.some((item) => item.name === 'Visible page')),
432
+ ).toBe(true);
433
+ expect(proLayoutLifecycle.routes.at(-1)?.children?.some((item) => item.name === 'Visible page')).toBe(false);
434
+ });
188
435
  });
@@ -7,7 +7,7 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import React, { useMemo, useState } from 'react';
10
+ import React, { useCallback, useMemo, useState } from 'react';
11
11
  import { DatePicker, Select, Space } from 'antd';
12
12
  import { inferPickerType } from '../../flow-compat';
13
13
  import { dayjs, getDateTimeFormat, getPickerFormat } from '@nocobase/utils/client';
@@ -29,6 +29,7 @@ export interface FieldAssignExactDatePickerProps {
29
29
  isRange?: boolean;
30
30
  onChange?: (value: ExactDatePickerValue) => void;
31
31
  style?: React.CSSProperties;
32
+ disabled?: boolean;
32
33
  }
33
34
 
34
35
  function getRawSingleValue(value: unknown, isRange: boolean): unknown {
@@ -114,7 +115,7 @@ function parseRangeValue(value: unknown, format: string): [dayjs.Dayjs, dayjs.Da
114
115
  }
115
116
 
116
117
  export const FieldAssignExactDatePicker: React.FC<FieldAssignExactDatePickerProps> = (props) => {
117
- const { picker = 'date', format, showTime, timeFormat, value, isRange = false, onChange, style } = props;
118
+ const { picker = 'date', format, showTime, timeFormat, value, isRange = false, onChange, style, disabled } = props;
118
119
  const flowCtx = useFlowContext();
119
120
  const t = flowCtx.model.translate.bind(flowCtx.model);
120
121
 
@@ -124,17 +125,17 @@ export const FieldAssignExactDatePicker: React.FC<FieldAssignExactDatePickerProp
124
125
  return inferPickerFromRawValue(rawSingleValue, picker);
125
126
  });
126
127
 
127
- const getResolvedFormat = (nextPicker: ExactDatePickerMode) => {
128
- const baseFormat = nextPicker === picker && format ? format : getPickerFormat(nextPicker);
129
- const dateFormat = nextPicker === 'date' ? stripTimeFromFormat(baseFormat) : baseFormat;
130
- return getDateTimeFormat(nextPicker, dateFormat, showTime, timeFormat);
131
- };
132
-
133
- const resolvedFormat = useMemo(
134
- () => getResolvedFormat(targetPicker),
135
- [targetPicker, format, picker, showTime, timeFormat],
128
+ const getResolvedFormat = useCallback(
129
+ (nextPicker: ExactDatePickerMode) => {
130
+ const baseFormat = nextPicker === picker && format ? format : getPickerFormat(nextPicker);
131
+ const dateFormat = nextPicker === 'date' ? stripTimeFromFormat(baseFormat) : baseFormat;
132
+ return getDateTimeFormat(nextPicker, dateFormat, showTime, timeFormat);
133
+ },
134
+ [format, picker, showTime, timeFormat],
136
135
  );
137
136
 
137
+ const resolvedFormat = useMemo(() => getResolvedFormat(targetPicker), [getResolvedFormat, targetPicker]);
138
+
138
139
  const singleValue = useMemo(() => {
139
140
  if (isRange) return null;
140
141
  return parseDateFromRawValue(value, resolvedFormat);
@@ -156,6 +157,10 @@ export const FieldAssignExactDatePicker: React.FC<FieldAssignExactDatePickerProp
156
157
  );
157
158
 
158
159
  const handlePickerTypeChange = (nextPicker: ExactDatePickerMode) => {
160
+ if (disabled) {
161
+ return;
162
+ }
163
+
159
164
  setTargetPicker(nextPicker);
160
165
 
161
166
  if (!onChange) {
@@ -182,7 +187,11 @@ export const FieldAssignExactDatePicker: React.FC<FieldAssignExactDatePickerProp
182
187
  inputReadOnly: flowCtx.isMobileLayout,
183
188
  showTime: showTime ? { defaultValue: dayjs('00:00:00', 'HH:mm:ss') } : false,
184
189
  value: singleValue,
190
+ disabled,
185
191
  onChange: (nextDate: dayjs.Dayjs | null) => {
192
+ if (disabled) {
193
+ return;
194
+ }
186
195
  if (nextDate && dayjs.isDayjs(nextDate)) {
187
196
  onChange?.(nextDate);
188
197
  return;
@@ -200,7 +209,11 @@ export const FieldAssignExactDatePicker: React.FC<FieldAssignExactDatePickerProp
200
209
  inputReadOnly: flowCtx.isMobileLayout,
201
210
  showTime: showTime ? { defaultValue: [dayjs('00:00:00', 'HH:mm:ss'), dayjs('23:59:59', 'HH:mm:ss')] } : false,
202
211
  value: rangeValue,
212
+ disabled,
203
213
  onChange: (nextDates: [dayjs.Dayjs | null, dayjs.Dayjs | null] | null) => {
214
+ if (disabled) {
215
+ return;
216
+ }
204
217
  if (Array.isArray(nextDates) && nextDates[0] && nextDates[1]) {
205
218
  onChange?.([nextDates[0], nextDates[1]]);
206
219
  return;
@@ -218,6 +231,7 @@ export const FieldAssignExactDatePicker: React.FC<FieldAssignExactDatePickerProp
218
231
  value={targetPicker}
219
232
  options={pickerOptions}
220
233
  onChange={handlePickerTypeChange}
234
+ disabled={disabled}
221
235
  />
222
236
  {isRange ? <DatePicker.RangePicker {...rangePickerProps} /> : <DatePicker {...singlePickerProps} />}
223
237
  </Space.Compact>
@@ -344,6 +344,7 @@ interface Props {
344
344
  /** 是否允许在变量选择器中使用 RunJS。默认 true,保持历史行为。 */
345
345
  allowRunJS?: boolean;
346
346
  maxAssociationFieldDepth?: number;
347
+ disabled?: boolean;
347
348
  }
348
349
 
349
350
  type ResolvedFieldContext = {
@@ -716,6 +717,7 @@ export const FieldAssignValueInput: React.FC<Props> = ({
716
717
  enableDateVariableAsConstant = false,
717
718
  allowRunJS = true,
718
719
  maxAssociationFieldDepth = 2,
720
+ disabled = false,
719
721
  }) => {
720
722
  const flowCtx = useFlowContext<FlowModelContext>();
721
723
  const normalizeEventValue = React.useCallback((eventOrValue: unknown) => {
@@ -1031,18 +1033,22 @@ export const FieldAssignValueInput: React.FC<Props> = ({
1031
1033
  collectionField: effectiveCollectionField,
1032
1034
  });
1033
1035
 
1034
- const created = engine?.createModel?.({
1035
- use: 'VariableFieldFormModel',
1036
- subModels: {
1037
- fields: [
1038
- {
1039
- use: effectiveFieldModelUse,
1040
- stepParams: tempFieldStepParams,
1041
- props: tempFieldProps,
1042
- },
1043
- ],
1036
+ const sourceContext = resolved?.itemModel?.context || flowCtx.model?.context;
1037
+ const created = engine?.createModel?.(
1038
+ {
1039
+ use: 'VariableFieldFormModel',
1040
+ subModels: {
1041
+ fields: [
1042
+ {
1043
+ use: effectiveFieldModelUse,
1044
+ stepParams: tempFieldStepParams,
1045
+ props: tempFieldProps,
1046
+ },
1047
+ ],
1048
+ },
1044
1049
  },
1045
- });
1050
+ sourceContext ? { delegate: sourceContext } : undefined,
1051
+ );
1046
1052
  if (!created) return;
1047
1053
 
1048
1054
  // 注入上下文(集合/数据源/字段/区块/资源)
@@ -1076,7 +1082,7 @@ export const FieldAssignValueInput: React.FC<Props> = ({
1076
1082
  fm.setStepParams('selectSettings', 'fieldNames', { label: overrideLabel });
1077
1083
  }
1078
1084
  fm?.setProps?.({
1079
- disabled: false,
1085
+ disabled,
1080
1086
  readPretty: false,
1081
1087
  pattern: 'editable',
1082
1088
  updateAssociation: false,
@@ -1139,6 +1145,7 @@ export const FieldAssignValueInput: React.FC<Props> = ({
1139
1145
  preferFormItemFieldModel,
1140
1146
  associationFieldNamesOverride?.label,
1141
1147
  associationFieldNamesOverride?.value,
1148
+ disabled,
1142
1149
  ]);
1143
1150
 
1144
1151
  // 当传入 operator / operatorMetaList 时,按 operator schema 适配临时字段的输入组件与 props。
@@ -1191,6 +1198,9 @@ export const FieldAssignValueInput: React.FC<Props> = ({
1191
1198
  React.useEffect(() => {
1192
1199
  const coercedValue = coerceEmptyValueForRenderer(inputProps?.value);
1193
1200
  const handleChange = (ev: any) => {
1201
+ if (inputProps?.disabled) {
1202
+ return;
1203
+ }
1194
1204
  const nextRaw = normalizeEventValue(ev);
1195
1205
  const normalizedForStore = operator ? normalizeFilterValueByOperator(operator, nextRaw) : nextRaw;
1196
1206
  const nextValue = coerceEmptyValueForRenderer(normalizedForStore);
@@ -1222,6 +1232,7 @@ export const FieldAssignValueInput: React.FC<Props> = ({
1222
1232
  value={inputProps?.value}
1223
1233
  onChange={(e) => inputProps?.onChange?.(normalizeEventValue(e))}
1224
1234
  placeholder={placeholder}
1235
+ disabled={inputProps?.disabled}
1225
1236
  style={withFullWidthStyle(wrapperStyle)}
1226
1237
  />
1227
1238
  );
@@ -1240,6 +1251,7 @@ export const FieldAssignValueInput: React.FC<Props> = ({
1240
1251
  const C: React.FC<any> = (inputProps) => {
1241
1252
  const wrapperStyle = pickStyle(inputProps?.style);
1242
1253
  const raw = inputProps?.value;
1254
+ const isDisabled = Boolean(inputProps?.disabled);
1243
1255
  const parsed = isCtxDateExpression(raw) ? parseCtxDateExpression(raw) : raw;
1244
1256
  const parsedValue = typeof parsed === 'undefined' ? undefined : parsed;
1245
1257
  const { token } = theme.useToken();
@@ -1264,6 +1276,9 @@ export const FieldAssignValueInput: React.FC<Props> = ({
1264
1276
  });
1265
1277
 
1266
1278
  const handleSelect = (val: string) => {
1279
+ if (isDisabled) {
1280
+ return;
1281
+ }
1267
1282
  setOpen(false);
1268
1283
  if (val === 'exact') {
1269
1284
  inputProps?.onChange?.('');
@@ -1278,10 +1293,16 @@ export const FieldAssignValueInput: React.FC<Props> = ({
1278
1293
  };
1279
1294
 
1280
1295
  const handleExactSingleChange = (nextValue: any) => {
1296
+ if (isDisabled) {
1297
+ return;
1298
+ }
1281
1299
  inputProps?.onChange?.(nextValue || '');
1282
1300
  };
1283
1301
 
1284
1302
  const handleExactRangeChange = (nextValue: any) => {
1303
+ if (isDisabled) {
1304
+ return;
1305
+ }
1285
1306
  inputProps?.onChange?.(nextValue || '');
1286
1307
  };
1287
1308
 
@@ -1326,7 +1347,11 @@ export const FieldAssignValueInput: React.FC<Props> = ({
1326
1347
  <Select
1327
1348
  options={options}
1328
1349
  open={open}
1329
- onDropdownVisibleChange={setOpen}
1350
+ onDropdownVisibleChange={(nextOpen) => {
1351
+ if (!isDisabled) {
1352
+ setOpen(nextOpen);
1353
+ }
1354
+ }}
1330
1355
  allowClear={false}
1331
1356
  style={{
1332
1357
  width: '100%',
@@ -1336,13 +1361,18 @@ export const FieldAssignValueInput: React.FC<Props> = ({
1336
1361
  value={selectedType}
1337
1362
  onChange={handleSelect}
1338
1363
  dropdownRender={dropdownRender}
1364
+ disabled={isDisabled}
1339
1365
  />
1340
1366
  {['past', 'next'].includes(selectedType) && [
1341
1367
  <InputNumber
1342
1368
  key="number"
1343
1369
  style={{ flex: 1 }}
1344
1370
  value={(parsedValue as any)?.number}
1371
+ disabled={isDisabled}
1345
1372
  onChange={(nextNumber) => {
1373
+ if (isDisabled) {
1374
+ return;
1375
+ }
1346
1376
  inputProps?.onChange?.({
1347
1377
  ...(parsedValue as any),
1348
1378
  type: selectedType,
@@ -1355,7 +1385,11 @@ export const FieldAssignValueInput: React.FC<Props> = ({
1355
1385
  key="unit"
1356
1386
  value={(parsedValue as any)?.unit}
1357
1387
  style={{ minWidth: 130, maxWidth: 140 }}
1388
+ disabled={isDisabled}
1358
1389
  onChange={(nextUnit) => {
1390
+ if (isDisabled) {
1391
+ return;
1392
+ }
1359
1393
  inputProps?.onChange?.({
1360
1394
  ...(parsedValue as any),
1361
1395
  type: selectedType,
@@ -1378,6 +1412,7 @@ export const FieldAssignValueInput: React.FC<Props> = ({
1378
1412
  isRange={isRange}
1379
1413
  value={isRange ? exactRangeValue : exactSingleValue}
1380
1414
  onChange={isRange ? handleExactRangeChange : handleExactSingleChange}
1415
+ disabled={isDisabled}
1381
1416
  style={{ flex: 1 }}
1382
1417
  />
1383
1418
  )}
@@ -1397,7 +1432,12 @@ export const FieldAssignValueInput: React.FC<Props> = ({
1397
1432
 
1398
1433
  const RunJSComponent = React.useMemo(() => {
1399
1434
  const C: React.FC<any> = (inputProps) => (
1400
- <RunJSValueEditor t={flowCtx.t} value={inputProps?.value} onChange={inputProps?.onChange} />
1435
+ <RunJSValueEditor
1436
+ t={flowCtx.t}
1437
+ value={inputProps?.value}
1438
+ onChange={inputProps?.onChange}
1439
+ disabled={inputProps?.disabled}
1440
+ />
1401
1441
  );
1402
1442
  return C;
1403
1443
  }, [flowCtx]);
@@ -1443,6 +1483,10 @@ export const FieldAssignValueInput: React.FC<Props> = ({
1443
1483
 
1444
1484
  const handleVariableInputChange = React.useCallback(
1445
1485
  (nextValue: any) => {
1486
+ if (disabled) {
1487
+ return;
1488
+ }
1489
+
1446
1490
  if (!useDateVariableConstant) {
1447
1491
  onChange(nextValue);
1448
1492
  return;
@@ -1450,7 +1494,7 @@ export const FieldAssignValueInput: React.FC<Props> = ({
1450
1494
 
1451
1495
  onChange(normalizeDateVariableOutput(nextValue, dateVariableComponentProps));
1452
1496
  },
1453
- [dateVariableComponentProps, onChange, useDateVariableConstant],
1497
+ [dateVariableComponentProps, disabled, onChange, useDateVariableConstant],
1454
1498
  );
1455
1499
 
1456
1500
  if (!fieldPath) {
@@ -1465,6 +1509,7 @@ export const FieldAssignValueInput: React.FC<Props> = ({
1465
1509
  metaTree={metaTree}
1466
1510
  style={{ width: '100%' }}
1467
1511
  clearValue={''}
1512
+ disabled={disabled}
1468
1513
  converters={{
1469
1514
  renderInputComponent: (meta) => {
1470
1515
  const firstPath = meta?.paths?.[0];
@@ -274,11 +274,12 @@ const FlowRoute = (props: FlowRouteProps = {}) => {
274
274
  useEffect(() => {
275
275
  let active = true;
276
276
  const requestId = ++requestIdRef.current;
277
+ const requiresAccessibleRoute = shouldRequireAccessibleRoute(routeLayout);
277
278
 
278
279
  const run = async () => {
279
280
  setGuardState({ pageUid, pending: true, allowBridge: false, notFound: false });
280
281
 
281
- if (!skipRouteRepositoryCheck && !routeRepository?.isAccessibleLoaded?.()) {
282
+ if (requiresAccessibleRoute && !skipRouteRepositoryCheck && !routeRepository?.isAccessibleLoaded?.()) {
282
283
  try {
283
284
  await routeRepository?.ensureAccessibleLoaded?.();
284
285
  } catch (_error) {
@@ -293,8 +294,11 @@ const FlowRoute = (props: FlowRouteProps = {}) => {
293
294
  return;
294
295
  }
295
296
 
296
- const route = skipRouteRepositoryCheck ? undefined : getAccessibleRouteByPageUid(routeRepository, pageUid);
297
- if (!route && !skipRouteRepositoryCheck && shouldRequireAccessibleRoute(routeLayout)) {
297
+ const route =
298
+ skipRouteRepositoryCheck || !requiresAccessibleRoute
299
+ ? undefined
300
+ : getAccessibleRouteByPageUid(routeRepository, pageUid);
301
+ if (!route && !skipRouteRepositoryCheck && requiresAccessibleRoute) {
298
302
  setGuardState({ pageUid, pending: false, allowBridge: false, notFound: true });
299
303
  return;
300
304
  }
@@ -15,6 +15,7 @@ export interface RunJSValueEditorProps {
15
15
  t?: (key: string) => string;
16
16
  value?: unknown;
17
17
  onChange?: (value: RunJSValue) => void;
18
+ disabled?: boolean;
18
19
  height?: string;
19
20
  scene?: string;
20
21
  containerStyle?: React.CSSProperties;
@@ -25,6 +26,7 @@ export const RunJSValueEditor: React.FC<RunJSValueEditorProps> = (props) => {
25
26
  t,
26
27
  value,
27
28
  onChange,
29
+ disabled,
28
30
  height = '200px',
29
31
  scene = 'formValue',
30
32
  containerStyle = { flex: 1, minWidth: 0 },
@@ -38,9 +40,15 @@ export const RunJSValueEditor: React.FC<RunJSValueEditorProps> = (props) => {
38
40
  <div style={containerStyle}>
39
41
  <CodeEditor
40
42
  value={current.code}
41
- onChange={(code) => onChange?.({ ...current, code })}
43
+ onChange={(code) => {
44
+ if (disabled) {
45
+ return;
46
+ }
47
+ onChange?.({ ...current, code });
48
+ }}
42
49
  version={current.version}
43
50
  height={height}
51
+ readonly={disabled}
44
52
  enableLinter
45
53
  placeholder={placeholderText}
46
54
  scene={scene}