@nocobase/client-v2 2.2.0-beta.7 → 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/actions/afterSuccess.d.ts +8 -1
- package/es/flow/actions/index.d.ts +1 -1
- 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/AdminLayoutSlotModels.d.ts +1 -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/models/blocks/form/submitHandler.d.ts +1 -1
- package/es/flow/models/blocks/form/value-runtime/runtime.d.ts +9 -0
- package/es/flow/models/blocks/js-block/JSBlock.d.ts +1 -0
- package/es/flow/models/blocks/table/TableActionsColumnModel.d.ts +1 -0
- package/es/flow/models/blocks/table/TableBlockModel.d.ts +1 -0
- package/es/flow/models/blocks/table/dragSort/dragSortHooks.d.ts +1 -1
- package/es/flow/models/blocks/table/dragSort/dragSortSettings.d.ts +2 -1
- package/es/flow/models/blocks/table/dragSort/dragSortUtils.d.ts +6 -4
- package/es/flow/resolveViewParamsToViewList.d.ts +1 -1
- package/es/flow/routeTransientInputArgs.d.ts +14 -0
- package/es/index.d.ts +2 -0
- package/es/index.mjs +243 -141
- package/es/utils/markdownSanitize.d.ts +11 -0
- package/lib/index.js +232 -130
- 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/collection-manager/field-configure.ts +1 -1
- package/src/collection-manager/interfaces/id.ts +1 -1
- package/src/collection-manager/interfaces/m2m.tsx +2 -2
- package/src/collection-manager/interfaces/m2o.tsx +2 -2
- package/src/collection-manager/interfaces/nanoid.ts +1 -1
- package/src/collection-manager/interfaces/o2m.tsx +2 -2
- package/src/collection-manager/interfaces/obo.tsx +2 -2
- package/src/collection-manager/interfaces/oho.tsx +2 -2
- package/src/collection-manager/interfaces/properties/index.ts +2 -2
- package/src/collection-manager/interfaces/uuid.ts +1 -1
- 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/__tests__/getKey.test.ts +7 -0
- package/src/flow/__tests__/resolveViewParamsToViewList.test.ts +34 -0
- package/src/flow/actions/__tests__/afterSuccess.test.ts +73 -0
- package/src/flow/actions/__tests__/dataScopeFilter.test.ts +62 -0
- package/src/flow/actions/__tests__/openView.defineProps.route.test.tsx +164 -0
- package/src/flow/actions/afterSuccess.tsx +142 -3
- package/src/flow/actions/dataScopeFilter.ts +10 -1
- package/src/flow/actions/dateTimeFormat.tsx +2 -2
- package/src/flow/actions/index.ts +1 -1
- package/src/flow/actions/openView.tsx +59 -5
- package/src/flow/admin-shell/BaseLayoutModel.tsx +149 -3
- package/src/flow/admin-shell/BaseLayoutRouteCoordinator.ts +187 -42
- package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +1020 -110
- package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +42 -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/AdminLayoutSlotModels.tsx +5 -4
- 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 +518 -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/getViewDiffAndUpdateHidden.tsx +1 -0
- 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 +11 -4
- package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +37 -0
- package/src/flow/models/blocks/filter-form/fields/FilterFormCustomFieldModel.tsx +1 -1
- package/src/flow/models/blocks/form/FormActionModel.tsx +1 -1
- package/src/flow/models/blocks/form/__tests__/submitHandler.test.ts +54 -1
- package/src/flow/models/blocks/form/submitHandler.ts +17 -3
- package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +87 -0
- package/src/flow/models/blocks/form/value-runtime/runtime.ts +91 -0
- package/src/flow/models/blocks/js-block/JSBlock.tsx +223 -2
- package/src/flow/models/blocks/js-block/__tests__/JSBlockModel.test.tsx +150 -0
- package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +35 -12
- package/src/flow/models/blocks/table/TableBlockModel.tsx +25 -14
- package/src/flow/models/blocks/table/TableColumnModel.tsx +6 -2
- package/src/flow/models/blocks/table/__tests__/TableActionsColumnModel.test.tsx +57 -1
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.dragSort.test.ts +127 -0
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.rowSelection.test.tsx +1 -0
- package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +51 -0
- package/src/flow/models/blocks/table/dragSort/dragSortHooks.tsx +28 -17
- package/src/flow/models/blocks/table/dragSort/dragSortSettings.ts +13 -6
- package/src/flow/models/blocks/table/dragSort/dragSortUtils.ts +15 -2
- 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/resolveViewParamsToViewList.tsx +11 -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/settings-center/SystemSettingsPage.tsx +1 -1
- package/src/utils/markdownSanitize.ts +88 -0
|
@@ -8,41 +8,110 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { useApp } from '../../../flow-compat';
|
|
11
|
+
import type { AppListProps } from '@ant-design/pro-layout/es/components/AppsLogoComponents/types';
|
|
11
12
|
import React from 'react';
|
|
13
|
+
import type { AdminLayoutModel } from './AdminLayoutModel';
|
|
14
|
+
import { ADMIN_LAYOUT_MODEL_UID } from './constants';
|
|
15
|
+
import type { AppSwitcherActionPanelModel } from './AppSwitcherActionPanelModel';
|
|
12
16
|
|
|
13
|
-
export const
|
|
17
|
+
export const APP_SWITCHER_ACTION_PANEL_MODEL_UID = `${ADMIN_LAYOUT_MODEL_UID}-app-switcher-actions`;
|
|
18
|
+
|
|
19
|
+
function getErrorStatus(error: unknown) {
|
|
20
|
+
if (!error || typeof error !== 'object') {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const errorLike = error as { response?: { status?: unknown }; status?: unknown };
|
|
24
|
+
const status = errorLike.response?.status ?? errorLike.status;
|
|
25
|
+
return typeof status === 'number' ? status : undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const useApplications = (adminLayoutModel?: AdminLayoutModel) => {
|
|
14
29
|
const app = useApp();
|
|
15
30
|
const loadAppList = app.apps.loadAppList;
|
|
16
|
-
const [
|
|
31
|
+
const [legacyAppList, setLegacyAppList] = React.useState<AppListProps>([]);
|
|
32
|
+
const [appSwitcherModel, setAppSwitcherModel] = React.useState<AppSwitcherActionPanelModel>();
|
|
33
|
+
|
|
34
|
+
React.useEffect(() => {
|
|
35
|
+
let canceled = false;
|
|
36
|
+
let modelWithLayoutContext: AppSwitcherActionPanelModel | undefined;
|
|
37
|
+
|
|
38
|
+
if (!adminLayoutModel) {
|
|
39
|
+
setAppSwitcherModel(undefined);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const load = async () => {
|
|
44
|
+
try {
|
|
45
|
+
const model = await adminLayoutModel.flowEngine.loadOrCreateModel<AppSwitcherActionPanelModel>({
|
|
46
|
+
uid: APP_SWITCHER_ACTION_PANEL_MODEL_UID,
|
|
47
|
+
use: 'AppSwitcherActionPanelModel',
|
|
48
|
+
parentId: adminLayoutModel.uid,
|
|
49
|
+
subKey: 'appSwitcher',
|
|
50
|
+
subType: 'object',
|
|
51
|
+
});
|
|
52
|
+
if (canceled || !model) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
model.context.addDelegate(adminLayoutModel.context);
|
|
56
|
+
modelWithLayoutContext = model;
|
|
57
|
+
setAppSwitcherModel(model);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if (canceled) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (getErrorStatus(error) !== 404) {
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
setAppSwitcherModel(undefined);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
load();
|
|
69
|
+
|
|
70
|
+
return () => {
|
|
71
|
+
canceled = true;
|
|
72
|
+
modelWithLayoutContext?.context.removeDelegate(adminLayoutModel.context);
|
|
73
|
+
};
|
|
74
|
+
}, [adminLayoutModel]);
|
|
17
75
|
|
|
18
76
|
React.useEffect(() => {
|
|
19
77
|
let canceled = false;
|
|
20
78
|
|
|
79
|
+
if (adminLayoutModel) {
|
|
80
|
+
setLegacyAppList([]);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
21
84
|
if (!loadAppList) {
|
|
22
|
-
|
|
85
|
+
setLegacyAppList([]);
|
|
23
86
|
return;
|
|
24
87
|
}
|
|
25
88
|
|
|
26
|
-
|
|
27
|
-
|
|
89
|
+
const load = async () => {
|
|
90
|
+
try {
|
|
91
|
+
const list = await Promise.resolve(loadAppList(app));
|
|
28
92
|
if (!canceled) {
|
|
29
|
-
|
|
93
|
+
setLegacyAppList(Array.isArray(list) ? list : []);
|
|
30
94
|
}
|
|
31
|
-
})
|
|
32
|
-
.catch((error) => {
|
|
95
|
+
} catch (error) {
|
|
33
96
|
console.error('[NocoBase] Failed to load application switcher list.', error);
|
|
34
97
|
if (!canceled) {
|
|
35
|
-
|
|
98
|
+
setLegacyAppList([]);
|
|
36
99
|
}
|
|
37
|
-
}
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
load();
|
|
38
103
|
|
|
39
104
|
return () => {
|
|
40
105
|
canceled = true;
|
|
41
106
|
};
|
|
42
|
-
}, [app, loadAppList]);
|
|
107
|
+
}, [adminLayoutModel, app, loadAppList]);
|
|
108
|
+
|
|
109
|
+
const shouldRenderConfiguredSwitcher =
|
|
110
|
+
!!appSwitcherModel && (appSwitcherModel.hasActions() || app.flowEngine.context.flowSettingsEnabled);
|
|
43
111
|
|
|
44
112
|
return {
|
|
45
113
|
Component: app.apps.Component,
|
|
46
|
-
appList,
|
|
114
|
+
appList: shouldRenderConfiguredSwitcher ? [{ title: '', url: '#' }] : legacyAppList,
|
|
115
|
+
appSwitcherModel,
|
|
47
116
|
};
|
|
48
117
|
};
|
|
@@ -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',
|
|
@@ -25,7 +25,7 @@ export function ActionWithoutPermission(props) {
|
|
|
25
25
|
const dataSourcePrefix = `${t(dataSource.displayName || dataSource.key)} > `;
|
|
26
26
|
const collectionPrefix = collection ? `${t(collection.title) || collection.name || collection.tableName} ` : '';
|
|
27
27
|
return `${dataSourcePrefix}${collectionPrefix}`;
|
|
28
|
-
}, []);
|
|
28
|
+
}, [collection, dataSource.displayName, dataSource.key, t]);
|
|
29
29
|
const { actionName } = props?.forbidden || model.forbidden;
|
|
30
30
|
const messageValue = useMemo(() => {
|
|
31
31
|
return t(
|
|
@@ -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
|
}
|
|
@@ -143,10 +148,11 @@ export class ActionModel<T extends DefaultStructure = DefaultStructure> extends
|
|
|
143
148
|
renderButton() {
|
|
144
149
|
const { iconOnly, ...props } = this.props;
|
|
145
150
|
const icon = this.getIcon() ? <Icon type={this.getIcon() as any} /> : undefined;
|
|
151
|
+
const titleContent = iconOnly && icon ? null : props.children || this.getTitle();
|
|
146
152
|
|
|
147
153
|
return (
|
|
148
154
|
<Button {...props} onClick={this.onClick.bind(this)} icon={icon}>
|
|
149
|
-
{
|
|
155
|
+
{titleContent}
|
|
150
156
|
</Button>
|
|
151
157
|
);
|
|
152
158
|
}
|
|
@@ -162,11 +168,12 @@ export class ActionModel<T extends DefaultStructure = DefaultStructure> extends
|
|
|
162
168
|
renderHiddenInConfig(): React.ReactNode | undefined {
|
|
163
169
|
const { iconOnly, ...props } = this.props;
|
|
164
170
|
const icon = this.getIcon() ? <Icon type={this.getIcon() as any} /> : undefined;
|
|
171
|
+
const titleContent = iconOnly && icon ? null : props.children || this.getTitle();
|
|
165
172
|
if (this.forbidden) {
|
|
166
173
|
return (
|
|
167
174
|
<ActionWithoutPermission>
|
|
168
175
|
<Button {...props} onClick={this.onClick.bind(this)} icon={icon} style={{ opacity: '0.3' }}>
|
|
169
|
-
{
|
|
176
|
+
{titleContent}
|
|
170
177
|
</Button>
|
|
171
178
|
</ActionWithoutPermission>
|
|
172
179
|
);
|
|
@@ -174,7 +181,7 @@ export class ActionModel<T extends DefaultStructure = DefaultStructure> extends
|
|
|
174
181
|
return (
|
|
175
182
|
<Tooltip title={this.context.t('The button is hidden and only visible when the UI Editor is active')}>
|
|
176
183
|
<Button {...props} onClick={this.onClick.bind(this)} icon={icon} style={{ opacity: '0.3' }}>
|
|
177
|
-
{
|
|
184
|
+
{titleContent}
|
|
178
185
|
</Button>
|
|
179
186
|
</Tooltip>
|
|
180
187
|
);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { render, screen } from '@nocobase/test/client';
|
|
2
|
+
import { FlowEngine, FlowEngineProvider } from '@nocobase/flow-engine';
|
|
3
|
+
import { App, ConfigProvider } from 'antd';
|
|
4
|
+
import React from 'react';
|
|
5
|
+
import { describe, expect, it } from 'vitest';
|
|
6
|
+
import { ActionModel } from '../ActionModel';
|
|
7
|
+
|
|
8
|
+
class NoIconActionModel extends ActionModel {
|
|
9
|
+
defaultProps = {
|
|
10
|
+
type: 'link' as const,
|
|
11
|
+
title: 'Open details',
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('ActionModel rendering', () => {
|
|
16
|
+
it('shows the title when iconOnly is true but no icon is configured', () => {
|
|
17
|
+
const engine = new FlowEngine();
|
|
18
|
+
engine.registerModels({ NoIconActionModel });
|
|
19
|
+
const model = engine.createModel<NoIconActionModel>({
|
|
20
|
+
use: 'NoIconActionModel',
|
|
21
|
+
props: {
|
|
22
|
+
title: 'Open details',
|
|
23
|
+
iconOnly: true,
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
render(
|
|
28
|
+
<FlowEngineProvider engine={engine}>
|
|
29
|
+
<ConfigProvider>
|
|
30
|
+
<App>{model.render()}</App>
|
|
31
|
+
</ConfigProvider>
|
|
32
|
+
</FlowEngineProvider>,
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
expect(screen.getByRole('button', { name: 'Open details' })).toBeInTheDocument();
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -210,7 +210,7 @@ FilterFormCustomFieldModel.registerFlow({
|
|
|
210
210
|
'x-decorator': 'FormItem',
|
|
211
211
|
required: true,
|
|
212
212
|
description:
|
|
213
|
-
'{{t("Randomly generated and can be modified. Support letters, numbers and underscores, must start with
|
|
213
|
+
'{{t("Randomly generated and can be modified. Support letters, numbers and underscores, must start with a letter.")}}',
|
|
214
214
|
},
|
|
215
215
|
source: {
|
|
216
216
|
type: 'array',
|
|
@@ -117,7 +117,7 @@ FormSubmitActionModel.registerFlow({
|
|
|
117
117
|
try {
|
|
118
118
|
ctx.model.setProps('loading', true);
|
|
119
119
|
const { submitHandler } = await import('./submitHandler');
|
|
120
|
-
await submitHandler(ctx, params);
|
|
120
|
+
return await submitHandler(ctx, params);
|
|
121
121
|
} catch (error) {
|
|
122
122
|
ctx.model.setProps('loading', false);
|
|
123
123
|
if (error instanceof FlowExitAllException) {
|