@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
|
@@ -285,13 +285,44 @@ function SetIsMobileLayout(props: { isMobile: boolean; children: any; model?: Ad
|
|
|
285
285
|
const flowEngine = useFlowEngine();
|
|
286
286
|
const adminLayoutModel = props.model || flowEngine.getModel<AdminLayoutModel>(ADMIN_LAYOUT_MODEL_UID);
|
|
287
287
|
|
|
288
|
-
|
|
288
|
+
useLayoutEffect(() => {
|
|
289
289
|
adminLayoutModel?.setIsMobileLayout(props.isMobile);
|
|
290
290
|
}, [adminLayoutModel, props.isMobile]);
|
|
291
291
|
|
|
292
292
|
return props.children;
|
|
293
293
|
}
|
|
294
294
|
|
|
295
|
+
function AdminLayoutContentWithMobileState(props: {
|
|
296
|
+
isMobile: boolean;
|
|
297
|
+
model?: AdminLayoutModel;
|
|
298
|
+
designable?: boolean;
|
|
299
|
+
layout?: AdminLayoutModel['layout'];
|
|
300
|
+
onContentElementChange?: (element: HTMLDivElement | null) => void;
|
|
301
|
+
}) {
|
|
302
|
+
const modelRef = useRef(props.model);
|
|
303
|
+
const isMobileRef = useRef(props.isMobile);
|
|
304
|
+
const onContentElementChangeRef = useRef(props.onContentElementChange);
|
|
305
|
+
|
|
306
|
+
modelRef.current = props.model;
|
|
307
|
+
isMobileRef.current = props.isMobile;
|
|
308
|
+
onContentElementChangeRef.current = props.onContentElementChange;
|
|
309
|
+
|
|
310
|
+
const handleContentElementChange = useCallback((element: HTMLDivElement | null) => {
|
|
311
|
+
if (element) {
|
|
312
|
+
modelRef.current?.setIsMobileLayout(isMobileRef.current);
|
|
313
|
+
}
|
|
314
|
+
onContentElementChangeRef.current?.(element);
|
|
315
|
+
}, []);
|
|
316
|
+
|
|
317
|
+
return (
|
|
318
|
+
<AdminLayoutContent
|
|
319
|
+
designable={props.designable}
|
|
320
|
+
layout={props.layout}
|
|
321
|
+
onContentElementChange={handleContentElementChange}
|
|
322
|
+
/>
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
295
326
|
const DesignerButtonMenuItem: FC<{ item: AdminLayoutMenuNode; fallbackParentRoute?: NocoBaseDesktopRoute }> = (
|
|
296
327
|
props,
|
|
297
328
|
) => {
|
|
@@ -410,8 +441,8 @@ export const AdminLayoutComponent = observer((props: any) => {
|
|
|
410
441
|
);
|
|
411
442
|
const designable = !isMobileSider && preferredFlowSettingsEnabled;
|
|
412
443
|
const { styles } = useHeaderStyle();
|
|
413
|
-
const { appList } = useApplications();
|
|
414
|
-
const appListRender = useAppListRender();
|
|
444
|
+
const { appList, appSwitcherModel } = useApplications(adminLayoutModel);
|
|
445
|
+
const appListRender = useAppListRender(appSwitcherModel);
|
|
415
446
|
const flowSettingsSyncRef = useRef(0);
|
|
416
447
|
const desiredFlowSettingsEnabledRef = useRef(false);
|
|
417
448
|
const handleLayoutContentElementChange = useCallback(
|
|
@@ -634,6 +665,10 @@ export const AdminLayoutComponent = observer((props: any) => {
|
|
|
634
665
|
z-index: 2000 !important;
|
|
635
666
|
}
|
|
636
667
|
|
|
668
|
+
.ant-pro-layout-apps-popover .ant-popover-content {
|
|
669
|
+
margin-top: 12px;
|
|
670
|
+
}
|
|
671
|
+
|
|
637
672
|
.ant-pro-layout-apps-icon {
|
|
638
673
|
color: ${customToken.colorTextHeaderMenu || 'rgba(255, 255, 255, 0.85)'};
|
|
639
674
|
}
|
|
@@ -717,7 +752,10 @@ export const AdminLayoutComponent = observer((props: any) => {
|
|
|
717
752
|
<SetIsMobileLayout isMobile={isMobile} model={adminLayoutModel}>
|
|
718
753
|
<ConfigProvider theme={isMobile ? mobileTheme : theme}>
|
|
719
754
|
<GlobalStyle />
|
|
720
|
-
<
|
|
755
|
+
<AdminLayoutContentWithMobileState
|
|
756
|
+
isMobile={isMobile}
|
|
757
|
+
model={adminLayoutModel}
|
|
758
|
+
designable={designable}
|
|
721
759
|
layout={adminLayoutModel?.layout}
|
|
722
760
|
onContentElementChange={handleLayoutContentElementChange}
|
|
723
761
|
/>
|
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
import { FlowEngine, FlowEngineProvider } from '@nocobase/flow-engine';
|
|
11
11
|
import { act, render, screen, waitFor } from '@testing-library/react';
|
|
12
12
|
import React from 'react';
|
|
13
|
-
import { createMemoryRouter, RouterProvider } from 'react-router-dom';
|
|
13
|
+
import { createMemoryRouter, Outlet, RouterProvider } from 'react-router-dom';
|
|
14
14
|
import { describe, expect, it, vi } from 'vitest';
|
|
15
|
+
import { NocoBaseDesktopRouteType } from '../../../flow-compat';
|
|
15
16
|
import { RouteRepository } from '../../../RouteRepository';
|
|
16
17
|
import { AdminLayoutEntryGuard } from './AdminLayoutEntryGuard';
|
|
17
18
|
import type { AdminLayoutModel } from './AdminLayoutModel';
|
|
@@ -307,4 +308,179 @@ describe('AdminLayoutEntryGuard', () => {
|
|
|
307
308
|
expect(activateLayout).toHaveBeenCalledTimes(2);
|
|
308
309
|
expect(activateLayout).toHaveBeenLastCalledWith(expect.objectContaining({ uid: 'admin-layout-next' }));
|
|
309
310
|
});
|
|
311
|
+
|
|
312
|
+
it('should redirect group id paths to the first v2 landing route', async () => {
|
|
313
|
+
const modernWindow = window as Window & { __nocobase_modern_client_prefix__?: string };
|
|
314
|
+
modernWindow.__nocobase_modern_client_prefix__ = 'v2';
|
|
315
|
+
const engine = new FlowEngine();
|
|
316
|
+
const request = vi.fn().mockResolvedValue({
|
|
317
|
+
data: {
|
|
318
|
+
data: [],
|
|
319
|
+
},
|
|
320
|
+
});
|
|
321
|
+
const routeRepository = new RouteRepository({
|
|
322
|
+
api: {
|
|
323
|
+
request,
|
|
324
|
+
resource: vi.fn(),
|
|
325
|
+
},
|
|
326
|
+
} as never);
|
|
327
|
+
routeRepository.setRoutes([
|
|
328
|
+
{
|
|
329
|
+
id: 1,
|
|
330
|
+
title: 'Group',
|
|
331
|
+
type: NocoBaseDesktopRouteType.group,
|
|
332
|
+
children: [
|
|
333
|
+
{
|
|
334
|
+
id: 11,
|
|
335
|
+
title: 'Flow page',
|
|
336
|
+
schemaUid: 'flow-page-1',
|
|
337
|
+
type: NocoBaseDesktopRouteType.flowPage,
|
|
338
|
+
},
|
|
339
|
+
],
|
|
340
|
+
},
|
|
341
|
+
]);
|
|
342
|
+
engine.context.defineProperty('routeRepository', {
|
|
343
|
+
value: routeRepository,
|
|
344
|
+
});
|
|
345
|
+
engine.context.defineProperty('app', {
|
|
346
|
+
value: {
|
|
347
|
+
getPublicPath: () => '/apps/demo/v2/',
|
|
348
|
+
router: {
|
|
349
|
+
getBasename: () => '/apps/demo/v2',
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
const model = {
|
|
355
|
+
layout: {
|
|
356
|
+
routeName: 'admin',
|
|
357
|
+
routePath: '/admin',
|
|
358
|
+
uid: 'admin-layout-model',
|
|
359
|
+
},
|
|
360
|
+
} as AdminLayoutModel;
|
|
361
|
+
const router = createMemoryRouter(
|
|
362
|
+
[
|
|
363
|
+
{
|
|
364
|
+
path: '/admin',
|
|
365
|
+
id: 'admin',
|
|
366
|
+
element: (
|
|
367
|
+
<FlowEngineProvider engine={engine}>
|
|
368
|
+
<AdminLayoutEntryGuard model={model}>
|
|
369
|
+
<Outlet />
|
|
370
|
+
</AdminLayoutEntryGuard>
|
|
371
|
+
</FlowEngineProvider>
|
|
372
|
+
),
|
|
373
|
+
children: [
|
|
374
|
+
{
|
|
375
|
+
path: ':name',
|
|
376
|
+
id: 'admin.__page',
|
|
377
|
+
element: <div>Admin page shell</div>,
|
|
378
|
+
},
|
|
379
|
+
],
|
|
380
|
+
},
|
|
381
|
+
],
|
|
382
|
+
{
|
|
383
|
+
initialEntries: ['/admin/1'],
|
|
384
|
+
},
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
try {
|
|
388
|
+
render(<RouterProvider router={router} />);
|
|
389
|
+
|
|
390
|
+
await waitFor(() => {
|
|
391
|
+
expect(router.state.location.pathname).toBe('/admin/flow-page-1');
|
|
392
|
+
});
|
|
393
|
+
expect(await screen.findByText('Admin page shell')).toBeInTheDocument();
|
|
394
|
+
} finally {
|
|
395
|
+
delete modernWindow.__nocobase_modern_client_prefix__;
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it('should prefer schema uid routes before resolving numeric group ids', async () => {
|
|
400
|
+
const modernWindow = window as Window & { __nocobase_modern_client_prefix__?: string };
|
|
401
|
+
modernWindow.__nocobase_modern_client_prefix__ = 'v2';
|
|
402
|
+
const engine = new FlowEngine();
|
|
403
|
+
const routeRepository = {
|
|
404
|
+
activateLayout: vi.fn(() => vi.fn()),
|
|
405
|
+
ensureAccessibleLoaded: vi.fn().mockResolvedValue([]),
|
|
406
|
+
isAccessibleLoaded: vi.fn(() => true),
|
|
407
|
+
getRouteBySchemaUid: vi.fn((pageUid: string) =>
|
|
408
|
+
pageUid === '1'
|
|
409
|
+
? {
|
|
410
|
+
schemaUid: '1',
|
|
411
|
+
type: NocoBaseDesktopRouteType.flowPage,
|
|
412
|
+
}
|
|
413
|
+
: undefined,
|
|
414
|
+
),
|
|
415
|
+
getRouteById: vi.fn((routeId: string) =>
|
|
416
|
+
routeId === '1'
|
|
417
|
+
? {
|
|
418
|
+
id: 1,
|
|
419
|
+
type: NocoBaseDesktopRouteType.group,
|
|
420
|
+
children: [
|
|
421
|
+
{
|
|
422
|
+
schemaUid: 'flow-page-1',
|
|
423
|
+
type: NocoBaseDesktopRouteType.flowPage,
|
|
424
|
+
},
|
|
425
|
+
],
|
|
426
|
+
}
|
|
427
|
+
: undefined,
|
|
428
|
+
),
|
|
429
|
+
listAccessible: vi.fn(() => []),
|
|
430
|
+
};
|
|
431
|
+
engine.context.defineProperty('routeRepository', {
|
|
432
|
+
value: routeRepository,
|
|
433
|
+
});
|
|
434
|
+
engine.context.defineProperty('app', {
|
|
435
|
+
value: {
|
|
436
|
+
getPublicPath: () => '/apps/demo/v2/',
|
|
437
|
+
router: {
|
|
438
|
+
getBasename: () => '/apps/demo/v2',
|
|
439
|
+
},
|
|
440
|
+
},
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
const model = {
|
|
444
|
+
layout: {
|
|
445
|
+
routeName: 'admin',
|
|
446
|
+
routePath: '/admin',
|
|
447
|
+
uid: 'admin-layout-model',
|
|
448
|
+
},
|
|
449
|
+
} as AdminLayoutModel;
|
|
450
|
+
const router = createMemoryRouter(
|
|
451
|
+
[
|
|
452
|
+
{
|
|
453
|
+
path: '/admin',
|
|
454
|
+
id: 'admin',
|
|
455
|
+
element: (
|
|
456
|
+
<FlowEngineProvider engine={engine}>
|
|
457
|
+
<AdminLayoutEntryGuard model={model}>
|
|
458
|
+
<Outlet />
|
|
459
|
+
</AdminLayoutEntryGuard>
|
|
460
|
+
</FlowEngineProvider>
|
|
461
|
+
),
|
|
462
|
+
children: [
|
|
463
|
+
{
|
|
464
|
+
path: ':name',
|
|
465
|
+
id: 'admin.__page',
|
|
466
|
+
element: <div>Admin page shell</div>,
|
|
467
|
+
},
|
|
468
|
+
],
|
|
469
|
+
},
|
|
470
|
+
],
|
|
471
|
+
{
|
|
472
|
+
initialEntries: ['/admin/1'],
|
|
473
|
+
},
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
try {
|
|
477
|
+
render(<RouterProvider router={router} />);
|
|
478
|
+
|
|
479
|
+
await screen.findByText('Admin page shell');
|
|
480
|
+
expect(router.state.location.pathname).toBe('/admin/1');
|
|
481
|
+
expect(routeRepository.getRouteById).not.toHaveBeenCalled();
|
|
482
|
+
} finally {
|
|
483
|
+
delete modernWindow.__nocobase_modern_client_prefix__;
|
|
484
|
+
}
|
|
485
|
+
});
|
|
310
486
|
});
|
|
@@ -132,6 +132,26 @@ export const AdminLayoutEntryGuard: FC<{ children: React.ReactNode; model?: Admi
|
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
const groupRoute = currentRoute ? undefined : routeRepository?.getRouteById?.(pageUid);
|
|
136
|
+
if (!currentRoute && groupRoute?.type === NocoBaseDesktopRouteType.group) {
|
|
137
|
+
const target = resolveAdminRouteRuntimeTarget({
|
|
138
|
+
app,
|
|
139
|
+
route: groupRoute,
|
|
140
|
+
layout: layoutRuntime,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
if (target.runtimePath) {
|
|
144
|
+
replaceTriggeredRef.current = true;
|
|
145
|
+
if (target.navigationMode === 'document') {
|
|
146
|
+
window.location.replace(target.runtimePath);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
navigate(toRouterNavigationPath(target.runtimePath, app.router.getBasename()), { replace: true });
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
135
155
|
if (active) {
|
|
136
156
|
setReady(true);
|
|
137
157
|
}
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
type AdminLayoutContentProps = {
|
|
27
27
|
onContentElementChange?: (element: HTMLDivElement | null) => void;
|
|
28
28
|
layout?: (AdminLayoutRoutePathLike & { routeName?: string }) | null;
|
|
29
|
+
designable?: boolean;
|
|
29
30
|
};
|
|
30
31
|
|
|
31
32
|
const layoutContentClass = css`
|
|
@@ -70,7 +71,7 @@ function isDvhSupported() {
|
|
|
70
71
|
return testEl.style.height === '1dvh';
|
|
71
72
|
}
|
|
72
73
|
|
|
73
|
-
const ShowTipWhenNoPages = observer((props: { layout?: AdminLayoutRoutePathLike | null }) => {
|
|
74
|
+
const ShowTipWhenNoPages = observer((props: { designable?: boolean; layout?: AdminLayoutRoutePathLike | null }) => {
|
|
74
75
|
const flowEngine = useFlowEngine();
|
|
75
76
|
const { token } = antdTheme.useToken();
|
|
76
77
|
const { t } = useTranslation();
|
|
@@ -79,7 +80,7 @@ const ShowTipWhenNoPages = observer((props: { layout?: AdminLayoutRoutePathLike
|
|
|
79
80
|
const visibleRoutes = isV2AdminRuntime(flowEngine.context.app)
|
|
80
81
|
? allAccessRoutes.filter((route) => isV2MenuRoute(route))
|
|
81
82
|
: allAccessRoutes;
|
|
82
|
-
const designable = !!flowEngine.context.flowSettingsEnabled;
|
|
83
|
+
const designable = !!props.designable || !!flowEngine.context.flowSettingsEnabled;
|
|
83
84
|
const layoutRoutePath = getAdminLayoutRoutePath(props.layout);
|
|
84
85
|
|
|
85
86
|
if (
|
|
@@ -104,7 +105,7 @@ const ShowTipWhenNoPages = observer((props: { layout?: AdminLayoutRoutePathLike
|
|
|
104
105
|
*
|
|
105
106
|
* 内容区不再依赖独立 FlowModel,而是通过回调把挂载目标同步给 root model。
|
|
106
107
|
*/
|
|
107
|
-
export const AdminLayoutContent: FC<AdminLayoutContentProps> = ({ onContentElementChange, layout }) => {
|
|
108
|
+
export const AdminLayoutContent: FC<AdminLayoutContentProps> = ({ designable, onContentElementChange, layout }) => {
|
|
108
109
|
const style = useMemo(() => (isDvhSupported() ? mobileHeight : undefined), []);
|
|
109
110
|
const params = useParams();
|
|
110
111
|
const matches = useMatches();
|
|
@@ -131,7 +132,7 @@ export const AdminLayoutContent: FC<AdminLayoutContentProps> = ({ onContentEleme
|
|
|
131
132
|
>
|
|
132
133
|
<div style={pageContentStyle}>
|
|
133
134
|
{shouldKeepAlive && pageUid ? <KeepAlive uid={pageUid}>{() => <Outlet />}</KeepAlive> : <Outlet />}
|
|
134
|
-
<ShowTipWhenNoPages layout={layout} />
|
|
135
|
+
<ShowTipWhenNoPages designable={designable} layout={layout} />
|
|
135
136
|
</div>
|
|
136
137
|
</div>
|
|
137
138
|
);
|
|
@@ -10,17 +10,28 @@
|
|
|
10
10
|
import type { AppListProps } from '@ant-design/pro-layout/es/components/AppsLogoComponents/types';
|
|
11
11
|
import { css } from '@emotion/css';
|
|
12
12
|
import { Card, Typography, theme } from 'antd';
|
|
13
|
+
import { observer } from '@nocobase/flow-engine';
|
|
13
14
|
import React from 'react';
|
|
15
|
+
import type { AppSwitcherActionPanelModel } from './AppSwitcherActionPanelModel';
|
|
14
16
|
|
|
15
17
|
function renderIcon(icon: React.ReactNode | (() => React.ReactNode)) {
|
|
16
18
|
return typeof icon === 'function' ? icon() : icon;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
|
-
|
|
21
|
+
const AppSwitcherActionsContent = observer(
|
|
22
|
+
(props: { model: AppSwitcherActionPanelModel }) => <>{props.model.renderContent()}</>,
|
|
23
|
+
{ displayName: 'AppSwitcherActionsContent' },
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
export function useAppListRender(appSwitcherModel?: AppSwitcherActionPanelModel) {
|
|
20
27
|
const { token } = theme.useToken();
|
|
21
28
|
|
|
22
29
|
return React.useCallback(
|
|
23
30
|
(appList: AppListProps) => {
|
|
31
|
+
if (appSwitcherModel?.hasActions() || appSwitcherModel?.context.flowSettingsEnabled) {
|
|
32
|
+
return <AppSwitcherActionsContent model={appSwitcherModel} />;
|
|
33
|
+
}
|
|
34
|
+
|
|
24
35
|
const columnCount = Math.min(Math.max(appList.length, 1), 2);
|
|
25
36
|
|
|
26
37
|
return (
|
|
@@ -134,6 +145,6 @@ export function useAppListRender() {
|
|
|
134
145
|
</div>
|
|
135
146
|
);
|
|
136
147
|
},
|
|
137
|
-
[token],
|
|
148
|
+
[appSwitcherModel, token],
|
|
138
149
|
);
|
|
139
150
|
}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { SettingOutlined } from '@ant-design/icons';
|
|
11
|
+
import { css } from '@emotion/css';
|
|
12
|
+
import { Icon } from '../../../components';
|
|
13
|
+
import { ActionModel } from '../../models/base';
|
|
14
|
+
import {
|
|
15
|
+
AddSubModelButton,
|
|
16
|
+
DndProvider,
|
|
17
|
+
DragHandler,
|
|
18
|
+
Droppable,
|
|
19
|
+
FlowModel,
|
|
20
|
+
FlowModelRenderer,
|
|
21
|
+
FlowSettingsButton,
|
|
22
|
+
mergeSubModelItems,
|
|
23
|
+
observer,
|
|
24
|
+
type SubModelItem,
|
|
25
|
+
type SubModelItemsType,
|
|
26
|
+
} from '@nocobase/flow-engine';
|
|
27
|
+
import { Avatar, Button, Card, Tooltip, Typography } from 'antd';
|
|
28
|
+
import React from 'react';
|
|
29
|
+
|
|
30
|
+
const defaultIconColors = ['#3d8bff', '#00a8b5', '#35b26b', '#f5a623', '#ff7a45', '#e85d75', '#9254de', '#597ef7'];
|
|
31
|
+
|
|
32
|
+
function getDefaultIconColor(title?: React.ReactNode) {
|
|
33
|
+
const source = typeof title === 'string' ? title : '';
|
|
34
|
+
const hash = Array.from(source).reduce((sum, char) => sum + char.charCodeAt(0), 0);
|
|
35
|
+
return defaultIconColors[hash % defaultIconColors.length];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type AppSwitcherActionPanelStructure = {
|
|
39
|
+
subModels: {
|
|
40
|
+
actions?: ActionModel[];
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
function isRenderableActionModel(action: unknown): action is ActionModel {
|
|
45
|
+
if (!action || typeof action !== 'object') {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const candidate = action as Partial<ActionModel>;
|
|
50
|
+
return typeof candidate.getTitle === 'function' && typeof candidate.onClick === 'function';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
type EntryActionAvailability = {
|
|
54
|
+
isEntryActionAvailable?: () => boolean;
|
|
55
|
+
getEntryActionUnavailableMessage?: () => string | undefined;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
function isEntryActionUnavailable(action: ActionModel) {
|
|
59
|
+
const candidate = action as ActionModel & EntryActionAvailability;
|
|
60
|
+
return typeof candidate.isEntryActionAvailable === 'function' && !candidate.isEntryActionAvailable();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function getEntryActionUnavailableMessage(action: ActionModel) {
|
|
64
|
+
const candidate = action as ActionModel & EntryActionAvailability;
|
|
65
|
+
return candidate.getEntryActionUnavailableMessage?.();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export class AppSwitcherActionPanelModel extends FlowModel<AppSwitcherActionPanelStructure> {
|
|
69
|
+
getConfigureActionsItems(): SubModelItemsType {
|
|
70
|
+
const linkAction: SubModelItem = {
|
|
71
|
+
key: 'app-switcher:link',
|
|
72
|
+
label: this.context.t('Link'),
|
|
73
|
+
createModelOptions: {
|
|
74
|
+
use: 'LinkActionModel',
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
const jsAction: SubModelItem = {
|
|
78
|
+
key: 'app-switcher:js-action',
|
|
79
|
+
label: this.context.t('JS action'),
|
|
80
|
+
createModelOptions: {
|
|
81
|
+
use: 'JSActionModel',
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
return mergeSubModelItems([[linkAction], this.context.app.entryActionManager.getItems('app-switcher'), [jsAction]]);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
renderConfigureActions() {
|
|
89
|
+
return <AppSwitcherConfigureActionsButton model={this} />;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private getRenderableActions(options: { includeHidden?: boolean; includeUnavailable?: boolean } = {}) {
|
|
93
|
+
return this.mapSubModels('actions', (action) => action)
|
|
94
|
+
.filter(isRenderableActionModel)
|
|
95
|
+
.filter((action) => options.includeUnavailable || !isEntryActionUnavailable(action))
|
|
96
|
+
.filter((action) => options.includeHidden || !action.hidden);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
hasActions() {
|
|
100
|
+
return this.getRenderableActions().length > 0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
renderContent() {
|
|
104
|
+
const token = this.context.themeToken;
|
|
105
|
+
const designable = !!this.context.flowSettingsEnabled;
|
|
106
|
+
const actions = this.getRenderableActions({ includeHidden: designable, includeUnavailable: designable });
|
|
107
|
+
const columnCount = Math.min(Math.max(actions.length || 1, 1), 2);
|
|
108
|
+
const buttonResetClass = css`
|
|
109
|
+
&.ant-btn {
|
|
110
|
+
display: block;
|
|
111
|
+
width: 100%;
|
|
112
|
+
height: 100%;
|
|
113
|
+
padding: 0;
|
|
114
|
+
border: none;
|
|
115
|
+
box-shadow: none;
|
|
116
|
+
background: none;
|
|
117
|
+
color: ${token.colorText};
|
|
118
|
+
text-align: left;
|
|
119
|
+
}
|
|
120
|
+
&.ant-btn > span {
|
|
121
|
+
display: block;
|
|
122
|
+
width: 100%;
|
|
123
|
+
height: 100%;
|
|
124
|
+
}
|
|
125
|
+
.ant-btn-icon {
|
|
126
|
+
display: none;
|
|
127
|
+
}
|
|
128
|
+
`;
|
|
129
|
+
const contentClass = css`
|
|
130
|
+
max-width: calc(100vw - 48px);
|
|
131
|
+
max-height: calc(100vh - 48px);
|
|
132
|
+
overflow: auto;
|
|
133
|
+
padding: ${token.paddingXS}px;
|
|
134
|
+
`;
|
|
135
|
+
const listClass = css`
|
|
136
|
+
display: grid;
|
|
137
|
+
grid-template-columns: repeat(${columnCount}, 260px);
|
|
138
|
+
gap: ${token.marginXXS}px;
|
|
139
|
+
margin: 0;
|
|
140
|
+
padding: 0;
|
|
141
|
+
list-style: none;
|
|
142
|
+
`;
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<div className={contentClass}>
|
|
146
|
+
{actions.length ? (
|
|
147
|
+
<DndProvider>
|
|
148
|
+
<ul className={listClass}>
|
|
149
|
+
{actions.map((action) => {
|
|
150
|
+
const { icon = 'AppstoreOutlined', color } = action.props;
|
|
151
|
+
const title = action.getTitle();
|
|
152
|
+
const entryActionUnavailable = isEntryActionUnavailable(action);
|
|
153
|
+
const renderActionContent = (compact = false) => (
|
|
154
|
+
<Card
|
|
155
|
+
bordered={false}
|
|
156
|
+
className={css`
|
|
157
|
+
height: 56px;
|
|
158
|
+
border-radius: ${token.borderRadius}px;
|
|
159
|
+
box-shadow: none !important;
|
|
160
|
+
opacity: ${compact ? token.opacityLoading : 1};
|
|
161
|
+
|
|
162
|
+
&:hover {
|
|
163
|
+
background: ${token.colorBgTextHover};
|
|
164
|
+
box-shadow: none !important;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.ant-card-body {
|
|
168
|
+
height: 100%;
|
|
169
|
+
padding: ${token.paddingXS}px;
|
|
170
|
+
display: flex;
|
|
171
|
+
align-items: center;
|
|
172
|
+
gap: ${token.marginXS}px;
|
|
173
|
+
}
|
|
174
|
+
`}
|
|
175
|
+
styles={{ body: { background: 'transparent' } }}
|
|
176
|
+
>
|
|
177
|
+
<Avatar
|
|
178
|
+
size={32}
|
|
179
|
+
shape="square"
|
|
180
|
+
icon={<Icon type={icon as string} />}
|
|
181
|
+
style={{
|
|
182
|
+
flex: '0 0 auto',
|
|
183
|
+
borderRadius: 6,
|
|
184
|
+
background: color || getDefaultIconColor(title),
|
|
185
|
+
}}
|
|
186
|
+
/>
|
|
187
|
+
<Typography.Text
|
|
188
|
+
ellipsis
|
|
189
|
+
title={typeof title === 'string' ? title : undefined}
|
|
190
|
+
style={{
|
|
191
|
+
minWidth: 0,
|
|
192
|
+
color: token.colorText,
|
|
193
|
+
fontSize: token.fontSize,
|
|
194
|
+
lineHeight: token.lineHeight,
|
|
195
|
+
}}
|
|
196
|
+
>
|
|
197
|
+
{title}
|
|
198
|
+
</Typography.Text>
|
|
199
|
+
</Card>
|
|
200
|
+
);
|
|
201
|
+
action.enableEditDanger = false;
|
|
202
|
+
action.enableEditType = false;
|
|
203
|
+
action.enableEditIconOnly = false;
|
|
204
|
+
action.enableEditColor = true;
|
|
205
|
+
action.renderButton = () => {
|
|
206
|
+
if (entryActionUnavailable) {
|
|
207
|
+
return (
|
|
208
|
+
<Tooltip title={getEntryActionUnavailableMessage(action)}>
|
|
209
|
+
<Button className={buttonResetClass} onClick={action.onClick.bind(action)}>
|
|
210
|
+
{renderActionContent(true)}
|
|
211
|
+
</Button>
|
|
212
|
+
</Tooltip>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
return (
|
|
216
|
+
<Button className={buttonResetClass} onClick={action.onClick.bind(action)}>
|
|
217
|
+
{renderActionContent()}
|
|
218
|
+
</Button>
|
|
219
|
+
);
|
|
220
|
+
};
|
|
221
|
+
action.renderHiddenInConfig = () => (
|
|
222
|
+
<Tooltip
|
|
223
|
+
title={
|
|
224
|
+
entryActionUnavailable
|
|
225
|
+
? getEntryActionUnavailableMessage(action)
|
|
226
|
+
: this.context.t('The button is hidden and only visible when the UI Editor is active')
|
|
227
|
+
}
|
|
228
|
+
>
|
|
229
|
+
<Button className={buttonResetClass} onClick={action.onClick.bind(action)}>
|
|
230
|
+
{renderActionContent(true)}
|
|
231
|
+
</Button>
|
|
232
|
+
</Tooltip>
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
return (
|
|
236
|
+
<li key={action.uid} style={{ width: 260, height: 56, listStyle: 'none' }}>
|
|
237
|
+
<Droppable model={action}>
|
|
238
|
+
<FlowModelRenderer
|
|
239
|
+
model={action}
|
|
240
|
+
showFlowSettings={{ showBackground: false, showBorder: false, toolbarPosition: 'above' }}
|
|
241
|
+
extraToolbarItems={
|
|
242
|
+
designable
|
|
243
|
+
? [
|
|
244
|
+
{
|
|
245
|
+
key: 'drag-handler',
|
|
246
|
+
component: DragHandler,
|
|
247
|
+
sort: 1,
|
|
248
|
+
},
|
|
249
|
+
]
|
|
250
|
+
: []
|
|
251
|
+
}
|
|
252
|
+
/>
|
|
253
|
+
</Droppable>
|
|
254
|
+
</li>
|
|
255
|
+
);
|
|
256
|
+
})}
|
|
257
|
+
</ul>
|
|
258
|
+
</DndProvider>
|
|
259
|
+
) : (
|
|
260
|
+
<div style={{ width: 260, minHeight: token.marginXS }} />
|
|
261
|
+
)}
|
|
262
|
+
{designable && <div style={{ marginTop: token.marginXS }}>{this.renderConfigureActions()}</div>}
|
|
263
|
+
</div>
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
AppSwitcherActionPanelModel.define({
|
|
269
|
+
label: 'App switcher actions',
|
|
270
|
+
children: false,
|
|
271
|
+
createModelOptions: {
|
|
272
|
+
use: 'AppSwitcherActionPanelModel',
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
const AppSwitcherConfigureActionsButton = observer(({ model }: { model: AppSwitcherActionPanelModel }) => {
|
|
277
|
+
const entryActionsRevision = model.context.app.entryActionManager.revision;
|
|
278
|
+
return (
|
|
279
|
+
<AddSubModelButton
|
|
280
|
+
key={`app-switcher-add-actions-${entryActionsRevision}`}
|
|
281
|
+
model={model}
|
|
282
|
+
items={model.getConfigureActionsItems()}
|
|
283
|
+
subModelKey="actions"
|
|
284
|
+
keepDropdownOpen
|
|
285
|
+
>
|
|
286
|
+
<FlowSettingsButton icon={<SettingOutlined />}>{model.context.t('Actions')}</FlowSettingsButton>
|
|
287
|
+
</AddSubModelButton>
|
|
288
|
+
);
|
|
289
|
+
});
|