@nocobase/app-template-default 1.0.0-beta.87 → 1.0.0-beta.89
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/notification-templates.md +5 -0
- package/.agents/rules/shell.md +9 -3
- package/AGENTS.md +4 -2
- package/README.MD +11 -5
- package/client/components/back-button.tsx +37 -12
- package/client/components/body-portal.tsx +25 -0
- package/client/components/brand-loading-mark.tsx +5 -2
- package/client/components/brand-mark-geometry.ts +3 -3
- package/client/components/breadcrumbs.tsx +60 -3
- package/client/components/data-table/data-table.tsx +90 -19
- package/client/components/data-table/pagination.tsx +17 -2
- package/client/components/flow-diagram.tsx +2 -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 +8 -8
- package/client/layouts/components/sidebar-footer.tsx +18 -25
- package/client/lib/navigation-transform.ts +156 -22
- package/client/locales/system/en-US.ts +26 -5
- package/client/locales/system/settings-en-US.ts +17 -0
- package/client/locales/system/settings-zh-CN.ts +15 -0
- package/client/locales/system/zh-CN.ts +26 -5
- package/client/pages/auth/register.tsx +39 -22
- package/client/pages/auth/shared.tsx +1 -3
- package/client/pages/settings/default-access.tsx +1 -1
- package/client/pages/settings/hooks.tsx +3 -68
- package/client/pages/settings/notification-routes.ts +109 -0
- package/client/pages/settings/notification-templates.tsx +26 -0
- package/client/pages/settings/permission-management-page.tsx +15 -90
- package/client/pages/settings/roles.tsx +27 -27
- package/client/pages/settings/routes.ts +107 -101
- package/client/pages/settings/settings-home.tsx +109 -0
- package/client/pages/settings/settings-module-page.tsx +158 -0
- package/client/pages/settings/shell-routes.ts +53 -0
- 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/routes.ts +17 -1
- package/client/routing/app-router.tsx +1 -14
- package/client/routing/client-route.tsx +199 -30
- package/client/routing/navigation-destination.ts +42 -0
- package/client/routing/route-context.tsx +30 -3
- package/client/routing/route-leave-boundary.tsx +261 -0
- package/client/routing/route-navigation.ts +72 -8
- package/client/routing/route-tree.tsx +7 -12
- package/client/runtime.ts +2 -2
- package/client/styles.css +18 -1
- package/config.example.yml +1 -1
- package/package.json +13 -13
- package/server/config/auth.ts +1 -1
- package/server/providers/index.ts +2 -0
- package/server/providers/notification-management.ts +27 -0
- package/tests/client/account-permissions.test.tsx +1 -1
- 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-auth.test.tsx +10 -1
- package/tests/client/client-route-recovery.test.tsx +93 -1
- package/tests/client/client-routes.test.ts +137 -4
- package/tests/client/client-shell.test.tsx +23 -8
- package/tests/client/foundation-integration.test.ts +38 -17
- package/tests/client/layout-i18n.test.tsx +9 -4
- 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 +458 -17
- package/tests/client/primitive-labels.test.tsx +2 -2
- package/tests/client/registration-feedback.test.tsx +143 -0
- 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 +112 -0
- package/tests/client/settings-idle-list.test.tsx +124 -0
- package/tests/client/settings-navigation.test.ts +230 -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 +70 -22
- package/tests/playwright/dialog-footer.test.ts +227 -0
- package/tests/server/notification-templates.test.ts +144 -0
- package/tests/client/permission-management-routes.test.ts +0 -64
|
@@ -15,7 +15,7 @@ yes n | pnpm exec shadcn add card
|
|
|
15
15
|
pnpm exec prettier --write client/components/ui/card.tsx
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
Build your own components by composing these primitives, and put them in `client/components/`. A
|
|
18
|
+
Build your own components by composing these primitives, and put them in `client/components/`. A covering child page, independent detail or edit declares its complete navigation with `breadcrumb` or `usePageBreadcrumb`; the shell renders desktop breadcrumbs and the mobile parent arrow. Declare dynamic/logic parents before loading succeeds and keep them on failure. Use `BackButton` only without that shell navigation, with route/access context for target validation; never show duplicate return entries.
|
|
19
19
|
|
|
20
20
|
**Look components up in the shadcn skill.** The development Skill carries the shadcn/ui skill, unchanged from shadcn 4.21.0, in `.agents/skills/nocobase-app-development/references/frontend/shadcn/`. Read [the shadcn guide](../skills/nocobase-app-development/references/frontend/references/shadcn.md) first: it routes to the skill's rules and states where this application departs from them, among others `pnpm exec shadcn` instead of `npx shadcn@latest`, no `shadcn apply` or `--preset`, and create, edit and detail as `RouteDialog` or `RouteDrawer` child routes rather than a `Sheet`. `pnpm exec shadcn docs <name>` gives one primitive's documentation and examples for the Base UI version. For a whole page, start from the worked example that [the example index](../skills/nocobase-app-development/references/frontend/references/example.md) indexes.
|
|
21
21
|
|
|
@@ -31,6 +31,8 @@ Style with the semantic Tailwind tokens — `bg-background`, `text-foreground`,
|
|
|
31
31
|
|
|
32
32
|
The default brand palette and density live in `client/theme/themes/*.css`; shared hover/cursor and Badge geometry live in `client/styles.css`, outside generated shadcn primitives. For semantic status labels, use `StatusBadge` from `client/components/status-badge.tsx` with `neutral`, `info`, `warning`, or `success`; choose the tone by meaning and keep the visible status text. A badge beside a title or on a dense card is the small one, `size='sm'` on `StatusBadge` or `data-size='sm'` on `Badge`. Reuse these shared components and tokens in new pages so template upgrades can carry the design consistently.
|
|
33
33
|
|
|
34
|
+
Dialog and AlertDialog footer actions use the shared semantic-slot sizing rule in `client/styles.css`, including linked Buttons and RouteOverlay's composed DialogFooter. Keep route links and pending guards; avoid page height/padding patches. The [footer contract](../skills/nocobase-app-development/references/frontend/references/overlay.md#footer-ownership) explains precedence, wrapping and the source upgrade path for existing applications.
|
|
35
|
+
|
|
34
36
|
**Follow the UI guidelines.** [The UI guidelines](../skills/nocobase-app-development/references/frontend/ui-guidelines.md) set the conventions every page follows, among them the page header and the single placement of the primary action, empty states, row actions behind one "…" menu, dates picked from a `Calendar` in a `Popover` rather than a native date input, and medium-width drawers; the frontend workflow says how much of it to read for a change.
|
|
35
37
|
|
|
36
38
|
## Standard process management
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Notification template administration
|
|
2
|
+
|
|
3
|
+
When notification business is used, register the real scenarios alongside their shared sending renderers and deliver the read-only template directory with synthetic preview. Read the installed Notification Skill’s `references/template-directory.md` for registration, safety and coverage requirements. The app-owned `NotificationManagementProvider` projects only `client.notificationTemplates.invitationConfigured` from the same channel selector used by Users sending and the directory. Default's runtime route factory calls `includeNotificationTemplateRoutes` with that bootstrap flag; a configured invitation mailer (including a configured but disabled legacy channel) is real business use. No transport configuration enters the browser. Restart after changing server mailer configuration. The helper reuses `notificationManagement` and preserves existing log children, paths and grants; when integrating a richer settings module container, insert `notificationTemplateRoute` into its template view once. Preserve the host's settings navigation container. The template page permission and API permission are both `page:notification.templates/access`; grant it only to intended administrators. Default does not register unused business settings. Examples registers task assignment/update and Users invitation scenarios.
|
|
4
|
+
|
|
5
|
+
For an existing application, follow the installed upgrade Skill’s version-selected workflow and `references/notification-template-upgrade.md`. Reconcile source, dependency ranges and Skills; retain custom templates, workflows, grants and data. Updating packages alone does not insert or update application pages.
|
package/.agents/rules/shell.md
CHANGED
|
@@ -4,6 +4,8 @@ Read this before changing the layouts, header, sidebar, breadcrumbs, account men
|
|
|
4
4
|
|
|
5
5
|
## Breadcrumbs and layouts
|
|
6
6
|
|
|
7
|
+
Both automatic and explicit parents pass through `client/routing/navigation-destination.ts`, using the shell's existing route/access context. Keep permission loading fail-closed, check every target ancestor, and remove denied parent names. Desktop breadcrumbs and the mobile parent arrow use the same resolved levels; mobile keeps the current title and omits the duplicate brand. Follow the [detail/edit generation contract](../skills/nocobase-app-development/references/frontend/references/navigation.md), including stable parents during record failures, source-tab state and direct-entry defaults. Do not add a second page BackButton under the shell.
|
|
8
|
+
|
|
7
9
|
Layouts own breadcrumb route context and render the current page's trail in the header after the sidebar toggle (`client/components/breadcrumbs.tsx`): the route trail by default, or the trail a page declares with `usePageBreadcrumb` from `@nocobase/app-client` when it names records. Pages do not render breadcrumbs themselves. `AppRouter` selects routes and layouts. See [pages and routes](../skills/nocobase-app-development/references/frontend/references/page.md) for each layout's scope.
|
|
8
10
|
|
|
9
11
|
The route-tree provider remembers each visited ancestor's query while its descendants are open. Default breadcrumbs and back links restore that query, including an empty one, instead of applying detail-only parameters to the list. Keep this provider mounted above page navigation. Explicit breadcrumb/back destinations remain application-owned. See the linked pages-and-routes reference for deep-link and reload fallbacks.
|
|
@@ -12,7 +14,7 @@ The route-tree provider remembers each visited ancestor's query while its descen
|
|
|
12
14
|
|
|
13
15
|
`client/layouts/components/layout-header.tsx` is a presentation container accepting children. The sidebar is shadcn's Sidebar: `components/app-sidebar.tsx` wraps its provider, desktop icon mode and phone sheet, and `components/navigation-menu.tsx` draws the permission-filtered route tree with `SidebarMenu*`: a top-level group with no page of its own is a section, its entries always shown under a `SidebarGroupLabel` heading, while clickable parents and nested groups are controlled `Collapsible` sub-menus, and an item `client/lib/navigation-transform.ts` marks `section` deeper in the tree is a heading over its direct links inside that menu; a count beside an entry, such as unread items, is passed by the App layout in `NavigationMenu`'s `badges`, keyed by route name. Keep `client/components/ui/sidebar.tsx` as the shadcn CLI writes it except for removing its unused origin-wide `sidebar_state` cookie write; persistence belongs to the application-scoped preference. Customize from outside: widths go through the provider's `style` in spacing units, the phone sheet is rendered in `app-sidebar.tsx` with a translated title, and the provider's Ctrl/Cmd+B shortcut is stopped at `document`. The App layout owns menus, branding, permissions and redirects. Desktop icon mode hides top-level section headings and keeps their entries' icons, shows leaf labels as zero-delay tooltips and opens groups in hover and keyboard-focus popovers, preserving filtered navigation, parent-page links, nested groups and the selected group's highlight. Keep this behavior aligned across layouts. Desktop collapse state is shared through `useSidebarPreference` at `nocobase:sidebar:collapsed:app:<basename>` across layouts and tabs of the same application. `createApp(runtime)` provides a stable store using `runtime.basename`, with only empty/root and boundary slashes normalized; paths and case remain intact. Missing new keys default to expanded without migrating the old shared preference; whether the phone sheet is open stays local to each layout.
|
|
14
16
|
|
|
15
|
-
The header carries no Settings or Inbox entry
|
|
17
|
+
The header carries no Settings or Inbox entry, and there is no separate settings layout. A page that configures something is an ordinary route in `client/routes.ts` registered into System settings: the `settings` group (`client/pages/settings/shell-routes.ts`) with its `settingsHome` page and category groups. `client/lib/navigation-transform.ts` folds that group into one sidebar entry opening the home, which stays selected on every page below it; the home (`client/pages/settings/settings-home.tsx`) shows the modules a viewer may open as cards by category, and `SettingsModulePage` turns the pages of a module group into URL-backed tabs. Both read the shell's single page permission check through `RouteTreeProvider`'s `access`. See [System settings navigation](../skills/nocobase-app-development/references/settings-navigation.md). The `/inbox` page declares no navigation; give it an entry where the application wants one.
|
|
16
18
|
|
|
17
19
|
`client/lib/navigation-transform.ts` is the application-owned hook for customizing the permission-filtered navigation tree. The shared sidebar supports static section labels with direct links as well as collapsible groups.
|
|
18
20
|
|
|
@@ -24,13 +26,13 @@ Theme lives in the account menu as a submenu beside Language, rather than a sepa
|
|
|
24
26
|
|
|
25
27
|
The account menu checks Better Auth sign-out results before refreshing the session and shows a localized error toast for API or network failures. Preserve this behavior when upgrading the shell; navigation alone does not revoke a session.
|
|
26
28
|
|
|
27
|
-
The authorization provider clears the permission snapshot before rendering a new session. Route navigation and page guards subscribe to the authorization revision; preserve these checks when customizing the shell so account changes and permission updates take effect without a reload. Pending checks hide protected content
|
|
29
|
+
The authorization provider clears the permission snapshot before rendering a new session. Route navigation and page guards subscribe to the authorization revision; preserve these checks when customizing the shell so account changes and permission updates take effect without a reload. Keep `RouteLeaveBoundary` in `AppLayout`, outside all page Activities including nested routes, so `useSettingsLeave` navigation guards, confirmation UI and `beforeunload` survive permission checks. Clear retained guards on denial and session changes. Restored Activity Effects re-run existing data requests. Keep each resolved route module for the same loader and retry attempt when Activity reconnects; namespace-wrapping loaders may return a new component type on every invocation and discard form state if invoked again. Pending checks hide protected content with React Activity while retaining drafts; failed checks unmount it and deny access. Body portals must detach in an Effect cleanup so they also disappear while the page is hidden; use `BodyPortal` from `#components/body-portal` for application-owned content mounted on `document.body`.
|
|
28
30
|
|
|
29
31
|
The account menu language control in `client/layouts/components/language-switcher.tsx` uses a shadcn submenu with radio items. Render it inside `DropdownMenuContent` to preserve menu keyboard navigation and selection semantics.
|
|
30
32
|
|
|
31
33
|
## Branded loading
|
|
32
34
|
|
|
33
|
-
Use `Loading` for page or panel waits and `Spinner` (the shadcn-compatible adapter to `BrandSpinner`) for inline pending actions, uploads, and refresh indicators. Both share the stable NocoBase mark and orbit animation; do not introduce independent Lucide spinners for application loading. Keep skeletons for content placeholders and progress bars for determinate progress. `index.html` has the matching pre-JavaScript mark and must stay visually consistent. All animations respect reduced motion.
|
|
35
|
+
Follow [pending feedback](../skills/nocobase-app-development/references/pending-feedback.md) for shared 300ms display and 10s long-wait guidance. Use `PendingButton` with real `submitting` for native form actions; preserve the shared submission lock. Use `Loading` for page or panel waits and `Spinner` (the shadcn-compatible adapter to `BrandSpinner`) for inline pending actions, uploads, and refresh indicators. Both share the stable NocoBase mark and orbit animation; do not introduce independent Lucide spinners for application loading. Keep skeletons for content placeholders and progress bars for determinate progress. `index.html` has the matching pre-JavaScript mark and must stay visually consistent. All animations respect reduced motion.
|
|
34
36
|
|
|
35
37
|
The shared list of shell behaviors to keep when customizing is in [customizing the shell](../skills/nocobase-app-development/references/frontend/references/shell.md).
|
|
36
38
|
|
|
@@ -39,3 +41,7 @@ The signed-in region waits in `client/routing/session-preparation.tsx` before mo
|
|
|
39
41
|
## List context
|
|
40
42
|
|
|
41
43
|
Keep the identity-keyed `SessionPreparation` boundary: mounted private list conditions and ancestor query memory must be discarded when the signed-in user changes. Public list state uses `useTableUrlState`; tabbed datasets use `useListTabs`, with complete owned parameter keys and permission-filtered tabs. Follow the synchronized [list navigation rules](../skills/nocobase-app-development/references/frontend/references/child-routes.md#list-tab-breadcrumbs-and-private-conditions). Default back links use an empty ancestor query when no source visit exists; explicit return destinations remain application-owned.
|
|
44
|
+
|
|
45
|
+
## Page data freshness
|
|
46
|
+
|
|
47
|
+
Follow the synchronized [page data freshness rules](../skills/nocobase-app-development/references/frontend/references/page-freshness.md): ordinary pages have no persistent Refresh action. Preserve contextual read retries, write invalidation and live progress updates. Dependency upgrades do not rewrite application-owned pages or installed Registry source; reconcile them through the template upgrade workflow.
|
package/AGENTS.md
CHANGED
|
@@ -57,13 +57,13 @@ A feature with a page and an API touches five places: a migration for the table,
|
|
|
57
57
|
|
|
58
58
|
## System settings are part of feature delivery
|
|
59
59
|
|
|
60
|
-
A bare create-app shows Home to signed-in users and System settings
|
|
60
|
+
A bare create-app shows Home to signed-in users and, only to unrestricted administrators, one System settings entry whose home page holds the App foundation card. Every administration surface joins that structure — one sidebar entry, a home of cards by category, tabs inside a module — as [System settings navigation](.agents/skills/nocobase-app-development/references/settings-navigation.md) describes; never add another sidebar entry for administration. As the application develops business capabilities, inspect its current state on every iteration and automatically register the prepared Users and Permission management contribution from `client/pages/settings/routes.ts` once in `client/routes.ts`; preserve it and reconcile missing management surfaces on every iteration, without a first-build flag or one-time setup stage. Whenever an application adds lifecycles, workflows, approvals, notifications, files or other configurable capabilities, automatically include their real administration surfaces without requiring a second user request. Follow [system settings delivery](.agents/skills/nocobase-app-development/references/system-settings.md); in particular, every used process includes a definitions directory, standard diagram, supported parameters and authorized execution history. Do not add demo plugins or empty settings for unused capabilities. The template preinstalls `#components/lifecycle-diagram` (React Flow + ELK) and `client/extensions/nocobase-workflow-management/`: use them by default, preserve their visuals unless the user explicitly requests another style, and connect authorized parameter bindings through `LifecycleDiagram.configuration` to the real editor. These are UI Registry sources; an npm package lookup is not a component availability check.
|
|
61
61
|
|
|
62
62
|
## Rules that apply everywhere
|
|
63
63
|
|
|
64
64
|
### Pages and routes
|
|
65
65
|
|
|
66
|
-
Declare every page in `client/routes.ts` with `defineAppRoutes()` and keep its component behind a lazy `componentLoader` whose module default-exports it. Never write the deployment base path such as `/main` into a route — the runtime restores it. `auth` controls browser navigation only and is not server security. Declare `navigation` on the route to put it in a menu, and always declare `authz` on the first page of every path; the page id identifies stored page grants, so changing it requires migrating the grants that reference it. Read [pages and routes](.agents/skills/nocobase-app-development/references/frontend/references/page.md), and [overlays](.agents/skills/nocobase-app-development/references/frontend/references/overlay.md) before implementing dialogs, drawers or close guards.
|
|
66
|
+
Declare every page in `client/routes.ts` with `defineAppRoutes()` and keep its component behind a lazy `componentLoader` whose module default-exports it. Never write the deployment base path such as `/main` into a route — the runtime restores it. `auth` controls browser navigation only and is not server security. Declare `navigation` on the route to put it in a menu, and always declare `authz` on the first page of every path; the page id identifies stored page grants, so changing it requires migrating the grants that reference it. Every new independent detail/edit page follows the [navigation pattern](.agents/skills/nocobase-app-development/references/frontend/references/navigation.md), including a parent during loading/errors and mobile return; omit duplicate page back links under the shell. Read [pages and routes](.agents/skills/nocobase-app-development/references/frontend/references/page.md), and [overlays](.agents/skills/nocobase-app-development/references/frontend/references/overlay.md) before implementing dialogs, drawers or close guards.
|
|
67
67
|
|
|
68
68
|
Every route listed as a page permission needs a localized display name: use its existing navigation/breadcrumb title, or add its application route name to `client/pages/settings/page-permission-titles.ts` for an overlay or other page with no title metadata. Do not add breadcrumbs/menus just to label an overlay. Run `tests/client/page-permission-titles.test.ts`; it requires non-empty English/Chinese labels and rejects missing or stale mappings. Never rename route/permission ids or change `authz` to fix display text.
|
|
69
69
|
|
|
@@ -138,3 +138,5 @@ Standard Repository writes require an operation key through the registered submi
|
|
|
138
138
|
Assign application business roles to explicitly identified users through the prepared User management role-assignment UI. Never seed a business permission set onto `authenticated:*`, broaden the inherited platform `member` role, or write startup reconciliation that grants access to everyone merely to make a feature usable. Unchecked actions in one role do not revoke grants inherited from another. If the user explicitly requests a global default role, first deliver a permission-protected administration control that shows its recipients and allows changing or removing the assignment; show inherited roles and their source in user access details. Without that UI, use explicit user assignments and explain the limitation. Keep page access independent from business operations.
|
|
139
139
|
|
|
140
140
|
Run `pnpm exec vitest run tests/server/authorization-assignment-safety.test.ts` after permission initialization changes and before delivery. It boots the real application on an isolated database and inspects initialized wildcard assignments and the inherited member role. Keep this test in generated applications. An intentional global default requires explicit user authorization, a working administration surface and corresponding acceptance tests before deliberately updating the expected baseline; do not delete, skip or loosen the guard to make tests pass. Test an ordinary user with only the configured role: inspect all effective grant sources and verify both hidden buttons and rejected direct API calls.
|
|
141
|
+
|
|
142
|
+
For notification business, follow [notification template delivery](.agents/rules/notification-templates.md), including source-owned scenario registration and synthetic-only preview.
|
package/README.MD
CHANGED
|
@@ -139,11 +139,11 @@ The three files named `runtime.ts`, `plugins.ts`, and `routes.ts` are compositio
|
|
|
139
139
|
|
|
140
140
|
### Everyday development
|
|
141
141
|
|
|
142
|
-
| Script | What it does
|
|
143
|
-
| ------------------ |
|
|
144
|
-
| `pnpm dev` | Start the API server and client with hot reload
|
|
145
|
-
| `pnpm build` | Build the client and server into `dist/`
|
|
146
|
-
| `pnpm start` | Run the built server; requires `pnpm build` first
|
|
142
|
+
| Script | What it does |
|
|
143
|
+
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------- |
|
|
144
|
+
| `pnpm dev` | Start the API server and client with hot reload |
|
|
145
|
+
| `pnpm build` | Build the client and server into `dist/` |
|
|
146
|
+
| `pnpm start` | Run the built server; requires `pnpm build` first |
|
|
147
147
|
| `pnpm build --tar` | Build, then pack `dist/`, `config.example.yml` and the `Dockerfile` with its `.dockerignore` into `storage/exports/dist.tar.gz` |
|
|
148
148
|
|
|
149
149
|
### Database
|
|
@@ -453,3 +453,9 @@ Set `NOCOBASE_STRICT_STARTUP=true` when running `pnpm dev` or `pnpm start` in au
|
|
|
453
453
|
## Internal workspace packages
|
|
454
454
|
|
|
455
455
|
The application can also be the root of a pnpm workspace, with internal packages under `packages/*` and the existing `client/`, `server/` and `database/` directories unchanged. New projects receive this workspace pattern automatically. Internal packages use `workspace:*` dependencies and may remain private. Run installation, development and deployment builds from the application root; `pnpm check` includes the internal packages' own checks. See [internal packages](.agents/rules/workspace.md) for configuration and development conventions.
|
|
456
|
+
|
|
457
|
+
### Registration feedback
|
|
458
|
+
|
|
459
|
+
Password registration signs in new accounts by default (`auth.emailAndPassword.autoSignIn: true` in `server/config/auth.ts` and `config.example.yml`). An existing email returns an explicit error, exposing whether that email is registered. For enumeration protection, explicitly set `autoSignIn: false` and configure email verification with a sender or `onExistingUserSignUp` to notify the email owner. Existing explicit overrides remain effective; no configuration migration is performed. The authentication plugin warns at startup about registration with auto sign-in disabled and neither verification nor an existing-user notification.
|
|
460
|
+
|
|
461
|
+
The registration page treats `usePasswordRegistration().isSuccess` as request acceptance only. A refreshed session navigates signed-in users into the application. Visitors who remain signed out see neutral sign-in and recovery guidance, without an account-creation or email-delivery promise. Applications customizing this page must keep that distinction.
|
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
import { useTranslation } from '@nocobase/i18n/client';
|
|
2
2
|
import { ArrowLeftIcon } from 'lucide-react';
|
|
3
3
|
import type { ReactElement, ReactNode } from 'react';
|
|
4
|
-
import { Link, type To, useResolvedPath } from 'react-router';
|
|
4
|
+
import { Link, type To, parsePath, useResolvedPath } from 'react-router';
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { navigationDestination } from '../routing/navigation-destination.js';
|
|
7
|
+
import {
|
|
8
|
+
useRouteTree,
|
|
9
|
+
useRouteAccess,
|
|
10
|
+
useRouteSearches,
|
|
11
|
+
} from '../routing/route-context.js';
|
|
7
12
|
|
|
8
13
|
import { cn } from 'cn';
|
|
9
14
|
|
|
10
15
|
export interface BackButtonProps {
|
|
11
16
|
/**
|
|
12
17
|
* Where it leads. Defaults to the parent route with its last visited query; a directly opened detail falls back
|
|
13
|
-
* to the default parent list. An explicit destination
|
|
18
|
+
* to the default parent list. An explicit destination must match an authorized application page.
|
|
14
19
|
*/
|
|
15
20
|
readonly to?: To;
|
|
16
21
|
/** The label, "Back" by default. */
|
|
@@ -31,11 +36,36 @@ export function BackButton({
|
|
|
31
36
|
children,
|
|
32
37
|
className,
|
|
33
38
|
to,
|
|
34
|
-
}: BackButtonProps = {}): ReactElement {
|
|
39
|
+
}: BackButtonProps = {}): ReactElement | null {
|
|
35
40
|
const { t } = useTranslation();
|
|
36
41
|
const parent = useResolvedPath('..');
|
|
42
|
+
const resolved = useResolvedPath(to ?? '..');
|
|
43
|
+
const routes = useRouteTree();
|
|
44
|
+
const access = useRouteAccess();
|
|
37
45
|
const searches = useRouteSearches();
|
|
38
46
|
|
|
47
|
+
const fallback = {
|
|
48
|
+
...parent,
|
|
49
|
+
search: searches.get(parent.pathname.replace(/\/+$/u, '') || '/') ?? '',
|
|
50
|
+
};
|
|
51
|
+
const rawPath =
|
|
52
|
+
typeof to === 'string' ? parsePath(to).pathname : to?.pathname;
|
|
53
|
+
const unsafe =
|
|
54
|
+
rawPath &&
|
|
55
|
+
(/^(?:[a-z][a-z\d+.-]*:|\/\/)/iu.test(rawPath) ||
|
|
56
|
+
rawPath.includes('\\') ||
|
|
57
|
+
[...rawPath].some((character) => character.charCodeAt(0) <= 32));
|
|
58
|
+
const primary = unsafe
|
|
59
|
+
? undefined
|
|
60
|
+
: navigationDestination(
|
|
61
|
+
to && rawPath?.startsWith('/') ? to : to ? resolved : fallback,
|
|
62
|
+
routes,
|
|
63
|
+
access,
|
|
64
|
+
);
|
|
65
|
+
const destination =
|
|
66
|
+
primary ?? navigationDestination(fallback, routes, access);
|
|
67
|
+
if (!destination) return null;
|
|
68
|
+
|
|
39
69
|
return (
|
|
40
70
|
<Link
|
|
41
71
|
replace
|
|
@@ -43,16 +73,11 @@ export function BackButton({
|
|
|
43
73
|
'inline-flex w-fit items-center gap-1 text-sm text-muted-foreground transition-colors hover:text-foreground',
|
|
44
74
|
className,
|
|
45
75
|
)}
|
|
46
|
-
to={
|
|
47
|
-
to ?? {
|
|
48
|
-
pathname: '..',
|
|
49
|
-
search:
|
|
50
|
-
searches.get(parent.pathname.replace(/\/+$/u, '') || '/') ?? '',
|
|
51
|
-
}
|
|
52
|
-
}
|
|
76
|
+
to={destination}
|
|
53
77
|
>
|
|
54
78
|
<ArrowLeftIcon className='size-4' />
|
|
55
|
-
{
|
|
79
|
+
{(primary ? children : undefined) ??
|
|
80
|
+
t('navigation.back', { defaultValue: 'Back' })}
|
|
56
81
|
</Link>
|
|
57
82
|
);
|
|
58
83
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useLayoutEffect,
|
|
3
|
+
useState,
|
|
4
|
+
type ReactElement,
|
|
5
|
+
type ReactNode,
|
|
6
|
+
} from 'react';
|
|
7
|
+
import { createPortal } from 'react-dom';
|
|
8
|
+
|
|
9
|
+
export interface BodyPortalProps {
|
|
10
|
+
readonly children: ReactNode;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Detach body content when its page is hidden by Activity, retaining its React state for the next permission result. */
|
|
14
|
+
export function BodyPortal({ children }: BodyPortalProps): ReactElement {
|
|
15
|
+
const [container] = useState(() => {
|
|
16
|
+
const element = document.createElement('div');
|
|
17
|
+
element.style.display = 'contents';
|
|
18
|
+
return element;
|
|
19
|
+
});
|
|
20
|
+
useLayoutEffect(() => {
|
|
21
|
+
document.body.append(container);
|
|
22
|
+
return () => container.remove();
|
|
23
|
+
}, [container]);
|
|
24
|
+
return createPortal(children, container);
|
|
25
|
+
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import type { ComponentPropsWithoutRef, ReactElement } from 'react';
|
|
2
2
|
import { cn } from 'cn';
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
BRAND_MARK_PATH,
|
|
6
|
+
BRAND_MARK_TRANSLATE,
|
|
7
|
+
} from './brand-mark-geometry.js';
|
|
5
8
|
|
|
6
9
|
/** Keep the brand recognizable; the orbit, rather than the bricks, conveys progress. */
|
|
7
10
|
export function BrandLoadingMark({
|
|
@@ -25,7 +28,7 @@ export function BrandLoadingMark({
|
|
|
25
28
|
strokeWidth='2'
|
|
26
29
|
opacity='.12'
|
|
27
30
|
/>
|
|
28
|
-
<g className='
|
|
31
|
+
<g className='origin-center motion-safe:animate-spin'>
|
|
29
32
|
<path
|
|
30
33
|
d='M32 4a28 28 0 0 1 28 28'
|
|
31
34
|
stroke='currentColor'
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Official Logomark vector from the supplied logos/Transparent/Vectors asset.
|
|
2
2
|
* Keep geometry unchanged; color, size and decorative animation belong to consumers.
|
|
3
3
|
*/
|
|
4
|
-
export const BRAND_MARK_VIEW_BOX = '0 0 100 87.19720458984375';
|
|
5
|
-
export const BRAND_MARK_TRANSLATE = 'translate(0 -6.401886463165283)';
|
|
6
|
-
export const BRAND_MARK_PATH =
|
|
4
|
+
export const BRAND_MARK_VIEW_BOX: string = '0 0 100 87.19720458984375';
|
|
5
|
+
export const BRAND_MARK_TRANSLATE: string = 'translate(0 -6.401886463165283)';
|
|
6
|
+
export const BRAND_MARK_PATH: string =
|
|
7
7
|
'M99.876 55.369c-.025-.045-.076-.072-.109-.111-.066-.078-.125-.168-.217-.221l-.014-.008-.001-.002h-.001L80.368 43.648v-24.67a.92.92 0 0 0-.124-.459c-.026-.045-.075-.072-.108-.111-.066-.08-.126-.168-.218-.221l-.014-.008-.001-.002h-.002L60.286 6.531a.92.92 0 0 0-.94 0L20.099 29.834h-.002l-.001.002-.014.006c-.092.055-.151.143-.218.221-.033.041-.082.068-.108.113a.907.907 0 0 0-.124.457v1.361L.466 43.373H.465l-.001.002c-.098.055-.162.143-.23.227-.034.041-.084.068-.109.113a.898.898 0 0 0-.125.457v25.195c0 .324.171.625.45.791l39.264 23.311c.002.002.006 0 .008.002l.007.008a.924.924 0 0 0 .911 0c.003-.002.004-.006.008-.008.001-.002.005 0 .007-.002l19.161-11.377 19.162 11.377c.002.002.006 0 .008.002.004.002.004.006.007.008a.924.924 0 0 0 .911 0c.003-.002.004-.006.008-.008.002-.002.005 0 .007-.002L99.55 81.812a.916.916 0 0 0 .45-.791V55.828a.92.92 0 0 0-.124-.459zM41.104 65.867V42.812l37.424-22.219v23.055L41.104 65.867zM59.815 8.391l17.83 10.588L59.82 29.561 40.185 41.219l-17.83-10.586 37.46-22.242zM39.264 42.812V65.866L21.472 55.305V32.248l17.792 10.564z';
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { usePageBreadcrumbLevels } from '@nocobase/app-client';
|
|
2
2
|
import type { PageBreadcrumbLevel } from '@nocobase/app-client';
|
|
3
3
|
import { useTranslation } from '@nocobase/i18n/client';
|
|
4
|
+
import { ArrowLeftIcon } from 'lucide-react';
|
|
4
5
|
import { Fragment, type ReactElement } from 'react';
|
|
5
6
|
import { Link } from 'react-router';
|
|
6
7
|
|
|
@@ -19,13 +20,17 @@ import {
|
|
|
19
20
|
DropdownMenuItem,
|
|
20
21
|
DropdownMenuTrigger,
|
|
21
22
|
} from '#components/ui/dropdown-menu';
|
|
23
|
+
import { settingsHomeRoute } from '#lib/navigation-transform';
|
|
22
24
|
import { cn } from 'cn';
|
|
23
25
|
|
|
24
26
|
import {
|
|
27
|
+
useRouteTree,
|
|
28
|
+
useRouteAccess,
|
|
25
29
|
useRouteTrail,
|
|
26
30
|
useRouteSearches,
|
|
27
31
|
type RouteTrailEntry,
|
|
28
32
|
} from '../routing/route-context.js';
|
|
33
|
+
import { navigationDestination } from '../routing/navigation-destination.js';
|
|
29
34
|
import { routeKey } from '../routing/route-navigation.js';
|
|
30
35
|
|
|
31
36
|
export interface BreadcrumbsProps {
|
|
@@ -44,7 +49,7 @@ export interface BreadcrumbsProps {
|
|
|
44
49
|
* `usePageBreadcrumb` from `@nocobase/app-client`. Earlier levels link where a page sits behind them; the last is the
|
|
45
50
|
* current page.
|
|
46
51
|
*
|
|
47
|
-
* On a phone
|
|
52
|
+
* On a phone the current title and a link to the nearest authorized parent show; on a medium screen a trail of three or more levels keeps its
|
|
48
53
|
* first and last and folds the middle into a menu.
|
|
49
54
|
*/
|
|
50
55
|
export function Breadcrumbs({
|
|
@@ -52,14 +57,34 @@ export function Breadcrumbs({
|
|
|
52
57
|
denied,
|
|
53
58
|
}: BreadcrumbsProps = {}): ReactElement | null {
|
|
54
59
|
const trail = useRouteTrail();
|
|
60
|
+
const routes = useRouteTree();
|
|
61
|
+
const contextAccess = useRouteAccess();
|
|
62
|
+
const access = denied
|
|
63
|
+
? {
|
|
64
|
+
...contextAccess,
|
|
65
|
+
denied: new Set([...contextAccess.denied, ...denied]),
|
|
66
|
+
}
|
|
67
|
+
: contextAccess;
|
|
55
68
|
const searches = useRouteSearches();
|
|
56
69
|
const declared = usePageBreadcrumbLevels();
|
|
57
70
|
const { t } = useTranslation();
|
|
58
|
-
const
|
|
71
|
+
const candidates =
|
|
59
72
|
declared ??
|
|
60
73
|
trail.flatMap((entry): PageBreadcrumbLevel[] => {
|
|
61
74
|
const title = routeLevelTitle(entry);
|
|
62
75
|
if (title === undefined || denied?.has(routeKey(entry.route))) return [];
|
|
76
|
+
// The settings group links to its home page, the way back from any settings module.
|
|
77
|
+
const home = settingsHomeRoute(entry.route);
|
|
78
|
+
if (home)
|
|
79
|
+
return [
|
|
80
|
+
{
|
|
81
|
+
label: t(title, {
|
|
82
|
+
ns: entry.route.packageName,
|
|
83
|
+
defaultValue: title,
|
|
84
|
+
}),
|
|
85
|
+
to: { pathname: home.path, search: searches.get(home.path) ?? '' },
|
|
86
|
+
},
|
|
87
|
+
];
|
|
63
88
|
return [
|
|
64
89
|
{
|
|
65
90
|
label: t(title, {
|
|
@@ -80,6 +105,19 @@ export function Breadcrumbs({
|
|
|
80
105
|
];
|
|
81
106
|
});
|
|
82
107
|
|
|
108
|
+
const trailLevels = candidates.flatMap(
|
|
109
|
+
(level, index): PageBreadcrumbLevel[] => {
|
|
110
|
+
if (!level.to) return [level];
|
|
111
|
+
const to = navigationDestination(level.to, routes, access);
|
|
112
|
+
// Keep the current title during permission checks, but never expose an inaccessible ancestor's name.
|
|
113
|
+
return to
|
|
114
|
+
? [{ ...level, to }]
|
|
115
|
+
: index === candidates.length - 1
|
|
116
|
+
? [{ label: level.label }]
|
|
117
|
+
: [];
|
|
118
|
+
},
|
|
119
|
+
);
|
|
120
|
+
|
|
83
121
|
if (trailLevels.length === 0) return null;
|
|
84
122
|
// A level is told apart by its depth and label; two levels may share either alone.
|
|
85
123
|
const levels = trailLevels.map((level, depth) => ({
|
|
@@ -88,6 +126,7 @@ export function Breadcrumbs({
|
|
|
88
126
|
}));
|
|
89
127
|
const last = levels.length - 1;
|
|
90
128
|
const folded = levels.slice(1, last);
|
|
129
|
+
const parent = levels.slice(0, last).findLast((level) => level.to);
|
|
91
130
|
|
|
92
131
|
return (
|
|
93
132
|
<Breadcrumb
|
|
@@ -95,6 +134,21 @@ export function Breadcrumbs({
|
|
|
95
134
|
className={cn('min-w-0', className)}
|
|
96
135
|
>
|
|
97
136
|
<BreadcrumbList className='flex-nowrap'>
|
|
137
|
+
{parent ? (
|
|
138
|
+
<BreadcrumbItem className='shrink-0 md:hidden'>
|
|
139
|
+
<BreadcrumbLink
|
|
140
|
+
aria-label={t('navigation.backTo', {
|
|
141
|
+
label: parent.label,
|
|
142
|
+
defaultValue: `Back to ${parent.label}`,
|
|
143
|
+
})}
|
|
144
|
+
title={parent.label}
|
|
145
|
+
className='inline-flex size-9 items-center justify-center rounded-sm focus-visible:outline-2 focus-visible:outline-ring'
|
|
146
|
+
render={<Link to={parent.to!} replace />}
|
|
147
|
+
>
|
|
148
|
+
<ArrowLeftIcon aria-hidden='true' className='size-4' />
|
|
149
|
+
</BreadcrumbLink>
|
|
150
|
+
</BreadcrumbItem>
|
|
151
|
+
) : null}
|
|
98
152
|
{levels.map((level, index) => (
|
|
99
153
|
<Fragment key={level.key}>
|
|
100
154
|
{index === 1 && folded.length > 0 ? (
|
|
@@ -141,7 +195,10 @@ export function Breadcrumbs({
|
|
|
141
195
|
)}
|
|
142
196
|
>
|
|
143
197
|
{index === last ? (
|
|
144
|
-
<BreadcrumbPage
|
|
198
|
+
<BreadcrumbPage
|
|
199
|
+
title={level.label}
|
|
200
|
+
className='truncate font-medium'
|
|
201
|
+
>
|
|
145
202
|
{level.label}
|
|
146
203
|
</BreadcrumbPage>
|
|
147
204
|
) : level.to ? (
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ListState } from '@nocobase/app-client';
|
|
2
2
|
import { ListFeedback, ListFeedbackActions } from '#components/list-feedback';
|
|
3
|
+
import { RefreshIndicator } from '#components/loading';
|
|
3
4
|
import {
|
|
4
5
|
type ColumnDef,
|
|
5
6
|
type PaginationState,
|
|
@@ -17,7 +18,14 @@ import {
|
|
|
17
18
|
getSortedRowModel,
|
|
18
19
|
useReactTable,
|
|
19
20
|
} from '@tanstack/react-table';
|
|
20
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
type ReactElement,
|
|
23
|
+
type ReactNode,
|
|
24
|
+
useEffect,
|
|
25
|
+
useMemo,
|
|
26
|
+
useRef,
|
|
27
|
+
useState,
|
|
28
|
+
} from 'react';
|
|
21
29
|
|
|
22
30
|
import { DataTablePagination } from './pagination.js';
|
|
23
31
|
import {
|
|
@@ -41,7 +49,18 @@ export interface DataTableProps<TData, TValue = unknown> {
|
|
|
41
49
|
readonly sortingState?: SortingState;
|
|
42
50
|
readonly onSortingChange?: OnChangeFn<SortingState>;
|
|
43
51
|
readonly showSelectedCount?: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* The current request is in flight. With rows of the current query and permission scope this is a background
|
|
54
|
+
* refresh: rows stay and a delayed refresh indicator appears. Without them the body shows the loading state.
|
|
55
|
+
* Explicit false pauses loading feedback for an unresolved, inactive query; omitting it uses listState.
|
|
56
|
+
*/
|
|
44
57
|
readonly loading?: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* The current query's stable identity, such as its request path. When it
|
|
60
|
+
* changes the loading and refresh feedback restart their display delay;
|
|
61
|
+
* the table, its rows' geometry and its controls stay mounted.
|
|
62
|
+
*/
|
|
63
|
+
readonly loadingIdentity?: unknown;
|
|
45
64
|
readonly listState?: ListState;
|
|
46
65
|
readonly onRetry?: () => void;
|
|
47
66
|
readonly onClearFilters?: () => void;
|
|
@@ -95,7 +114,8 @@ export function DataTable<TData, TValue = unknown>({
|
|
|
95
114
|
sortingState,
|
|
96
115
|
onSortingChange,
|
|
97
116
|
showSelectedCount = true,
|
|
98
|
-
loading
|
|
117
|
+
loading,
|
|
118
|
+
loadingIdentity,
|
|
99
119
|
listState,
|
|
100
120
|
onRetry,
|
|
101
121
|
onClearFilters,
|
|
@@ -176,6 +196,37 @@ export function DataTable<TData, TValue = unknown>({
|
|
|
176
196
|
rows.length > 0 && (state === 'ready' || state === 'refresh-failed');
|
|
177
197
|
const showFeedback = !showRows || state === 'refresh-failed';
|
|
178
198
|
const feedbackColumnCount = Math.max(1, table.getVisibleLeafColumns().length);
|
|
199
|
+
const showPagination =
|
|
200
|
+
pagination &&
|
|
201
|
+
(state === 'ready' || (state === 'refresh-failed' && rows.length > 0));
|
|
202
|
+
// A new query hides the previous rows at once; keep their body and footer
|
|
203
|
+
// geometry while it loads so the page below does not jump.
|
|
204
|
+
const bodyRef = useRef<HTMLTableSectionElement>(null);
|
|
205
|
+
const footerRef = useRef<HTMLDivElement>(null);
|
|
206
|
+
const [reserved, setReserved] = useState<{
|
|
207
|
+
readonly body: number;
|
|
208
|
+
readonly footer: number;
|
|
209
|
+
}>();
|
|
210
|
+
if (!showRows && state !== 'loading' && reserved !== undefined)
|
|
211
|
+
setReserved(undefined);
|
|
212
|
+
useEffect(() => {
|
|
213
|
+
const body = bodyRef.current;
|
|
214
|
+
if (!showRows || !body || typeof ResizeObserver === 'undefined') return;
|
|
215
|
+
const footer = footerRef.current;
|
|
216
|
+
const observer = new ResizeObserver(() => {
|
|
217
|
+
const next = {
|
|
218
|
+
body: body.offsetHeight,
|
|
219
|
+
footer: footer?.offsetHeight ?? 0,
|
|
220
|
+
};
|
|
221
|
+
setReserved((old) =>
|
|
222
|
+
old?.body === next.body && old.footer === next.footer ? old : next,
|
|
223
|
+
);
|
|
224
|
+
});
|
|
225
|
+
observer.observe(body);
|
|
226
|
+
if (footer) observer.observe(footer);
|
|
227
|
+
return () => observer.disconnect();
|
|
228
|
+
}, [showRows, showPagination]);
|
|
229
|
+
const reserving = state === 'loading' && reserved !== undefined;
|
|
179
230
|
|
|
180
231
|
return (
|
|
181
232
|
<div className={cn('flex flex-col gap-4', className)}>
|
|
@@ -194,8 +245,14 @@ export function DataTable<TData, TValue = unknown>({
|
|
|
194
245
|
/>
|
|
195
246
|
</div>
|
|
196
247
|
) : null}
|
|
197
|
-
<div className='overflow-hidden rounded-lg border'>
|
|
198
|
-
|
|
248
|
+
<div className='relative overflow-hidden rounded-lg border'>
|
|
249
|
+
{/* A retry after a refresh failure is announced by its own toolbar button. */}
|
|
250
|
+
<RefreshIndicator
|
|
251
|
+
pending={Boolean(loading && showRows && state !== 'refresh-failed')}
|
|
252
|
+
identity={loadingIdentity}
|
|
253
|
+
/>
|
|
254
|
+
{/* Only retained rows are busy; a loading body owns its own busy region and status. */}
|
|
255
|
+
<Table aria-busy={loading && showRows}>
|
|
199
256
|
<TableHeader>
|
|
200
257
|
{table.getHeaderGroups().map((headerGroup) => (
|
|
201
258
|
<TableRow key={headerGroup.id}>
|
|
@@ -224,15 +281,27 @@ export function DataTable<TData, TValue = unknown>({
|
|
|
224
281
|
</TableRow>
|
|
225
282
|
))}
|
|
226
283
|
</TableHeader>
|
|
227
|
-
<TableBody>
|
|
284
|
+
<TableBody ref={bodyRef}>
|
|
228
285
|
{showFeedback ? (
|
|
229
286
|
<TableRow>
|
|
230
|
-
<TableCell
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
287
|
+
<TableCell
|
|
288
|
+
colSpan={feedbackColumnCount}
|
|
289
|
+
className='p-0'
|
|
290
|
+
style={
|
|
291
|
+
reserving && reserved.body > 0
|
|
292
|
+
? { height: reserved.body }
|
|
293
|
+
: undefined
|
|
294
|
+
}
|
|
295
|
+
>
|
|
296
|
+
{/* Readiness may stay unresolved while the parent query is paused. */}
|
|
297
|
+
{state !== 'loading' || loading !== false ? (
|
|
298
|
+
<ListFeedback
|
|
299
|
+
state={state}
|
|
300
|
+
identity={loadingIdentity}
|
|
301
|
+
empty={emptyMessage}
|
|
302
|
+
emptyAction={emptyAction}
|
|
303
|
+
/>
|
|
304
|
+
) : null}
|
|
236
305
|
</TableCell>
|
|
237
306
|
</TableRow>
|
|
238
307
|
) : null}
|
|
@@ -258,14 +327,16 @@ export function DataTable<TData, TValue = unknown>({
|
|
|
258
327
|
</TableBody>
|
|
259
328
|
</Table>
|
|
260
329
|
</div>
|
|
261
|
-
{pagination &&
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
330
|
+
{showPagination || (pagination && reserving) ? (
|
|
331
|
+
<div ref={footerRef} data-slot='data-table-footer'>
|
|
332
|
+
<DataTablePagination
|
|
333
|
+
table={table}
|
|
334
|
+
pageSizeOptions={pageSizeOptions}
|
|
335
|
+
showSelectedCount={showSelectedCount}
|
|
336
|
+
disabled={loading || !showPagination}
|
|
337
|
+
countPending={!showPagination}
|
|
338
|
+
/>
|
|
339
|
+
</div>
|
|
269
340
|
) : null}
|
|
270
341
|
</div>
|
|
271
342
|
);
|
|
@@ -26,6 +26,8 @@ export interface DataTablePaginationProps<
|
|
|
26
26
|
/** Hide the "n of m selected" summary for tables without row selection. */
|
|
27
27
|
readonly showSelectedCount?: boolean;
|
|
28
28
|
readonly disabled?: boolean;
|
|
29
|
+
/** The current query has no total yet: keep the controls' place but show no previous count. */
|
|
30
|
+
readonly countPending?: boolean;
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
const DEFAULT_PAGE_SIZE_OPTIONS: readonly number[] = [10, 20, 30, 40, 50];
|
|
@@ -40,6 +42,7 @@ export function DataTablePagination<TData>({
|
|
|
40
42
|
pageSizeOptions = DEFAULT_PAGE_SIZE_OPTIONS,
|
|
41
43
|
showSelectedCount = true,
|
|
42
44
|
disabled = false,
|
|
45
|
+
countPending = false,
|
|
43
46
|
className,
|
|
44
47
|
...props
|
|
45
48
|
}: DataTablePaginationProps<TData>): ReactElement {
|
|
@@ -60,7 +63,13 @@ export function DataTablePagination<TData>({
|
|
|
60
63
|
)}
|
|
61
64
|
{...props}
|
|
62
65
|
>
|
|
63
|
-
<div
|
|
66
|
+
<div
|
|
67
|
+
className={cn(
|
|
68
|
+
'flex-1 text-sm text-muted-foreground',
|
|
69
|
+
countPending && 'invisible',
|
|
70
|
+
)}
|
|
71
|
+
aria-hidden={countPending || undefined}
|
|
72
|
+
>
|
|
64
73
|
{showSelectedCount
|
|
65
74
|
? t('dataTable.selectedCount', {
|
|
66
75
|
defaultValue: '{{selected}} of {{total}} row(s) selected.',
|
|
@@ -102,7 +111,13 @@ export function DataTablePagination<TData>({
|
|
|
102
111
|
</SelectContent>
|
|
103
112
|
</Select>
|
|
104
113
|
</div>
|
|
105
|
-
<div
|
|
114
|
+
<div
|
|
115
|
+
className={cn(
|
|
116
|
+
'shrink-0 text-sm font-medium whitespace-nowrap tabular-nums',
|
|
117
|
+
countPending && 'invisible',
|
|
118
|
+
)}
|
|
119
|
+
aria-hidden={countPending || undefined}
|
|
120
|
+
>
|
|
106
121
|
{t('dataTable.pageOf', {
|
|
107
122
|
defaultValue: 'Page {{page}} of {{pageCount}}',
|
|
108
123
|
page: pageIndex + 1,
|