@nocobase/client-v2 2.2.0-alpha.3 → 2.2.0-alpha.5

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 (81) hide show
  1. package/es/BaseApplication.d.ts +2 -0
  2. package/es/RouteRepository.d.ts +8 -0
  3. package/es/RouterManager.d.ts +15 -0
  4. package/es/authRedirect.d.ts +1 -0
  5. package/es/components/form/TypedVariableInput.d.ts +9 -1
  6. package/es/components/form/filter/CollectionFilter.d.ts +2 -0
  7. package/es/components/form/filter/CollectionFilterPanel.d.ts +2 -0
  8. package/es/components/form/filter/useFilterActionProps.d.ts +2 -0
  9. package/es/entry-actions/EntryActionManager.d.ts +24 -0
  10. package/es/entry-actions/index.d.ts +9 -0
  11. package/es/flow/admin-shell/BaseLayoutModel.d.ts +6 -0
  12. package/es/flow/admin-shell/BaseLayoutRouteCoordinator.d.ts +7 -0
  13. package/es/flow/admin-shell/admin-layout/AppListRender.d.ts +2 -1
  14. package/es/flow/admin-shell/admin-layout/AppSwitcherActionPanelModel.d.ts +24 -0
  15. package/es/flow/admin-shell/admin-layout/index.d.ts +1 -0
  16. package/es/flow/admin-shell/admin-layout/useApplications.d.ts +7 -2
  17. package/es/flow/models/base/ActionModelCore.d.ts +2 -0
  18. package/es/flow/routeTransientInputArgs.d.ts +14 -0
  19. package/es/index.d.ts +6 -0
  20. package/es/index.mjs +229 -143
  21. package/es/utils/getRouteRuntimeVersion.d.ts +23 -0
  22. package/es/utils/index.d.ts +1 -0
  23. package/es/utils/markdownSanitize.d.ts +11 -0
  24. package/lib/index.js +219 -133
  25. package/package.json +7 -7
  26. package/src/BaseApplication.tsx +2 -0
  27. package/src/RouteRepository.ts +25 -0
  28. package/src/RouterManager.tsx +142 -1
  29. package/src/__tests__/RouteRepository.test.ts +23 -0
  30. package/src/__tests__/RouterManager.test.ts +125 -0
  31. package/src/__tests__/authRedirect.test.ts +17 -0
  32. package/src/__tests__/getRouteRuntimeVersion.test.ts +68 -0
  33. package/src/authRedirect.ts +38 -0
  34. package/src/components/form/JsonTextArea.tsx +4 -0
  35. package/src/components/form/TypedVariableInput.tsx +58 -34
  36. package/src/components/form/__tests__/TypedVariableInput.test.tsx +52 -0
  37. package/src/components/form/filter/CollectionFilter.tsx +4 -0
  38. package/src/components/form/filter/CollectionFilterPanel.tsx +4 -0
  39. package/src/components/form/filter/__tests__/useFilterActionProps.test.tsx +95 -0
  40. package/src/components/form/filter/useFilterActionProps.ts +37 -6
  41. package/src/entry-actions/EntryActionManager.ts +76 -0
  42. package/src/entry-actions/index.ts +10 -0
  43. package/src/flow/FlowPage.tsx +29 -2
  44. package/src/flow/__tests__/FlowPage.test.tsx +152 -25
  45. package/src/flow/__tests__/FlowRoute.test.tsx +547 -2
  46. package/src/flow/actions/__tests__/dataScopeFilter.test.ts +62 -0
  47. package/src/flow/actions/__tests__/openView.defineProps.route.test.tsx +80 -20
  48. package/src/flow/actions/dataScopeFilter.ts +10 -1
  49. package/src/flow/actions/dateTimeFormat.tsx +2 -2
  50. package/src/flow/actions/openView.tsx +21 -1
  51. package/src/flow/admin-shell/BaseLayoutModel.tsx +124 -0
  52. package/src/flow/admin-shell/BaseLayoutRouteCoordinator.ts +184 -42
  53. package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +1016 -119
  54. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +41 -4
  55. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.test.tsx +177 -1
  56. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +20 -0
  57. package/src/flow/admin-shell/admin-layout/AppListRender.tsx +13 -2
  58. package/src/flow/admin-shell/admin-layout/AppSwitcherActionPanelModel.tsx +289 -0
  59. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutModel.test.tsx +436 -2
  60. package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +48 -0
  61. package/src/flow/admin-shell/admin-layout/index.ts +1 -0
  62. package/src/flow/admin-shell/admin-layout/useApplications.tsx +81 -12
  63. package/src/flow/common/Markdown/Display.tsx +14 -3
  64. package/src/flow/common/Markdown/Edit.tsx +19 -7
  65. package/src/flow/components/FlowRoute.tsx +128 -16
  66. package/src/flow/internal/components/Markdown/util.ts +2 -1
  67. package/src/flow/models/base/ActionModel.tsx +10 -8
  68. package/src/flow/models/base/ActionModelCore.tsx +5 -0
  69. package/src/flow/models/blocks/table/TableColumnModel.tsx +6 -2
  70. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +51 -0
  71. package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +45 -13
  72. package/src/flow/models/fields/TextareaFieldModel.tsx +42 -19
  73. package/src/flow/models/fields/__tests__/TextareaFieldModel.test.tsx +32 -1
  74. package/src/flow/models/topbar/TopbarActionModel.tsx +78 -3
  75. package/src/flow/routeTransientInputArgs.ts +27 -0
  76. package/src/flow/utils/__tests__/dateTimeFormat.test.ts +42 -0
  77. package/src/index.ts +6 -0
  78. package/src/nocobase-buildin-plugin/index.tsx +7 -1
  79. package/src/utils/getRouteRuntimeVersion.ts +185 -0
  80. package/src/utils/index.tsx +1 -0
  81. package/src/utils/markdownSanitize.ts +88 -0
@@ -12,6 +12,7 @@ import { css } from '@emotion/css';
12
12
  import { createRoot } from 'react-dom/client';
13
13
  import React, { CSSProperties, useCallback, useEffect, useRef, useState } from 'react';
14
14
  import Vditor from 'vditor';
15
+ import { removeMarkdownIframes, stripMarkdownIframes } from '../../../utils/markdownSanitize';
15
16
  import { useCDN } from './useCDN';
16
17
  import useStyle from './style';
17
18
 
@@ -51,8 +52,15 @@ function DisplayInner(props: { value: string; style?: CSSProperties; loadImages?
51
52
  Vditor.preview(containerRef.current, props.value ?? '', {
52
53
  mode: 'light',
53
54
  cdn,
54
- });
55
+ markdown: {
56
+ sanitize: true,
57
+ },
58
+ transform: stripMarkdownIframes,
59
+ })
60
+ .then(() => removeMarkdownIframes(containerRef.current))
61
+ .catch(() => removeMarkdownIframes(containerRef.current));
55
62
  setTimeout(() => {
63
+ removeMarkdownIframes(containerRef.current);
56
64
  containerRef.current?.querySelectorAll('img').forEach((img: HTMLImageElement) => {
57
65
  img.style.cursor = 'zoom-in';
58
66
  img.addEventListener('click', () => {
@@ -129,13 +137,16 @@ export const Display = (props) => {
129
137
  Vditor.md2html(props.value, {
130
138
  mode: 'light',
131
139
  cdn,
140
+ markdown: {
141
+ sanitize: true,
142
+ },
132
143
  })
133
144
  .then((html) => {
134
- setText(convertToText(html));
145
+ setText(convertToText(stripMarkdownIframes(html)));
135
146
  })
136
147
  .catch(() => setText(''));
137
148
  }
138
- }, [props.value, textOnly]);
149
+ }, [props.value, textOnly, cdn]);
139
150
 
140
151
  const isOverflowTooltip = useCallback(() => {
141
152
  if (!elRef.current) return false;
@@ -17,6 +17,7 @@ import React, { useEffect, useCallback, useLayoutEffect, useMemo, useRef, useSta
17
17
  import { useTranslation } from 'react-i18next';
18
18
  import Vditor from 'vditor';
19
19
  import 'vditor/dist/index.css';
20
+ import { stripMarkdownIframeTags, stripMarkdownIframes } from '../../../utils/markdownSanitize';
20
21
  import { useCDN } from './useCDN';
21
22
  import useStyle from './style';
22
23
 
@@ -102,13 +103,20 @@ const Edit = (props) => {
102
103
  if (!containerRef.current) return;
103
104
 
104
105
  const toolbarConfig = placeToolbarTooltipsBelow(toolbar ?? defaultToolbar);
106
+ const safeValue = stripMarkdownIframeTags(value ?? '');
105
107
  const vditor = new Vditor(containerRef.current, {
106
- value: value ?? '',
108
+ value: safeValue,
107
109
  lang,
108
110
  cache: { enable: false },
109
111
  undoDelay: 0,
110
112
  mode: props.mode || 'ir',
111
- preview: { math: { engine: 'KaTeX' } },
113
+ preview: {
114
+ markdown: {
115
+ sanitize: true,
116
+ },
117
+ math: { engine: 'KaTeX' },
118
+ transform: stripMarkdownIframes,
119
+ },
112
120
  toolbar: toolbarConfig,
113
121
  fullscreen: { index: 1200 },
114
122
  cdn,
@@ -117,7 +125,6 @@ const Edit = (props) => {
117
125
  after: () => {
118
126
  vdRef.current = vditor;
119
127
  setEditorReady(true); // Notify that the editor is ready
120
- vditor.setValue(value ?? '');
121
128
  if (disabled) {
122
129
  vditor.disabled();
123
130
  } else {
@@ -131,8 +138,12 @@ const Edit = (props) => {
131
138
  });
132
139
  }
133
140
  },
134
- input(value) {
135
- onChange(value);
141
+ input(nextValue) {
142
+ const safeNextValue = stripMarkdownIframeTags(nextValue);
143
+ if (safeNextValue !== nextValue) {
144
+ vditor.setValue(safeNextValue);
145
+ }
146
+ onChange(safeNextValue);
136
147
  },
137
148
  upload: {
138
149
  multiple: false,
@@ -229,8 +240,9 @@ const Edit = (props) => {
229
240
  useEffect(() => {
230
241
  if (editorReady && vdRef.current) {
231
242
  const editor = vdRef.current;
232
- if (value !== editor.getValue()) {
233
- editor.setValue(value ?? '');
243
+ const safeValue = stripMarkdownIframeTags(value ?? '');
244
+ if (safeValue !== editor.getValue()) {
245
+ editor.setValue(safeValue);
234
246
 
235
247
  const preArea = containerRef.current?.querySelector(
236
248
  'div.vditor-content > div.vditor-ir > pre',
@@ -10,10 +10,13 @@
10
10
  import { type FlowEngine, useFlowContext, useFlowEngine } from '@nocobase/flow-engine';
11
11
  import React, { useEffect, useMemo, useRef, useState } from 'react';
12
12
  import { deviceType } from 'react-device-detect';
13
- import { useParams } from 'react-router-dom';
13
+ import { useLocation, useNavigate, useParams } from 'react-router-dom';
14
14
  import { useApp } from '../../hooks/useApp';
15
- import { NocoBaseDesktopRouteType } from '../../flow-compat';
16
- import { resolveAdminRouteRuntimeTarget } from '../admin-shell/admin-layout/resolveAdminRouteRuntimeTarget';
15
+ import { NocoBaseDesktopRouteType, type NocoBaseDesktopRoute } from '../../flow-compat';
16
+ import {
17
+ resolveAdminRouteRuntimeTarget,
18
+ toRouterNavigationPath,
19
+ } from '../admin-shell/admin-layout/resolveAdminRouteRuntimeTarget';
17
20
  import { getAdminLayoutModel, type AdminLayoutModel } from '../admin-shell/admin-layout/AdminLayoutModel';
18
21
  import { getLayoutModel, type BaseLayoutModel } from '../admin-shell/BaseLayoutModel';
19
22
  import { useLayoutRoutePage } from '../admin-shell/useLayoutRoutePage';
@@ -21,6 +24,7 @@ import { AppNotFound } from '../../components';
21
24
  import { useKeepAlive } from '../../components/KeepAlive';
22
25
 
23
26
  type FlowRouteGuardState = {
27
+ pageUid?: string;
24
28
  pending: boolean;
25
29
  allowBridge: boolean;
26
30
  notFound: boolean;
@@ -35,6 +39,14 @@ type FlowRouteLayoutContext = {
35
39
  uid?: string;
36
40
  };
37
41
 
42
+ type FlowRouteRepositoryLike = {
43
+ refreshAccessible?: () => Promise<unknown>;
44
+ isAccessibleLoaded?: () => boolean;
45
+ ensureAccessibleLoaded?: () => Promise<unknown>;
46
+ getRouteBySchemaUid?: (schemaUid: string) => NocoBaseDesktopRoute | undefined;
47
+ listAccessible?: () => NocoBaseDesktopRoute[];
48
+ };
49
+
38
50
  export type FlowRouteProps = {
39
51
  pageUid?: string;
40
52
  active?: boolean;
@@ -70,6 +82,44 @@ const getDefaultLegacyPageBehavior = (
70
82
 
71
83
  const shouldRequireAccessibleRoute = (contextLayout?: FlowRouteLayoutContext) => contextLayout?.authCheck !== false;
72
84
 
85
+ const findAccessibleRouteByIdentity = (
86
+ routes: NocoBaseDesktopRoute[] | undefined,
87
+ pageUid: string,
88
+ ): NocoBaseDesktopRoute | undefined => {
89
+ if (!Array.isArray(routes)) {
90
+ return undefined;
91
+ }
92
+
93
+ for (const route of routes) {
94
+ if (
95
+ (route.type !== NocoBaseDesktopRouteType.group && route.schemaUid === pageUid) ||
96
+ (route.type === NocoBaseDesktopRouteType.group && route.id != null && String(route.id) === pageUid)
97
+ ) {
98
+ return route;
99
+ }
100
+
101
+ const matched = findAccessibleRouteByIdentity(route.children, pageUid);
102
+ if (matched) {
103
+ return matched;
104
+ }
105
+ }
106
+
107
+ return undefined;
108
+ };
109
+
110
+ const getAccessibleRouteByPageUid = (
111
+ routeRepository: FlowRouteRepositoryLike | undefined,
112
+ pageUid: string,
113
+ ): NocoBaseDesktopRoute | undefined => {
114
+ const routeBySchemaUid = routeRepository?.getRouteBySchemaUid?.(pageUid);
115
+
116
+ if (routeBySchemaUid && routeBySchemaUid.type !== NocoBaseDesktopRouteType.group) {
117
+ return routeBySchemaUid;
118
+ }
119
+
120
+ return findAccessibleRouteByIdentity(routeRepository?.listAccessible?.(), pageUid);
121
+ };
122
+
73
123
  const hasFlowModel = async (flowEngine: FlowEngine, pageUid: string) => {
74
124
  if (flowEngine.getModel(pageUid)) {
75
125
  return true;
@@ -197,11 +247,15 @@ const FlowRoute = (props: FlowRouteProps = {}) => {
197
247
  );
198
248
  const legacyPageBehavior = legacyPageBehaviorProp || getDefaultLegacyPageBehavior(flowEngine, routeLayout);
199
249
  const app = useApp();
200
- const routeRepository = flowEngine.context.routeRepository;
250
+ const location = useLocation();
251
+ const navigate = useNavigate();
252
+ const routeRepository = flowEngine.context.routeRepository as FlowRouteRepositoryLike | undefined;
201
253
  const params = useParams();
202
254
  const pageUid = pageUidProp || params?.name;
255
+ const hasNestedRoutePath = typeof params?.tabUid !== 'undefined' || typeof params?.['*'] !== 'undefined';
203
256
  const skipRouteRepositoryCheck = !routeRepository;
204
257
  const [guardState, setGuardState] = useState<FlowRouteGuardState>({
258
+ pageUid: undefined,
205
259
  pending: true,
206
260
  allowBridge: false,
207
261
  notFound: false,
@@ -213,19 +267,23 @@ const FlowRoute = (props: FlowRouteProps = {}) => {
213
267
  throw new Error('[NocoBase] FlowRoute requires pageUid or route.params.name.');
214
268
  }
215
269
 
270
+ useEffect(() => {
271
+ replaceTriggeredRef.current = false;
272
+ }, [location.hash, location.pathname, location.search, pageUid]);
273
+
216
274
  useEffect(() => {
217
275
  let active = true;
218
276
  const requestId = ++requestIdRef.current;
219
277
 
220
278
  const run = async () => {
221
- setGuardState({ pending: true, allowBridge: false, notFound: false });
279
+ setGuardState({ pageUid, pending: true, allowBridge: false, notFound: false });
222
280
 
223
281
  if (!skipRouteRepositoryCheck && !routeRepository?.isAccessibleLoaded?.()) {
224
282
  try {
225
283
  await routeRepository?.ensureAccessibleLoaded?.();
226
284
  } catch (_error) {
227
285
  if (active && requestId === requestIdRef.current) {
228
- setGuardState({ pending: false, allowBridge: true, notFound: false });
286
+ setGuardState({ pageUid, pending: false, allowBridge: true, notFound: false });
229
287
  }
230
288
  return;
231
289
  }
@@ -235,28 +293,64 @@ const FlowRoute = (props: FlowRouteProps = {}) => {
235
293
  return;
236
294
  }
237
295
 
238
- const route = skipRouteRepositoryCheck ? undefined : routeRepository?.getRouteBySchemaUid?.(pageUid);
296
+ const route = skipRouteRepositoryCheck ? undefined : getAccessibleRouteByPageUid(routeRepository, pageUid);
239
297
  if (!route && !skipRouteRepositoryCheck && shouldRequireAccessibleRoute(routeLayout)) {
240
- setGuardState({ pending: false, allowBridge: false, notFound: true });
298
+ setGuardState({ pageUid, pending: false, allowBridge: false, notFound: true });
241
299
  return;
242
300
  }
243
301
 
244
302
  if (!route && legacyPageBehavior === 'notFound') {
245
303
  const flowModelExists = await hasFlowModel(flowEngine, pageUid);
246
304
  if (active && requestId === requestIdRef.current) {
247
- setGuardState({ pending: false, allowBridge: flowModelExists, notFound: !flowModelExists });
305
+ setGuardState({ pageUid, pending: false, allowBridge: flowModelExists, notFound: !flowModelExists });
306
+ }
307
+ return;
308
+ }
309
+
310
+ if (route?.type === NocoBaseDesktopRouteType.group) {
311
+ if (hasNestedRoutePath) {
312
+ setGuardState({ pageUid, pending: false, allowBridge: false, notFound: true });
313
+ return;
314
+ }
315
+
316
+ const target = resolveAdminRouteRuntimeTarget({
317
+ app,
318
+ route,
319
+ layout: routeLayout,
320
+ });
321
+
322
+ if (target.reason === 'emptyGroup') {
323
+ setGuardState({ pageUid, pending: false, allowBridge: false, notFound: false });
324
+ return;
325
+ }
326
+
327
+ if (target.runtimePath) {
328
+ if (replaceTriggeredRef.current) {
329
+ setGuardState({ pageUid, pending: false, allowBridge: false, notFound: false });
330
+ return;
331
+ }
332
+
333
+ replaceTriggeredRef.current = true;
334
+ if (target.navigationMode === 'document') {
335
+ window.location.replace(target.runtimePath);
336
+ return;
337
+ }
338
+ navigate(toRouterNavigationPath(target.runtimePath, app.router?.getBasename?.()), { replace: true });
339
+ return;
248
340
  }
341
+
342
+ setGuardState({ pageUid, pending: false, allowBridge: false, notFound: true });
249
343
  return;
250
344
  }
251
345
 
252
346
  if (route?.type === NocoBaseDesktopRouteType.page) {
253
347
  if (legacyPageBehavior === 'notFound') {
254
- setGuardState({ pending: false, allowBridge: false, notFound: true });
348
+ setGuardState({ pageUid, pending: false, allowBridge: false, notFound: true });
255
349
  return;
256
350
  }
257
351
 
258
352
  if (legacyPageBehavior === 'bridge') {
259
- setGuardState({ pending: false, allowBridge: true, notFound: false });
353
+ setGuardState({ pageUid, pending: false, allowBridge: true, notFound: false });
260
354
  return;
261
355
  }
262
356
 
@@ -279,14 +373,14 @@ const FlowRoute = (props: FlowRouteProps = {}) => {
279
373
 
280
374
  if (target.reason === 'unsupportedV2Runtime') {
281
375
  if (active && requestId === requestIdRef.current) {
282
- setGuardState({ pending: false, allowBridge: false, notFound: true });
376
+ setGuardState({ pageUid, pending: false, allowBridge: false, notFound: true });
283
377
  }
284
378
  return;
285
379
  }
286
380
  }
287
381
 
288
382
  if (active && requestId === requestIdRef.current) {
289
- setGuardState({ pending: false, allowBridge: true, notFound: false });
383
+ setGuardState({ pageUid, pending: false, allowBridge: true, notFound: false });
290
384
  }
291
385
  };
292
386
 
@@ -295,10 +389,20 @@ const FlowRoute = (props: FlowRouteProps = {}) => {
295
389
  return () => {
296
390
  active = false;
297
391
  };
298
- }, [app, flowEngine, legacyPageBehavior, pageUid, routeLayout, routeRepository, skipRouteRepositoryCheck]);
392
+ }, [
393
+ app,
394
+ flowEngine,
395
+ hasNestedRoutePath,
396
+ legacyPageBehavior,
397
+ navigate,
398
+ pageUid,
399
+ routeLayout,
400
+ routeRepository,
401
+ skipRouteRepositoryCheck,
402
+ ]);
299
403
 
300
404
  const content = useMemo(() => {
301
- if (guardState.pending) {
405
+ if (guardState.pageUid !== pageUid || guardState.pending) {
302
406
  return null;
303
407
  }
304
408
 
@@ -311,7 +415,15 @@ const FlowRoute = (props: FlowRouteProps = {}) => {
311
415
  }
312
416
 
313
417
  return <BridgeFlowRoute pageUid={pageUid} active={active} getLayoutModel={getLayoutModel} />;
314
- }, [active, getLayoutModel, guardState.allowBridge, guardState.notFound, guardState.pending, pageUid]);
418
+ }, [
419
+ active,
420
+ getLayoutModel,
421
+ guardState.allowBridge,
422
+ guardState.notFound,
423
+ guardState.pageUid,
424
+ guardState.pending,
425
+ pageUid,
426
+ ]);
315
427
 
316
428
  return content;
317
429
  };
@@ -9,13 +9,14 @@
9
9
 
10
10
  import _ from 'lodash';
11
11
  import { useEffect, useState } from 'react';
12
+ import { stripMarkdownIframes } from '../../../../utils/markdownSanitize';
12
13
 
13
14
  export const parseMarkdown = _.memoize(async (text: string) => {
14
15
  if (!text) {
15
16
  return text;
16
17
  }
17
18
  const m = await import('./md');
18
- return m.default.render(text);
19
+ return stripMarkdownIframes(m.default.render(text));
19
20
  });
20
21
 
21
22
  export function useParseMarkdown(text: string) {
@@ -20,7 +20,7 @@ import { ActionModel } from './ActionModelCore';
20
20
 
21
21
  function getMobileDefaultProps(ctx) {
22
22
  const defaultProps = ctx.model.defaultProps || {};
23
- if (ctx.isMobileLayout && defaultProps.icon) {
23
+ if (ctx.isMobileLayout && defaultProps.icon && ctx.model.enableEditIconOnly !== false) {
24
24
  return {
25
25
  ...defaultProps,
26
26
  iconOnly: true,
@@ -43,6 +43,7 @@ ActionModel.registerFlow({
43
43
  'x-decorator': 'FormItem',
44
44
  'x-component': 'Input',
45
45
  title: tExpr('Button title'),
46
+ description: ctx.model.getTitleFieldDescription?.(),
46
47
  }
47
48
  : undefined,
48
49
  tooltip: ctx.model.enableEditTooltip
@@ -59,13 +60,14 @@ ActionModel.registerFlow({
59
60
  title: tExpr('Button icon'),
60
61
  }
61
62
  : undefined,
62
- iconOnly: ctx.model.enableEditIcon
63
- ? {
64
- 'x-decorator': 'FormItem',
65
- 'x-component': 'Switch',
66
- title: tExpr('Icon only'),
67
- }
68
- : undefined,
63
+ iconOnly:
64
+ ctx.model.enableEditIcon && ctx.model.enableEditIconOnly !== false
65
+ ? {
66
+ 'x-decorator': 'FormItem',
67
+ 'x-component': 'Switch',
68
+ title: tExpr('Icon only'),
69
+ }
70
+ : undefined,
69
71
  type: ctx.model.enableEditType
70
72
  ? {
71
73
  'x-decorator': 'FormItem',
@@ -62,6 +62,7 @@ export class ActionModel<T extends DefaultStructure = DefaultStructure> extends
62
62
  enableEditTooltip = true;
63
63
  enableEditTitle = true;
64
64
  enableEditIcon = true;
65
+ enableEditIconOnly = true;
65
66
  enableEditType = true;
66
67
  enableEditDanger = true;
67
68
  enableEditColor = false;
@@ -136,6 +137,10 @@ export class ActionModel<T extends DefaultStructure = DefaultStructure> extends
136
137
  return this.props.title;
137
138
  }
138
139
 
140
+ getTitleFieldDescription() {
141
+ return undefined;
142
+ }
143
+
139
144
  getIcon() {
140
145
  return this.props.icon;
141
146
  }
@@ -373,14 +373,18 @@ TableColumnModel.registerFlow({
373
373
  currentProps: ctx.model.props,
374
374
  })
375
375
  : undefined;
376
+ const collectionFieldComponentProps = collectionField.getComponentProps();
376
377
  const componentProps =
377
378
  collectionField.isAssociationField() && titleField
378
379
  ? {
379
- ...collectionField.getComponentProps(),
380
+ ...collectionFieldComponentProps,
380
381
  ...targetCollectionField?.getComponentProps?.(),
381
382
  ...savedDateTimeDisplayProps,
382
383
  }
383
- : collectionField.getComponentProps();
384
+ : {
385
+ ...collectionFieldComponentProps,
386
+ ...savedDateTimeDisplayProps,
387
+ };
384
388
  ctx.model.setProps('title', collectionField.title);
385
389
  ctx.model.setProps('dataIndex', collectionField.name);
386
390
  // for quick edit
@@ -327,6 +327,57 @@ describe('TableColumnModel sorter settings', () => {
327
327
  );
328
328
  });
329
329
 
330
+ it('keeps saved ordinary datetime format when table column initializes again', async () => {
331
+ const engine = new FlowEngine();
332
+ const model = new TableColumnModel({
333
+ uid: 'table-column-saved-datetime-format',
334
+ flowEngine: engine,
335
+ } as any);
336
+ const initStep = model.getFlow('tableColumnSettings')?.steps?.init as any;
337
+ const setProps = vi.fn();
338
+
339
+ await initStep.handler({
340
+ model: {
341
+ context: {
342
+ collectionField: {
343
+ title: 'Datetime',
344
+ name: 'datetime',
345
+ isAssociationField: () => false,
346
+ getComponentProps: () => ({
347
+ dateFormat: 'YYYY-MM-DD',
348
+ showTime: false,
349
+ }),
350
+ },
351
+ },
352
+ props: {},
353
+ subModels: {
354
+ field: {
355
+ getStepParams: (flowKey, stepKey) =>
356
+ flowKey === 'datetimeSettings' && stepKey === 'dateFormat'
357
+ ? {
358
+ picker: 'date',
359
+ dateFormat: 'YYYY-MM-DD',
360
+ showTime: true,
361
+ timeFormat: 'HH:mm:ss',
362
+ }
363
+ : undefined,
364
+ },
365
+ },
366
+ applySubModelsBeforeRenderFlows: vi.fn(),
367
+ setProps,
368
+ },
369
+ });
370
+
371
+ expect(setProps).toHaveBeenCalledWith(
372
+ expect.objectContaining({
373
+ dateFormat: 'YYYY-MM-DD',
374
+ format: 'YYYY-MM-DD HH:mm:ss',
375
+ showTime: true,
376
+ timeFormat: 'HH:mm:ss',
377
+ }),
378
+ );
379
+ });
380
+
330
381
  it('does not update field component setting when title field refresh fails', async () => {
331
382
  const engine = new FlowEngine();
332
383
  const model = new TableColumnModel({ uid: 'table-column-title-field-component-failed', flowEngine: engine } as any);
@@ -198,6 +198,48 @@ const useFieldPermissionMessage = (model, allowEdit) => {
198
198
  return messageValue;
199
199
  };
200
200
 
201
+ const recordSelectClassName = css`
202
+ min-width: 0;
203
+
204
+ .ant-select-selector {
205
+ min-width: 0;
206
+ overflow: hidden;
207
+ }
208
+
209
+ .ant-select-selection-search,
210
+ .ant-select-selection-item,
211
+ .ant-select-selection-placeholder,
212
+ .ant-select-selection-overflow,
213
+ .ant-select-selection-overflow-item {
214
+ min-width: 0;
215
+ max-width: 100%;
216
+ }
217
+
218
+ .ant-select-selection-item,
219
+ .ant-select-selection-item-content {
220
+ overflow: hidden;
221
+ text-overflow: ellipsis;
222
+ white-space: nowrap;
223
+ }
224
+ `;
225
+
226
+ const recordSelectLabelClassName = css`
227
+ display: block;
228
+ min-width: 0;
229
+ max-width: 100%;
230
+ overflow: hidden;
231
+ text-overflow: ellipsis;
232
+ white-space: nowrap;
233
+
234
+ * {
235
+ min-width: 0;
236
+ max-width: 100%;
237
+ overflow: hidden;
238
+ text-overflow: ellipsis;
239
+ white-space: nowrap !important;
240
+ }
241
+ `;
242
+
201
243
  const LazySelect = (props: Readonly<LazySelectProps>) => {
202
244
  const {
203
245
  fieldNames,
@@ -210,6 +252,7 @@ const LazySelect = (props: Readonly<LazySelectProps>) => {
210
252
  onChange,
211
253
  allowCreate = true,
212
254
  allowEdit = true,
255
+ className,
213
256
  ...others
214
257
  } = props;
215
258
  const model: any = useFlowModel();
@@ -317,6 +360,7 @@ const LazySelect = (props: Readonly<LazySelectProps>) => {
317
360
  <Select
318
361
  style={{ width: '100%' }}
319
362
  {...others}
363
+ className={[recordSelectClassName, className].filter(Boolean).join(' ')}
320
364
  allowClear
321
365
  showSearch
322
366
  maxTagCount="responsive"
@@ -409,19 +453,7 @@ const LazySelect = (props: Readonly<LazySelectProps>) => {
409
453
  }}
410
454
  popupMatchSelectWidth
411
455
  labelRender={(data) => {
412
- return (
413
- <div
414
- className={css`
415
- div {
416
- white-space: nowrap !important;
417
- overflow: hidden;
418
- text-overflow: ellipsis;
419
- }
420
- `}
421
- >
422
- {data.label}
423
- </div>
424
- );
456
+ return <div className={recordSelectLabelClassName}>{data.label}</div>;
425
457
  }}
426
458
  dropdownRender={(menu) => {
427
459
  const isFullMatch = realOptions.some((v) => v[normalizedFieldNames.label] === others.searchText);