@nocobase/app-template-default 1.0.0-beta.88 → 1.0.0-beta.90
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/.agents/rules/components.md +3 -1
- package/.agents/rules/shell.md +12 -2
- package/AGENTS.md +1 -1
- package/LICENSE.txt +129 -0
- package/client/components/back-button.tsx +37 -12
- package/client/components/brand-loading-mark.tsx +5 -2
- package/client/components/brand-mark-geometry.ts +3 -3
- package/client/components/breadcrumbs.tsx +47 -3
- package/client/components/data-table/data-table.tsx +90 -19
- package/client/components/data-table/pagination.tsx +17 -2
- package/client/components/list-feedback.tsx +31 -22
- package/client/components/loading.tsx +127 -16
- package/client/components/pending-button.tsx +111 -0
- package/client/components/role-list.tsx +7 -0
- package/client/components/route-child-page.tsx +72 -1
- package/client/components/route-overlay.tsx +149 -15
- package/client/components/task-progress.tsx +155 -0
- package/client/components/use-route-overlay.ts +8 -11
- package/client/extensions/nocobase-file-component-ui/components/file-upload-field.tsx +62 -32
- package/client/extensions/nocobase-file-component-ui/locales/de-DE.ts +2 -0
- package/client/extensions/nocobase-file-component-ui/locales/en-US.ts +1 -0
- package/client/extensions/nocobase-file-component-ui/locales/es-ES.ts +1 -0
- package/client/extensions/nocobase-file-component-ui/locales/fr-FR.ts +1 -0
- package/client/extensions/nocobase-file-component-ui/locales/id-ID.ts +1 -0
- package/client/extensions/nocobase-file-component-ui/locales/ja-JP.ts +2 -0
- package/client/extensions/nocobase-file-component-ui/locales/pt-BR.ts +1 -0
- package/client/extensions/nocobase-file-component-ui/locales/vi-VN.ts +1 -0
- package/client/extensions/nocobase-file-component-ui/locales/zh-CN.ts +1 -0
- package/client/extensions/nocobase-file-component-ui/locales/zh-TW.ts +1 -0
- package/client/extensions/nocobase-workflow-management/README.md +5 -5
- package/client/extensions/nocobase-workflow-management/definition-list.tsx +2 -9
- package/client/extensions/nocobase-workflow-management/execution-list.tsx +2 -4
- package/client/layouts/app-layout.tsx +0 -4
- package/client/lib/navigation-transform.ts +29 -1
- package/client/locales/system/en-US.ts +23 -0
- package/client/locales/system/zh-CN.ts +23 -0
- package/client/pages/home/index.tsx +66 -51
- package/client/pages/settings/database-explorer/en-US.ts +33 -0
- package/client/pages/settings/database-explorer/index.tsx +358 -0
- package/client/pages/settings/database-explorer/routes.ts +14 -0
- package/client/pages/settings/database-explorer/zh-CN.ts +30 -0
- package/client/pages/settings/default-access.tsx +1 -1
- package/client/pages/settings/roles.tsx +27 -27
- package/client/pages/settings/routes.ts +15 -7
- package/client/pages/settings/user-detail.tsx +3 -17
- package/client/pages/settings/user-form.tsx +9 -15
- package/client/pages/settings/users.tsx +11 -27
- package/client/plugins.ts +8 -1
- package/client/routes.ts +2 -1
- package/client/routing/client-route.tsx +199 -37
- package/client/routing/navigation-destination.ts +42 -0
- package/client/routing/route-leave-boundary.tsx +13 -1
- package/client/styles.css +18 -1
- package/config.example.yml +17 -11
- package/index.html +8 -11
- package/package.json +27 -27
- package/server/config/auth.ts +1 -1
- package/server/config/index.ts +3 -0
- package/server/config/jobs.ts +16 -8
- package/server/config/queue.ts +14 -13
- package/server/config/realtime.ts +4 -0
- package/tests/client/auth-i18n.test.tsx +1 -1
- package/tests/client/authenticated-client-fixture.test.tsx +37 -0
- package/tests/client/back-button.test.tsx +34 -0
- package/tests/client/client-route-recovery.test.tsx +93 -1
- package/tests/client/client-routes.test.ts +5 -3
- package/tests/client/database-explorer-integration.test.tsx +30 -0
- package/tests/client/database-explorer.test.tsx +152 -0
- package/tests/client/foundation-integration.test.ts +42 -29
- package/tests/client/home.test.tsx +26 -85
- package/tests/client/layout-i18n.test.tsx +83 -2
- package/tests/client/list-feedback.test.tsx +5 -5
- package/tests/client/list-loading.test.tsx +397 -0
- package/tests/client/loading.test.tsx +20 -11
- package/tests/client/manual-list-retry.test.tsx +2 -2
- package/tests/client/navigation-parent.test.tsx +172 -0
- package/tests/client/pending-button.test.tsx +106 -0
- package/tests/client/permission-management-tabs.test.tsx +48 -12
- package/tests/client/primitive-labels.test.tsx +2 -2
- package/tests/client/role-return.test.tsx +21 -3
- package/tests/client/roles.test.tsx +84 -3
- package/tests/client/route-leave-recheck.test.tsx +74 -0
- package/tests/client/route-presentation.test.tsx +557 -0
- package/tests/client/session-preparation.test.tsx +20 -17
- package/tests/client/settings-continuity.test.tsx +35 -11
- package/tests/client/settings-home.test.tsx +4 -2
- package/tests/client/settings-idle-list.test.tsx +124 -0
- package/tests/client/settings-navigation.test.ts +20 -0
- package/tests/client/user-drawer.test.tsx +3 -4
- package/tests/fixtures/dialog-footer.html +12 -0
- package/tests/fixtures/dialog-footer.tsx +215 -0
- package/tests/helpers/authenticated-client.tsx +77 -22
- package/tests/playwright/dialog-footer.test.ts +227 -0
- package/tests/project/bootstrap-loading.test.ts +60 -0
- package/tests/server/app-server.test.ts +18 -12
- package/tests/server/auth-email-config.test.ts +21 -0
- package/tests/server/backends-config.test.ts +118 -0
- package/tests/server/concurrency.test.ts +1 -1
- package/tests/server/config.test.ts +22 -14
- package/tests/server/lifecycle.test.ts +2 -2
- package/tests/server/realtime-config.test.ts +114 -0
- package/client/pages/home/capabilities.ts +0 -62
- package/client/pages/home/prompt-dialog.tsx +0 -122
|
@@ -5,8 +5,8 @@ Install with `yes n | pnpm exec shadcn add @nocobase/workflow-management`. This
|
|
|
5
5
|
| File | Exports and responsibility |
|
|
6
6
|
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
7
7
|
| `layout.tsx` | `WorkflowManagementLayout`: page container, shared header and line-style tabs, hidden in detail mode. `WorkflowDefinitionDetail`: diagram and parameter form cards in two columns. |
|
|
8
|
-
| `definition-list.tsx` | `WorkflowDefinitionList`: optional category rail, search slot
|
|
9
|
-
| `execution-list.tsx` | `WorkflowExecutionList`: filters slot,
|
|
8
|
+
| `definition-list.tsx` | `WorkflowDefinitionList`: optional category rail, search slot and execution-activity sort controls, adapted definition rows and empty/status content. |
|
|
9
|
+
| `execution-list.tsx` | `WorkflowExecutionList`: filters slot, controlled ordering, adapted execution rows, empty/status content and controlled pagination. |
|
|
10
10
|
| `execution-detail.tsx` | `WorkflowExecutionDetail`: ordered history, step selection, diagram rendering callback, effects and optional recorded-parameter content. |
|
|
11
11
|
|
|
12
12
|
## Routes and ownership
|
|
@@ -53,11 +53,11 @@ const routes = defineAppRoutes([
|
|
|
53
53
|
|
|
54
54
|
Add definition and execution detail routes beneath the corresponding tab route, also without `navigation`. The parent page maps the URL to `tab`, navigates in `onTabChange`, and supplies its `Outlet` as children. Reuse an existing system group instead of registering another one.
|
|
55
55
|
|
|
56
|
-
Keep requests, query parameters, IME-aware search, filtering, pagination, category CRUD and permissions in application adapters. Map rows into display values and router links before passing them to the lists. `loaded` distinguishes an empty response from data that has not arrived; `loading`
|
|
56
|
+
Keep requests, query parameters, IME-aware search, filtering, pagination, category CRUD and permissions in application adapters. Map rows into display values and router links before passing them to the lists. `loaded` distinguishes an empty response from data that has not arrived; `loading` supplies pending feedback, and the caller disables pagination while pending. Supply error/loading/retry content through `status`, and separate empty-catalog and no-results wording through `empty`. The search slot stays compact on wider screens; the category rail stacks above the list on small screens. Omit `categories` when classification is unnecessary. The installable demo provides in-memory category filtering, named color selection and add/edit/delete dialogs as an application adapter; deleting a category preserves its workflows and clears the selected category filter. Its definition header also demonstrates a colored category dropdown with an Uncategorized option; assignment uses the same category state as list filtering. The production adapter keeps the assignment API and read/update permission checks in the application.
|
|
57
57
|
|
|
58
58
|
`WorkflowDefinitionDetail` accepts a diagram slot and a parameter form slot. Render the installed `@nocobase/lifecycle-diagram` or an existing compatible diagram in a definite-height or content-sized canvas. Keep schema validation, effective timing, complete-group saves, CAS conflicts and read-only permissions in the application's parameter editor. Show a translated empty state when a definition declares no adjustable parameters.
|
|
59
59
|
|
|
60
|
-
`WorkflowExecutionDetail` sorts transitions by version, timestamp and id without mutating the input. Selecting a step passes only that history prefix and its destination state to `renderDiagram`; no transition or server action occurs. Pass the runtime's transition records directly, retaining extra fields through its generic history type. `effects` displays persisted statuses, attempts and failures; it does not retry them. Pass recorded parameter content through `parameters` and label historical facts separately from current configuration. Remount the detail on record changes
|
|
60
|
+
`WorkflowExecutionDetail` sorts transitions by version, timestamp and id without mutating the input. Selecting a step passes only that history prefix and its destination state to `renderDiagram`; no transition or server action occurs. Pass the runtime's transition records directly, retaining extra fields through its generic history type. `effects` displays persisted statuses, attempts and failures; it does not retry them. Pass recorded parameter content through `parameters` and label historical facts separately from current configuration. Remount the detail on record identity changes; preserve a selected historical step during automatic updates.
|
|
61
61
|
|
|
62
62
|
The application owns authentication, initial administrator grants, navigation and direct-page guards, server read/update authorization and any recovery actions. Configuration permission does not grant business approval permission. Installing this block creates source files, not routes, permissions, runtime services or database collections.
|
|
63
63
|
|
|
@@ -71,4 +71,4 @@ Omit `WorkflowDefinitionDetail.parameters` when the editor opens contextually so
|
|
|
71
71
|
|
|
72
72
|
Ordering is optional: omit `onSort` when activity ordering is not supported so the header stays plain text. Omit a row's optional `name` when its linked `title` already supplies the complete display name. Hosts own filtering, pagination and their URL state; keep that state when opening and returning from a definition or execution.
|
|
73
73
|
|
|
74
|
-
List hosts pass `listState={resolveListState(request, rows.length, hasEffectiveFilters)}` and `onClearFilters`, using the current request error and data. The DataTable dependency supplies shared loading, denial, session and failure copy; merge the `listFeedback` English/Chinese keys from the components README into application translations. `onRefresh`
|
|
74
|
+
List hosts pass `listState={resolveListState(request, rows.length, hasEffectiveFilters)}` and `onClearFilters`, using the current request error and data. The DataTable dependency supplies shared loading, denial, session and failure copy; merge the `listFeedback` English/Chinese keys from the components README into application translations. `onRefresh` handles contextual failed-read retry and must preserve the current query; ready lists render no Refresh action. The host owns subscription/snapshot updates for open progress views. Keep `loaded`, `loading`, custom `empty` content and existing permission checks when reconciling installed copies. A successful empty filter is `no-results`; an empty unfiltered read is `empty`. Hosts that retain the legacy status slot without a listState still own their error rendering.
|
|
@@ -27,7 +27,8 @@ export interface WorkflowDefinitionListProps {
|
|
|
27
27
|
loading: boolean;
|
|
28
28
|
empty?: ReactNode;
|
|
29
29
|
labels: {
|
|
30
|
-
|
|
30
|
+
/** Legacy adapter label; ready lists no longer render a Refresh control. */
|
|
31
|
+
refresh?: string;
|
|
31
32
|
workflow: string;
|
|
32
33
|
executionCount: string;
|
|
33
34
|
executionActivity: string;
|
|
@@ -66,14 +67,6 @@ export function WorkflowDefinitionList({
|
|
|
66
67
|
<div className='flex w-full min-w-0 flex-1 flex-col gap-4'>
|
|
67
68
|
<div className='flex flex-col items-stretch justify-between gap-2 sm:flex-row sm:items-center'>
|
|
68
69
|
<div className='min-w-0 flex-1 sm:max-w-xs'>{search}</div>
|
|
69
|
-
<Button
|
|
70
|
-
className='shrink-0 self-start sm:self-auto'
|
|
71
|
-
variant='outline'
|
|
72
|
-
disabled={loading}
|
|
73
|
-
onClick={onRefresh}
|
|
74
|
-
>
|
|
75
|
-
{labels.refresh}
|
|
76
|
-
</Button>
|
|
77
70
|
</div>
|
|
78
71
|
{status}
|
|
79
72
|
{(listState !== undefined || loaded || loading) && (
|
|
@@ -27,7 +27,8 @@ export interface WorkflowExecutionListProps {
|
|
|
27
27
|
status?: ReactNode;
|
|
28
28
|
empty?: ReactNode;
|
|
29
29
|
labels: {
|
|
30
|
-
|
|
30
|
+
/** Legacy adapter label; ready lists no longer render a Refresh control. */
|
|
31
|
+
refresh?: string;
|
|
31
32
|
record: string;
|
|
32
33
|
workflow: string;
|
|
33
34
|
state: string;
|
|
@@ -72,9 +73,6 @@ export function WorkflowExecutionList({
|
|
|
72
73
|
<section className='flex flex-col gap-4'>
|
|
73
74
|
<div className='flex flex-wrap items-center justify-between gap-3'>
|
|
74
75
|
<div className='flex flex-wrap items-center gap-2'>{filters}</div>
|
|
75
|
-
<Button variant='outline' disabled={loading} onClick={onRefresh}>
|
|
76
|
-
{labels.refresh}
|
|
77
|
-
</Button>
|
|
78
76
|
</div>
|
|
79
77
|
{status}
|
|
80
78
|
{(listState !== undefined || loaded || loading) && (
|
|
@@ -18,7 +18,6 @@ import {
|
|
|
18
18
|
AppSidebarToggle,
|
|
19
19
|
} from './components/app-sidebar.js';
|
|
20
20
|
import { NavigationMenu } from './components/navigation-menu.js';
|
|
21
|
-
import { AppBrand } from './components/app-brand.js';
|
|
22
21
|
import { HeaderActions } from './components/header-actions.js';
|
|
23
22
|
import { AppSidebarFooter } from './components/sidebar-footer.js';
|
|
24
23
|
import {
|
|
@@ -62,9 +61,6 @@ export function AppLayout({ routes }: AppLayoutProps): ReactElement {
|
|
|
62
61
|
<LayoutHeader className='sticky top-0 z-40 justify-between'>
|
|
63
62
|
<div className='flex min-w-0 items-center gap-3'>
|
|
64
63
|
<AppSidebarToggle />
|
|
65
|
-
<div className='md:hidden'>
|
|
66
|
-
<AppBrand />
|
|
67
|
-
</div>
|
|
68
64
|
<div className='h-5 w-px shrink-0 bg-border' />
|
|
69
65
|
{/* The current page's trail: the route tree's, or the one the page declares (`usePageBreadcrumb`). */}
|
|
70
66
|
<Breadcrumbs denied={denied} />
|
|
@@ -31,7 +31,35 @@ export function transformNavigation(
|
|
|
31
31
|
items: RouteNavigationItem[],
|
|
32
32
|
routes: readonly AppClientRegisteredRoute[],
|
|
33
33
|
): RouteNavigationItem[] {
|
|
34
|
-
|
|
34
|
+
const organized = organizeNavigation(items, routes);
|
|
35
|
+
const hasGuide = organized.some(
|
|
36
|
+
(item) =>
|
|
37
|
+
item.route.name === SETTINGS_GROUP &&
|
|
38
|
+
item.children.some(
|
|
39
|
+
(child) =>
|
|
40
|
+
child.route.packageName === '@nocobase/app-plugin-foundation' &&
|
|
41
|
+
child.route.name === 'capabilityGuide',
|
|
42
|
+
),
|
|
43
|
+
);
|
|
44
|
+
return collapseSettingsNavigation(organized).map((item) =>
|
|
45
|
+
hasGuide && item.route.name === SETTINGS_HOME
|
|
46
|
+
? {
|
|
47
|
+
...item,
|
|
48
|
+
activeRouteKeys: [
|
|
49
|
+
...(item.activeRouteKeys ?? []),
|
|
50
|
+
...routes
|
|
51
|
+
.filter(
|
|
52
|
+
(route) =>
|
|
53
|
+
route.packageName === '@nocobase/app-plugin-foundation' &&
|
|
54
|
+
['capabilityGuide', 'capabilityGuideEntry'].includes(
|
|
55
|
+
route.name,
|
|
56
|
+
),
|
|
57
|
+
)
|
|
58
|
+
.map((route) => `${route.packageName}:${route.path}:${route.id}`),
|
|
59
|
+
],
|
|
60
|
+
}
|
|
61
|
+
: item,
|
|
62
|
+
);
|
|
35
63
|
}
|
|
36
64
|
|
|
37
65
|
/**
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import databaseExplorer from '../../pages/settings/database-explorer/en-US.js';
|
|
1
2
|
import settings from './settings-en-US.js';
|
|
2
3
|
import type { LocaleResource } from '@nocobase/i18n';
|
|
3
4
|
import deviceApprovalEnUS from '#extensions/nocobase-device-approval/locales/en-US';
|
|
@@ -8,6 +9,21 @@ import inboxEnUS from '#extensions/nocobase-inbox/locales/en-US';
|
|
|
8
9
|
* upgrade replaces this file, so put the application's own copy in `../en-US.ts` rather than here.
|
|
9
10
|
*/
|
|
10
11
|
const systemEnUS = {
|
|
12
|
+
databaseExplorer,
|
|
13
|
+
pendingFeedback: {
|
|
14
|
+
working: 'Working…',
|
|
15
|
+
longWait: 'This is taking longer than usual. You can keep waiting.',
|
|
16
|
+
},
|
|
17
|
+
taskProgress: {
|
|
18
|
+
queued: 'Waiting to start',
|
|
19
|
+
running: 'In progress',
|
|
20
|
+
completed: 'Completed',
|
|
21
|
+
failed: 'Failed',
|
|
22
|
+
canceled: 'Canceled',
|
|
23
|
+
count: '{{processed}} of {{total}}',
|
|
24
|
+
processed: '{{processed}} processed',
|
|
25
|
+
background: 'It keeps running if you leave this page.',
|
|
26
|
+
},
|
|
11
27
|
listFeedback: {
|
|
12
28
|
loading: 'Loading',
|
|
13
29
|
empty: 'No data yet',
|
|
@@ -144,10 +160,14 @@ const systemEnUS = {
|
|
|
144
160
|
'auth.foundationDescription': 'Reliable data, access control and governance.',
|
|
145
161
|
'auth.marketingFooter': 'Freedom above. Confidence below.',
|
|
146
162
|
'status.loading': 'Loading',
|
|
163
|
+
'status.refreshing': 'Refreshing',
|
|
147
164
|
'status.loadingPage': 'Loading page',
|
|
148
165
|
'status.denied': 'Access denied',
|
|
149
166
|
'status.pageFailed': 'Unable to load page',
|
|
150
167
|
'status.retry': 'Retry',
|
|
168
|
+
'status.reloadPage': 'Reload page',
|
|
169
|
+
'status.retryFailedDescription':
|
|
170
|
+
'It still could not be loaded. Reloading the whole page may help; unsaved changes on this page may be lost.',
|
|
151
171
|
'navigation.brandHome': 'NocoBase home',
|
|
152
172
|
'navigation.brandApps': 'NocoBase applications',
|
|
153
173
|
'routeOverlay.close': 'Close',
|
|
@@ -401,6 +421,7 @@ const systemEnUS = {
|
|
|
401
421
|
signingOut: 'Signing out…',
|
|
402
422
|
},
|
|
403
423
|
navigation: {
|
|
424
|
+
resources: 'Resources',
|
|
404
425
|
home: 'Home',
|
|
405
426
|
open: 'Open navigation',
|
|
406
427
|
close: 'Close navigation',
|
|
@@ -412,6 +433,7 @@ const systemEnUS = {
|
|
|
412
433
|
breadcrumb: 'Breadcrumb',
|
|
413
434
|
breadcrumbMore: 'Show the levels in between',
|
|
414
435
|
back: 'Back',
|
|
436
|
+
backTo: 'Back to {{label}}',
|
|
415
437
|
},
|
|
416
438
|
dataTable: {
|
|
417
439
|
noResults: 'No results',
|
|
@@ -431,6 +453,7 @@ const systemEnUS = {
|
|
|
431
453
|
datePicker: {
|
|
432
454
|
placeholder: 'Pick a date',
|
|
433
455
|
rangePlaceholder: 'Pick a date range',
|
|
456
|
+
clear: 'Clear',
|
|
434
457
|
},
|
|
435
458
|
};
|
|
436
459
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import databaseExplorer from '../../pages/settings/database-explorer/zh-CN.js';
|
|
1
2
|
import settings from './settings-zh-CN.js';
|
|
2
3
|
import type { SystemResource } from './en-US.js';
|
|
3
4
|
import deviceApprovalZhCN from '#extensions/nocobase-device-approval/locales/zh-CN';
|
|
@@ -5,6 +6,21 @@ import inboxZhCN from '#extensions/nocobase-inbox/locales/zh-CN';
|
|
|
5
6
|
|
|
6
7
|
/** The Chinese copy of what the template ships; see `./en-US.ts`. */
|
|
7
8
|
const systemZhCN: SystemResource = {
|
|
9
|
+
databaseExplorer,
|
|
10
|
+
pendingFeedback: {
|
|
11
|
+
working: '正在处理…',
|
|
12
|
+
longWait: '耗时比平常更长,您可以继续等待。',
|
|
13
|
+
},
|
|
14
|
+
taskProgress: {
|
|
15
|
+
queued: '等待开始',
|
|
16
|
+
running: '进行中',
|
|
17
|
+
completed: '已完成',
|
|
18
|
+
failed: '失败',
|
|
19
|
+
canceled: '已取消',
|
|
20
|
+
count: '{{processed}} / {{total}}',
|
|
21
|
+
processed: '已处理 {{processed}}',
|
|
22
|
+
background: '离开此页面后任务会继续运行。',
|
|
23
|
+
},
|
|
8
24
|
listFeedback: {
|
|
9
25
|
loading: '加载中',
|
|
10
26
|
empty: '暂无数据',
|
|
@@ -133,10 +149,14 @@ const systemZhCN: SystemResource = {
|
|
|
133
149
|
'auth.foundationDescription': '可靠的数据、访问控制与治理',
|
|
134
150
|
'auth.marketingFooter': '自由构建,可靠支撑',
|
|
135
151
|
'status.loading': '加载中',
|
|
152
|
+
'status.refreshing': '正在刷新',
|
|
136
153
|
'status.loadingPage': '正在加载页面',
|
|
137
154
|
'status.denied': '无权访问',
|
|
138
155
|
'status.pageFailed': '无法加载页面',
|
|
139
156
|
'status.retry': '重试',
|
|
157
|
+
'status.reloadPage': '重新加载页面',
|
|
158
|
+
'status.retryFailedDescription':
|
|
159
|
+
'仍然无法加载。可以重新加载整个页面,但本页未保存的修改可能丢失。',
|
|
140
160
|
'navigation.brandHome': 'NocoBase 首页',
|
|
141
161
|
'navigation.brandApps': 'NocoBase 应用',
|
|
142
162
|
'routeOverlay.close': '关闭',
|
|
@@ -376,6 +396,7 @@ const systemZhCN: SystemResource = {
|
|
|
376
396
|
signingOut: '正在退出…',
|
|
377
397
|
},
|
|
378
398
|
navigation: {
|
|
399
|
+
resources: '参考资料',
|
|
379
400
|
home: '首页',
|
|
380
401
|
open: '打开导航',
|
|
381
402
|
close: '关闭导航',
|
|
@@ -387,6 +408,7 @@ const systemZhCN: SystemResource = {
|
|
|
387
408
|
breadcrumb: '面包屑',
|
|
388
409
|
breadcrumbMore: '显示中间层级',
|
|
389
410
|
back: '返回',
|
|
411
|
+
backTo: '返回{{label}}',
|
|
390
412
|
},
|
|
391
413
|
dataTable: {
|
|
392
414
|
noResults: '无结果',
|
|
@@ -406,6 +428,7 @@ const systemZhCN: SystemResource = {
|
|
|
406
428
|
datePicker: {
|
|
407
429
|
placeholder: '选择日期',
|
|
408
430
|
rangePlaceholder: '选择日期范围',
|
|
431
|
+
clear: '清除',
|
|
409
432
|
},
|
|
410
433
|
};
|
|
411
434
|
|
|
@@ -5,8 +5,28 @@ import {
|
|
|
5
5
|
CopyIcon,
|
|
6
6
|
MessageSquareTextIcon,
|
|
7
7
|
SparklesIcon,
|
|
8
|
+
ShieldCheckIcon,
|
|
9
|
+
PaperclipIcon,
|
|
10
|
+
GitBranchIcon,
|
|
11
|
+
BellIcon,
|
|
12
|
+
CalendarClockIcon,
|
|
13
|
+
BotIcon,
|
|
14
|
+
type LucideIcon,
|
|
8
15
|
} from 'lucide-react';
|
|
9
|
-
import { type ReactElement
|
|
16
|
+
import { type ReactElement } from 'react';
|
|
17
|
+
import { Link } from 'react-router';
|
|
18
|
+
import {
|
|
19
|
+
CAPABILITY_GUIDE_PATH,
|
|
20
|
+
capabilityPath,
|
|
21
|
+
createCapabilityCatalog,
|
|
22
|
+
entryContent,
|
|
23
|
+
format,
|
|
24
|
+
guideContent,
|
|
25
|
+
type CatalogEntry,
|
|
26
|
+
type GuideContent,
|
|
27
|
+
} from '@nocobase/app-plugin-foundation/capability-guide';
|
|
28
|
+
|
|
29
|
+
import applicationManifest from '../../../package.json' with { type: 'json' };
|
|
10
30
|
|
|
11
31
|
import { PageContainer } from '#components/page-container';
|
|
12
32
|
import { Button } from '#components/ui/button';
|
|
@@ -18,24 +38,25 @@ import {
|
|
|
18
38
|
CardTitle,
|
|
19
39
|
} from '#components/ui/card';
|
|
20
40
|
|
|
21
|
-
import { CAPABILITIES, type Capability } from './capabilities.js';
|
|
22
|
-
import { PromptDialog } from './prompt-dialog.js';
|
|
23
41
|
import { useCopy } from './use-copy.js';
|
|
24
42
|
|
|
43
|
+
// The cards and their wording come from the capability guide, built from this application's own dependencies, so
|
|
44
|
+
// the home page and the guide never disagree; only the icons are the page's own.
|
|
45
|
+
const catalog = createCapabilityCatalog(applicationManifest);
|
|
46
|
+
const HOME_ICONS: Readonly<Record<string, LucideIcon>> = {
|
|
47
|
+
'app-plugin-authorization': ShieldCheckIcon,
|
|
48
|
+
'app-plugin-file': PaperclipIcon,
|
|
49
|
+
lifecycle: GitBranchIcon,
|
|
50
|
+
'app-plugin-notification': BellIcon,
|
|
51
|
+
'app-plugin-scheduler': CalendarClockIcon,
|
|
52
|
+
'app-plugin-agents': BotIcon,
|
|
53
|
+
};
|
|
54
|
+
|
|
25
55
|
const STEPS = ['describe', 'build', 'review'] as const;
|
|
26
56
|
|
|
27
57
|
export default function HomePage(): ReactElement {
|
|
28
|
-
const {
|
|
29
|
-
const
|
|
30
|
-
const [open, setOpen] = useState(false);
|
|
31
|
-
// Changes on every opening, so the dialog remounts and drops the edits of the previous one.
|
|
32
|
-
const [openCount, setOpenCount] = useState(0);
|
|
33
|
-
|
|
34
|
-
const openCapability = (capability: Capability): void => {
|
|
35
|
-
setSelected(capability);
|
|
36
|
-
setOpenCount((count) => count + 1);
|
|
37
|
-
setOpen(true);
|
|
38
|
-
};
|
|
58
|
+
const { i18n } = useTranslation();
|
|
59
|
+
const content = guideContent(i18n.language);
|
|
39
60
|
|
|
40
61
|
return (
|
|
41
62
|
<PageContainer className='mx-auto max-w-6xl'>
|
|
@@ -44,33 +65,31 @@ export default function HomePage(): ReactElement {
|
|
|
44
65
|
aria-labelledby='home-capabilities'
|
|
45
66
|
className='flex flex-col gap-4'
|
|
46
67
|
>
|
|
47
|
-
<div className='flex flex-
|
|
48
|
-
<
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
68
|
+
<div className='flex flex-wrap items-end justify-between gap-2'>
|
|
69
|
+
<div className='flex flex-col gap-1'>
|
|
70
|
+
<h2 id='home-capabilities' className='text-base font-medium'>
|
|
71
|
+
{content.home.title}
|
|
72
|
+
</h2>
|
|
73
|
+
<p className='text-sm text-muted-foreground'>
|
|
74
|
+
{content.home.description}
|
|
75
|
+
</p>
|
|
76
|
+
</div>
|
|
77
|
+
<Link
|
|
78
|
+
to={CAPABILITY_GUIDE_PATH}
|
|
79
|
+
className='inline-flex items-center gap-1 text-sm font-medium text-primary hover:underline'
|
|
80
|
+
>
|
|
81
|
+
{format(content.home.viewAll, { count: catalog.entries.length })}
|
|
82
|
+
<ArrowRightIcon className='size-4' aria-hidden />
|
|
83
|
+
</Link>
|
|
54
84
|
</div>
|
|
55
85
|
<ul className='grid gap-4 sm:grid-cols-2 xl:grid-cols-3'>
|
|
56
|
-
{
|
|
57
|
-
<li key={
|
|
58
|
-
<CapabilityCard
|
|
59
|
-
capability={capability}
|
|
60
|
-
onOpen={() => openCapability(capability)}
|
|
61
|
-
/>
|
|
86
|
+
{catalog.featured.map((entry) => (
|
|
87
|
+
<li key={entry.id} className='flex'>
|
|
88
|
+
<CapabilityCard entry={entry} content={content} />
|
|
62
89
|
</li>
|
|
63
90
|
))}
|
|
64
91
|
</ul>
|
|
65
92
|
</section>
|
|
66
|
-
{selected ? (
|
|
67
|
-
<PromptDialog
|
|
68
|
-
key={openCount}
|
|
69
|
-
capability={selected}
|
|
70
|
-
open={open}
|
|
71
|
-
onOpenChange={setOpen}
|
|
72
|
-
/>
|
|
73
|
-
) : null}
|
|
74
93
|
</PageContainer>
|
|
75
94
|
);
|
|
76
95
|
}
|
|
@@ -144,26 +163,22 @@ function AgentIntro(): ReactElement {
|
|
|
144
163
|
}
|
|
145
164
|
|
|
146
165
|
function CapabilityCard({
|
|
147
|
-
|
|
148
|
-
|
|
166
|
+
entry,
|
|
167
|
+
content,
|
|
149
168
|
}: {
|
|
150
|
-
readonly
|
|
151
|
-
readonly
|
|
169
|
+
readonly entry: CatalogEntry;
|
|
170
|
+
readonly content: GuideContent;
|
|
152
171
|
}): ReactElement {
|
|
153
|
-
const
|
|
154
|
-
const
|
|
155
|
-
const Icon = capability.icon;
|
|
172
|
+
const text = entryContent(content, entry);
|
|
173
|
+
const Icon = HOME_ICONS[entry.id] ?? SparklesIcon;
|
|
156
174
|
|
|
157
175
|
return (
|
|
158
|
-
<
|
|
159
|
-
|
|
160
|
-
onClick={onOpen}
|
|
176
|
+
<Link
|
|
177
|
+
to={capabilityPath(entry.id)}
|
|
161
178
|
className='group flex w-full cursor-pointer rounded-xl text-left outline-none focus-visible:ring-3 focus-visible:ring-ring/50'
|
|
162
179
|
>
|
|
163
180
|
<Card className='w-full transition-[translate,box-shadow,background-color] duration-200 ease-out group-hover:-translate-y-0.5 group-hover:bg-hover group-hover:shadow-md motion-reduce:transition-none motion-reduce:group-hover:translate-y-0'>
|
|
164
181
|
<CardHeader className='gap-3'>
|
|
165
|
-
{/* The chip only deepens its tint on hover rather than inverting: swapping a light background and a dark icon
|
|
166
|
-
passes both through the same color halfway, where the icon vanishes for a moment and reads as a flicker. */}
|
|
167
182
|
<span className='flex size-9 items-center justify-center rounded-lg bg-primary/5 text-primary ring-1 ring-transparent transition-[background-color,box-shadow] duration-200 ease-out group-hover:bg-primary/10 group-hover:ring-border motion-reduce:transition-none'>
|
|
168
183
|
<Icon
|
|
169
184
|
className='size-4 transition-transform duration-200 ease-out group-hover:scale-110 motion-reduce:transition-none'
|
|
@@ -171,20 +186,20 @@ function CapabilityCard({
|
|
|
171
186
|
/>
|
|
172
187
|
</span>
|
|
173
188
|
<div className='flex flex-col gap-1'>
|
|
174
|
-
<CardTitle>{
|
|
189
|
+
<CardTitle>{text.title}</CardTitle>
|
|
175
190
|
<CardDescription className='leading-6'>
|
|
176
|
-
{
|
|
191
|
+
{text.summary}
|
|
177
192
|
</CardDescription>
|
|
178
193
|
</div>
|
|
179
194
|
</CardHeader>
|
|
180
195
|
<CardFooter className='mt-auto justify-between text-sm text-muted-foreground transition-colors duration-200 ease-out group-hover:text-foreground'>
|
|
181
|
-
{
|
|
196
|
+
{content.home.open}
|
|
182
197
|
<ArrowRightIcon
|
|
183
198
|
className='size-4 transition-transform duration-200 ease-out group-hover:translate-x-1 motion-reduce:transition-none'
|
|
184
199
|
aria-hidden
|
|
185
200
|
/>
|
|
186
201
|
</CardFooter>
|
|
187
202
|
</Card>
|
|
188
|
-
</
|
|
203
|
+
</Link>
|
|
189
204
|
);
|
|
190
205
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
title: 'Database Explorer',
|
|
3
|
+
description:
|
|
4
|
+
'Browse the application’s database connections, tables and fields.',
|
|
5
|
+
connections: 'Database connection',
|
|
6
|
+
defaultConnection: 'Default',
|
|
7
|
+
tables: 'Tables',
|
|
8
|
+
searchTables: 'Search tables',
|
|
9
|
+
fields: 'Fields',
|
|
10
|
+
searchFields: 'Search fields',
|
|
11
|
+
name: 'Name',
|
|
12
|
+
type: 'Type',
|
|
13
|
+
nullable: 'Nullable',
|
|
14
|
+
primaryKey: 'Primary key',
|
|
15
|
+
target: 'Related table',
|
|
16
|
+
yes: 'Yes',
|
|
17
|
+
no: 'No',
|
|
18
|
+
unknown: 'Not specified',
|
|
19
|
+
emptyConnections: 'No database connections are configured.',
|
|
20
|
+
emptyTables: 'No tables found.',
|
|
21
|
+
emptyFields: 'No fields found.',
|
|
22
|
+
selectTable: 'Select a table to view its fields.',
|
|
23
|
+
loading: 'Loading…',
|
|
24
|
+
failed: 'Unable to load database information.',
|
|
25
|
+
forbidden: 'You do not have permission to browse this database.',
|
|
26
|
+
expired: 'Your session has expired. Sign in again to continue.',
|
|
27
|
+
signIn: 'Sign in',
|
|
28
|
+
retry: 'Retry',
|
|
29
|
+
truncated:
|
|
30
|
+
'Only part of the table list could be loaded. Narrow the database scope to browse the remaining tables.',
|
|
31
|
+
warnings:
|
|
32
|
+
'Some fields could not be fully resolved. The displayed structure may be incomplete.',
|
|
33
|
+
};
|