@nocobase/client-v2 2.2.0-beta.8 → 2.2.0-beta.9
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/BaseApplication.d.ts +2 -0
- package/es/RouteRepository.d.ts +8 -0
- package/es/RouterManager.d.ts +15 -0
- package/es/authRedirect.d.ts +1 -0
- package/es/entry-actions/EntryActionManager.d.ts +24 -0
- package/es/entry-actions/index.d.ts +9 -0
- package/es/flow/admin-shell/BaseLayoutModel.d.ts +6 -0
- package/es/flow/admin-shell/BaseLayoutRouteCoordinator.d.ts +7 -0
- package/es/flow/admin-shell/admin-layout/AppListRender.d.ts +2 -1
- package/es/flow/admin-shell/admin-layout/AppSwitcherActionPanelModel.d.ts +24 -0
- package/es/flow/admin-shell/admin-layout/index.d.ts +1 -0
- package/es/flow/admin-shell/admin-layout/useApplications.d.ts +7 -2
- package/es/flow/models/base/ActionModelCore.d.ts +2 -0
- package/es/flow/routeTransientInputArgs.d.ts +14 -0
- package/es/index.d.ts +2 -0
- package/es/index.mjs +212 -126
- package/es/utils/markdownSanitize.d.ts +11 -0
- package/lib/index.js +198 -112
- package/package.json +7 -7
- package/src/BaseApplication.tsx +2 -0
- package/src/RouteRepository.ts +25 -0
- package/src/RouterManager.tsx +142 -1
- package/src/__tests__/RouteRepository.test.ts +23 -0
- package/src/__tests__/RouterManager.test.ts +125 -0
- package/src/__tests__/authRedirect.test.ts +17 -0
- package/src/authRedirect.ts +38 -0
- package/src/entry-actions/EntryActionManager.ts +76 -0
- package/src/entry-actions/index.ts +10 -0
- package/src/flow/FlowPage.tsx +29 -2
- package/src/flow/__tests__/FlowPage.test.tsx +152 -25
- package/src/flow/__tests__/FlowRoute.test.tsx +547 -2
- package/src/flow/actions/__tests__/dataScopeFilter.test.ts +62 -0
- package/src/flow/actions/__tests__/openView.defineProps.route.test.tsx +80 -20
- package/src/flow/actions/dataScopeFilter.ts +10 -1
- package/src/flow/actions/dateTimeFormat.tsx +2 -2
- package/src/flow/actions/openView.tsx +21 -1
- package/src/flow/admin-shell/BaseLayoutModel.tsx +124 -0
- package/src/flow/admin-shell/BaseLayoutRouteCoordinator.ts +184 -42
- package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +1016 -119
- package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +41 -4
- package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.test.tsx +177 -1
- package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +20 -0
- package/src/flow/admin-shell/admin-layout/AppListRender.tsx +13 -2
- package/src/flow/admin-shell/admin-layout/AppSwitcherActionPanelModel.tsx +289 -0
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutModel.test.tsx +436 -2
- package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +48 -0
- package/src/flow/admin-shell/admin-layout/index.ts +1 -0
- package/src/flow/admin-shell/admin-layout/useApplications.tsx +81 -12
- package/src/flow/common/Markdown/Display.tsx +14 -3
- package/src/flow/common/Markdown/Edit.tsx +19 -7
- package/src/flow/components/FlowRoute.tsx +128 -16
- package/src/flow/internal/components/Markdown/util.ts +2 -1
- package/src/flow/models/base/ActionModel.tsx +10 -8
- package/src/flow/models/base/ActionModelCore.tsx +5 -0
- package/src/flow/models/blocks/table/TableColumnModel.tsx +6 -2
- package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +51 -0
- package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +45 -13
- package/src/flow/models/fields/TextareaFieldModel.tsx +42 -19
- package/src/flow/models/fields/__tests__/TextareaFieldModel.test.tsx +32 -1
- package/src/flow/models/topbar/TopbarActionModel.tsx +78 -3
- package/src/flow/routeTransientInputArgs.ts +27 -0
- package/src/flow/utils/__tests__/dateTimeFormat.test.ts +42 -0
- package/src/index.ts +2 -0
- package/src/nocobase-buildin-plugin/index.tsx +7 -1
- 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:
|
|
108
|
+
value: safeValue,
|
|
107
109
|
lang,
|
|
108
110
|
cache: { enable: false },
|
|
109
111
|
undoDelay: 0,
|
|
110
112
|
mode: props.mode || 'ir',
|
|
111
|
-
preview: {
|
|
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(
|
|
135
|
-
|
|
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
|
-
|
|
233
|
-
|
|
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 {
|
|
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
|
|
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
|
|
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
|
-
}, [
|
|
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
|
-
}, [
|
|
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:
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
...
|
|
380
|
+
...collectionFieldComponentProps,
|
|
380
381
|
...targetCollectionField?.getComponentProps?.(),
|
|
381
382
|
...savedDateTimeDisplayProps,
|
|
382
383
|
}
|
|
383
|
-
:
|
|
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);
|