Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
51e00b4195
|
||
|
|
84c95e524a | ||
|
|
577bd3dee8
|
||
|
|
4586f5b4f2 | ||
|
|
fe0077e86c
|
||
|
|
e540b6fcbd
|
||
|
|
4e17d720f4
|
||
|
|
d1eae03894
|
No files matched your search
@@ -2,7 +2,7 @@
|
||||
|
||||
A metadata-driven admin UI for the QQQ low-code application framework, rewritten from React + Material UI to Next.js 16 + Tailwind CSS + shadcn/ui. The UI renders entirely from backend metadata — no table names, field lists, or navigation items are hardcoded in the frontend.
|
||||
|
||||
**This checkout targets Next UI 1.0.0-RC.12**, with the always-available record Actions menu, private query-state clearing between users, standalone branding and CSP, grid focus and other fixes. Confirm availability in the [RC12 GitHub release](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.12) before installing; see the [RC12 notes](docs/releases/1.0.0-RC.12.md) for the preparation checkpoint and verification scope. [RC11](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.11) was the previously verified public candidate at that checkpoint and remains immutable. Final 1.0 remains held for application soak and explicit owner approval.
|
||||
**This checkout targets Next UI 1.0.0-RC.13**, with the SQL Workbench, FILE_PREVIEW thumbnails and lightbox, responsive layouts with readable child lists and compact dates and the instance home app. Confirm availability in the [RC13 GitHub release](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.13) before installing; see the [RC13 notes](docs/releases/1.0.0-RC.13.md) for the preparation checkpoint and verification scope. [RC12](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.12) was the previously verified public candidate at that checkpoint and remains immutable. Final 1.0 remains held for application soak and explicit owner approval.
|
||||
|
||||
**Current roadmap and known issues:** [Next UI 1.0 tracker](https://github.com/QRun-IO/qqq/issues/713). GitHub issues are the source of truth for status and remaining work; see [issue tracking](docs/ISSUE-TRACKING.md).
|
||||
|
||||
@@ -14,11 +14,11 @@ The dashboard is the default admin UI for QQQ 4.1+. Add the jar (managed by `qqq
|
||||
<dependency>
|
||||
<groupId>com.kingsrook.qqq</groupId>
|
||||
<artifactId>qqq-frontend-next</artifactId>
|
||||
<version>1.0.0-RC.12</version>
|
||||
<version>1.0.0-RC.13</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
The explicit version overrides an older BOM-managed frontend. RC12 requires a published QQQ 4.1 candidate: 4.1.0-RC.1 or later, and 4.1.0-RC.2 or later for standalone branding images (QQQ #1009); see the release notes for the verified backend. Select Next explicitly with `-Dqqq.javalin.frontend=next`. This does not release QQQ 4.1.
|
||||
The explicit version overrides an older BOM-managed frontend. RC13 requires a published QQQ 4.1 candidate: 4.1.0-RC.1 or later; 4.1.0-RC.2 or later for standalone branding images (QQQ #1009); and **4.1.0-RC.3 or later** for FILE_PREVIEW thumbnails (QQQ #1056) and the instance home app (QQQ #1059). The SQL Workbench needs the qbit-sql-workbench QBit (1.0.0-RC.1 or later); see the release notes for the verified backend. Select Next explicitly with `-Dqqq.javalin.frontend=next`. This does not release QQQ 4.1.
|
||||
|
||||
The jar holds the static export (`pnpm build:export`) under `next-dashboard/`. Deep links such as `/app/person/1` are served from placeholder pages and read their route from the browser path, so no Node.js server is involved. The Material Dashboard remains available: select it with `withServeFrontendMaterialDashboard(true)` or `-Dqqq.javalin.frontend=material`. Material routes are `/<app>/<table>/<id>`; Next routes are `/app/<table>/<id>`.
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# Next UI agent guide
|
||||
|
||||
> **This checkout targets RC12.** Confirm availability in the [RC12 GitHub release](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.12) before installing. [RC12 notes](../releases/1.0.0-RC.12.md) record the preparation checkpoint and verification scope; RC11 was the previously verified public candidate at that checkpoint. Earlier versioned receipts remain historical evidence; final 1.0 remains held.
|
||||
> **This checkout targets RC13.** Confirm availability in the [RC13 GitHub release](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.13) before installing. [RC13 notes](../releases/1.0.0-RC.13.md) record the preparation checkpoint and verification scope; RC12 was the previously verified public candidate at that checkpoint. Earlier versioned receipts remain historical evidence; final 1.0 remains held.
|
||||
|
||||
> **Live roadmap and known issues:** [QRun-IO/qqq#713](https://github.com/QRun-IO/qqq/issues/713). GitHub issues own current status and remaining work. Follow the [issue-tracking policy](../ISSUE-TRACKING.md) when reporting or resolving findings; keep this guide focused on usage and evidence.
|
||||
|
||||
This is an implementation playbook for coding agents working on the QQQ Next UI. Read the [developer guide](./next-ui-developer-guide.md) for user and integrator behavior, the repository [AGENTS.md](../../AGENTS.md) and [CLAUDE.md](../../CLAUDE.md) for scoped conventions, and the [real-server feature matrix](../acceptance/feature-matrix.md) plus [Material parity ledger](../acceptance/material-parity.md) before claiming compatibility. This guide reflects the `feature/next-1.0` release worktree as of 2026-10-01; the previously verified public candidate at the RC12 preparation checkpoint was [1.0.0-RC.11](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.11) at `2853b9b`, and final `1.0.0` remains on hold. Later branch changes must not be attributed to that immutable release. Some ledger entries predate integrated changes, so reconcile them with source and real-server evidence. A green subset of tests is evidence for that subset, not proof of complete Material parity or a published 1.0 release.
|
||||
This is an implementation playbook for coding agents working on the QQQ Next UI. Read the [developer guide](./next-ui-developer-guide.md) for user and integrator behavior, the repository [AGENTS.md](../../AGENTS.md) and [CLAUDE.md](../../CLAUDE.md) for scoped conventions, and the [real-server feature matrix](../acceptance/feature-matrix.md) plus [Material parity ledger](../acceptance/material-parity.md) before claiming compatibility. This guide reflects the `feature/next-1.0` release worktree as of 2026-10-01; the previously verified public candidate at the RC13 preparation checkpoint was [1.0.0-RC.12](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.12) at `e333223`, and final `1.0.0` remains on hold. Later branch changes must not be attributed to that immutable release. Some ledger entries predate integrated changes, so reconcile them with source and real-server evidence. A green subset of tests is evidence for that subset, not proof of complete Material parity or a published 1.0 release.
|
||||
|
||||
## Operating rule: metadata is the product boundary
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# Next UI developer guide
|
||||
|
||||
> **This checkout targets RC12.** Confirm availability in the [RC12 GitHub release](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.12) before installing. [RC12 notes](../releases/1.0.0-RC.12.md) record the preparation checkpoint and verification scope; RC11 was the previously verified public candidate at that checkpoint. Earlier versioned receipts remain historical evidence; final 1.0 remains held.
|
||||
> **This checkout targets RC13.** Confirm availability in the [RC13 GitHub release](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.13) before installing. [RC13 notes](../releases/1.0.0-RC.13.md) record the preparation checkpoint and verification scope; RC12 was the previously verified public candidate at that checkpoint. Earlier versioned receipts remain historical evidence; final 1.0 remains held.
|
||||
|
||||
> **RC9:** application code is unchanged from RC8. See [RC9 notes](../releases/1.0.0-RC.9.md) and [published releases](https://github.com/QRun-IO/qqq-frontend-next/releases) for versioned verification and public delivery status. Earlier RC8 receipts below remain historical evidence.
|
||||
|
||||
> **Live roadmap and known issues:** [QRun-IO/qqq#713](https://github.com/QRun-IO/qqq/issues/713). GitHub issues own current status and remaining work. Follow the [issue-tracking policy](../ISSUE-TRACKING.md) when reporting or resolving findings; keep this guide focused on usage and evidence.
|
||||
|
||||
This guide describes the `feature/next-1.0` release worktree as of 2026-10-01. It explains the current frontend and backend metadata contract. The previously verified public candidate at the RC12 preparation checkpoint was [1.0.0-RC.11](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.11) (commit `2853b9b`); final `1.0.0` remains on hold. Later branch changes are not retroactive changes to that immutable release. For behavior proven against a real server use the [feature matrix](../acceptance/feature-matrix.md); for the full Material inventory use the [parity ledger](../acceptance/material-parity.md). Some ledger rows predate recent work, so verify an individual status against current source and acceptance results before making a release claim.
|
||||
This guide describes the `feature/next-1.0` release worktree as of 2026-10-01. It explains the current frontend and backend metadata contract. The previously verified public candidate at the RC13 preparation checkpoint was [1.0.0-RC.12](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.12) (commit `e333223`); final `1.0.0` remains on hold. Later branch changes are not retroactive changes to that immutable release. For behavior proven against a real server use the [feature matrix](../acceptance/feature-matrix.md); for the full Material inventory use the [parity ledger](../acceptance/material-parity.md). Some ledger rows predate recent work, so verify an individual status against current source and acceptance results before making a release claim.
|
||||
|
||||
The visual baseline is the September 25 Next UI design (`e42ad2b2`). Material remains the reference for backend contracts and functional behavior. The layout defaults below describe current source; they do not certify a completed visual review.
|
||||
|
||||
@@ -25,7 +25,7 @@ The source of truth for the frontend shape is [`src/types/metadata.ts`](../../sr
|
||||
}
|
||||
```
|
||||
|
||||
`appTree` controls the sidebar; `apps[name].widgets` controls ordered dashboard widgets; `apps[name].sections` controls the app home cards. The server may omit denied objects or send disabled permission flags; the frontend also checks flags before requesting data. The server must still enforce authorization. A widget can also be placed in a table section through `QTableSection.widgetName`, or embedded in a process step of type `WIDGET`. `QInstance.redirects` can map old exact or wildcard Material paths to new targets. `supplementalInstanceMetaData.materialDashboardTheme` supplies the application theme, and `environmentValues` can configure analytics.
|
||||
`appTree` controls the sidebar; `apps[name].widgets` controls ordered dashboard widgets; `apps[name].sections` controls the app home cards. The server may omit denied objects or send disabled permission flags; the frontend also checks flags before requesting data. The server must still enforce authorization. A widget can also be placed in a table section through `QTableSection.widgetName`, or embedded in a process step of type `WIDGET`. `QInstance.redirects` can map old exact or wildcard Material paths to new targets. `QInstance.homeAppName` (QQQ [#1059](https://github.com/QRun-IO/qqq/issues/1059)) designates the app whose dashboard is the instance's home page; it is published only to users who may open that app. `supplementalInstanceMetaData.materialDashboardTheme` supplies the application theme, and `environmentValues` can configure analytics.
|
||||
|
||||
## 2. Install and run
|
||||
|
||||
@@ -35,7 +35,7 @@ The preferred QQQ-hosted distribution is the `com.kingsrook.qqq:qqq-frontend-nex
|
||||
<dependency>
|
||||
<groupId>com.kingsrook.qqq</groupId>
|
||||
<artifactId>qqq-frontend-next</artifactId>
|
||||
<version>1.0.0-RC.12</version>
|
||||
<version>1.0.0-RC.13</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
@@ -48,7 +48,7 @@ pnpm build:export
|
||||
pnpm build:jar
|
||||
```
|
||||
|
||||
The mock setting is for isolated development. Set `NEXT_PUBLIC_MOCK_API=false` and connect a real QQQ backend for integration. `build:export` writes `out/`; `build:jar` packages it with Maven. The Maven POM enforces the presence of static placeholder pages. The default `pnpm build` instead creates a standalone Node server, whose `QQQ_BACKEND_URL` build setting adds same-origin rewrites. For the container path see [`docker/quickstart/Dockerfile`](../../docker/quickstart/Dockerfile). This checkout targets `1.0.0-RC.12`, overriding an older BOM-managed frontend; confirm its availability in the [RC12 GitHub release](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.12) before installing. It requires a published QQQ 4.1 candidate (4.1.0-RC.1 or later; 4.1.0-RC.2 or later for standalone branding images, QQQ #1009); see the release notes for the verified backend. Use a matching local backend/frontend build to test later branch changes.
|
||||
The mock setting is for isolated development. Set `NEXT_PUBLIC_MOCK_API=false` and connect a real QQQ backend for integration. `build:export` writes `out/`; `build:jar` packages it with Maven. The Maven POM enforces the presence of static placeholder pages. The default `pnpm build` instead creates a standalone Node server, whose `QQQ_BACKEND_URL` build setting adds same-origin rewrites. For the container path see [`docker/quickstart/Dockerfile`](../../docker/quickstart/Dockerfile). This checkout targets `1.0.0-RC.13`, overriding an older BOM-managed frontend; confirm its availability in the [RC13 GitHub release](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.13) before installing. It requires a published QQQ 4.1 candidate (4.1.0-RC.1 or later; 4.1.0-RC.2 or later for standalone branding images, QQQ #1009; 4.1.0-RC.3 or later for FILE_PREVIEW thumbnails and the instance home app, QQQ #1056/#1059); see the release notes for the verified backend. Use a matching local backend/frontend build to test later branch changes.
|
||||
|
||||
| Setting | Behavior in this checkout |
|
||||
|---|---|
|
||||
@@ -77,7 +77,7 @@ Permissions apply to metadata and data: check table capabilities and the corresp
|
||||
|---|---|
|
||||
| `/` | Opens `/app`, or handles an Auth0 authorization callback. |
|
||||
| `/login`, `/callback`, `/token` | Sign-in and supported authorization callback routes. |
|
||||
| `/app` | Instance dashboard / app entry point. |
|
||||
| `/app` | Instance home page: the `homeAppName` app's dashboard when the instance designates one the user may open (client-side redirect to `/app/{homeAppName}`; the sidebar logo and breadcrumb Home crumb link there and the sidebar has no separate Dashboard entry), otherwise Next's overview. |
|
||||
| `/app/{appName}` | App home and its ordered widgets/sections. |
|
||||
| `/app/{tableName}` | Query or key lookup, according to table capability. |
|
||||
| `/app/{tableName}/create` | Create record. |
|
||||
|
||||
@@ -10,11 +10,11 @@ RC1 remains immutable: [v1.0.0-RC.1](https://github.com/QRun-IO/qqq-frontend-nex
|
||||
|
||||
The owner authorizes publishing new RCs at verified stability or daily milestones. Each RC gets a new immutable version and notes describing its fixes, backend prerequisites, verification, and known gaps. RC publication does not authorize final 1.0.
|
||||
|
||||
Previously verified public candidate at the RC12 preparation checkpoint: **[1.0.0-RC.11](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.11)**. Its public release entry records exact artifact verification, backend requirements and known limits. RC1 through RC10 remain available and unchanged.
|
||||
Previously verified public candidate at the RC13 preparation checkpoint: **[1.0.0-RC.12](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.12)**. Its public release entry records exact artifact verification, backend requirements and known limits. RC1 through RC11 remain available and unchanged.
|
||||
|
||||
## Fixes during testing
|
||||
|
||||
This checkout targets [RC12](1.0.0-RC.12.md); confirm availability in the [RC12 GitHub release](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.12) before installing. The candidate adds the always-available record Actions menu ([#45](https://github.com/QRun-IO/qqq-frontend-next/issues/45)), private query-state clearing between users (QQQ #1006), standalone branding and CSP derivation (QQQ #1009, #1017), grid and report focus fixes, and the other changes listed in the RC12 notes. These changes are absent from immutable RC11. Candidate notes distinguish versioned local checks from public delivery and retain the historical browser failures and remaining RC limits. Local previews may intentionally preserve an earlier version.
|
||||
This checkout targets [RC13](1.0.0-RC.13.md); confirm availability in the [RC13 GitHub release](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.13) before installing. The candidate adds the SQL Workbench ([PR47](https://github.com/QRun-IO/qqq-frontend-next/pull/47)), FILE_PREVIEW thumbnails and lightbox ([PR49](https://github.com/QRun-IO/qqq-frontend-next/pull/49), QQQ #1056), responsive layouts with readable child lists and compact dates ([PR50](https://github.com/QRun-IO/qqq-frontend-next/pull/50)) and the instance home app ([PR53](https://github.com/QRun-IO/qqq-frontend-next/pull/53), QQQ #1059), and the other changes listed in the RC13 notes. These changes are absent from immutable RC12. Candidate notes distinguish versioned local checks from public delivery and retain the historical browser failures and remaining RC limits. Local previews may intentionally preserve an earlier version.
|
||||
|
||||
RC9's immutable-tag acceptance run completed with known Firefox and tablet failures; its release notes preserve the exact results. Follow [qqq#985](https://github.com/QRun-IO/qqq/issues/985) for browser investigations and [qqq#713](https://github.com/QRun-IO/qqq/issues/713) for the complete backlog. The [original Next visual review](../acceptance/next-ui-visual-review.md) and owner approval remain separate requirements.
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
# Next UI 1.0.0-RC.13 — preparation
|
||||
|
||||
At this preparation checkpoint, RC13 is being prepared for application testing and **is not yet published**. [RC12](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.12) remains the latest verified public candidate and is immutable. Planned artifacts are `com.kingsrook.qqq:qqq-frontend-next:1.0.0-RC.13` and `ghcr.io/qrun-io/qqq-frontend-next:1.0.0-RC.13`. Public hashes, signatures and image digests will be recorded after delivery verification. Final 1.0 remains held for application soak and explicit owner approval.
|
||||
|
||||
## Source
|
||||
|
||||
Main at `84c95e524a93056864e60863f5ab45357969e786` (merge of [PR53](https://github.com/QRun-IO/qqq-frontend-next/pull/53)). Merged since RC12: [PR47](https://github.com/QRun-IO/qqq-frontend-next/pull/47) (SQL Workbench), [PR49](https://github.com/QRun-IO/qqq-frontend-next/pull/49) (FILE_PREVIEW thumbnails and lightbox), [PR50](https://github.com/QRun-IO/qqq-frontend-next/pull/50) (responsive layouts, readable child lists, compact dates) and [PR53](https://github.com/QRun-IO/qqq-frontend-next/pull/53) (instance home app). The single page scroll container ([PR54](https://github.com/QRun-IO/qqq-frontend-next/pull/54), its WebKit acceptance not yet green) and widget width readability ([PR55](https://github.com/QRun-IO/qqq-frontend-next/pull/55)) are not included; they follow in RC14.
|
||||
|
||||
## Backend requirements
|
||||
|
||||
RC13 runs on a published QQQ 4.1 candidate: **4.1.0-RC.1** or later. Standalone branding images need **4.1.0-RC.2** or later (QQQ #1009). **FILE_PREVIEW thumbnails and the instance home app need QQQ 4.1.0-RC.3 or later**, which carries the `FILE_PREVIEW` adornment ([qqq#1056](https://github.com/QRun-IO/qqq/issues/1056)) and `QInstance.homeAppName` ([qqq#1059](https://github.com/QRun-IO/qqq/issues/1059)); on older backends neither appears and the rest of RC13 works unchanged. The SQL Workbench page needs the [qbit-sql-workbench](https://github.com/QRun-IO/qbit-sql-workbench) QBit, **1.0.0-RC.1** or later, on the backend. RC13 does not publish QQQ 4.1.
|
||||
|
||||
## New features
|
||||
|
||||
- SQL Workbench: render the `SQL_WORKBENCH` widget of the [qbit-sql-workbench](https://github.com/QRun-IO/qbit-sql-workbench) QBit as a super-user SQL notebook.
|
||||
- Personal and shared notebooks with SQL and markdown cells, `:parameters`, versions and pinned results.
|
||||
- A CodeMirror 6 editor with dialect highlighting, schema and QQQ-name autocomplete, and formatting.
|
||||
- A typed, sortable, paged result grid with CSV/XLSX/JSON export, quick charts, and EXPLAIN plan trees.
|
||||
- Read-only runs by default, and a two-step write confirmation for users with the write permission.
|
||||
- CodeMirror and the formatter load only on that page (new `lazyChunks` budgets).
|
||||
|
||||
## Features
|
||||
|
||||
- Support an instance home app. When the backend designates one with `QInstance.homeAppName` ([qqq#1059](https://github.com/QRun-IO/qqq/issues/1059)), and the user may open it, `/app` shows that app's dashboard (a client-side redirect, matching backends that redirect full page loads of `/app/`), the sidebar logo and the breadcrumb Home crumb link there, and the sidebar drops its separate Dashboard entry. Without a home app, or with one the user may not open, `/app` remains Next's overview.
|
||||
- Show `FILE_PREVIEW`-adorned fields as thumbnails, and open a preview on click or Enter. Thumbnails appear in the query grid and card view, the record view (a larger thumbnail with the file name), associated-record tables, child-record list widgets and record-grid widgets. They load lazily at a fixed size, so loading does not shift the layout, with a loading pulse and a file-type icon fallback. The preview is an accessible dialog: focus is trapped and restored, Escape closes it, and Left/Right/Home/End step through the other previews on the same screen. It offers zoom and fit, open-original and download links, and shows type, size, dimensions and caption. PDFs served by the backend (same-origin `/qqq/` paths) and video/audio play inline; other types, and other or cross-origin PDFs (frames are limited to `'self'`), get a file card or open in a new tab. Thumbnails are shown only for explicitly adorned fields; plain URL values are never auto-detected. URLs are limited to http(s), same-origin paths and inline images. The lightbox loads on demand (about 3.5 KB gzip). Requires the QQQ backend adornment from [qqq#1056](https://github.com/QRun-IO/qqq/issues/1056).
|
||||
|
||||
## Product fixes
|
||||
|
||||
- Size record-view cards, dashboard widgets and form fields by the width of their own grid (container queries) rather than the viewport: table panels sit side by side only when each is readable and otherwise stack, cards keep their own height, fields share a row only when they fit, and very wide screens use the room. No horizontal page scroll from 360px up.
|
||||
- Make child-record lists in record-view panels readable: they show the columns that matter most and fit (the record name, status and boolean columns, the most recently changed date; the key and long text last), with `+N more columns` and View All for the rest. Cells align to the top and long text is clamped with the full text on hover. A widget's `onlyIncludeFieldNames` order is respected.
|
||||
- Show dates compactly everywhere: `6:33 PM` today, `Jul 5, 6:33 PM` this year, `Jul 5, 2025` before, on one line with the full zoned timestamp on hover, in the viewer's zone and locale. DATE values are no longer shifted a day west of UTC. Exports are unchanged.
|
||||
|
||||
## Bundle budget
|
||||
|
||||
The record developer route `/app/_/_/dev` and the table developer route `/app/_/dev` each have a 360 KB first-load cap (measured 354.9 KB and 350.8 KB on the combined tree); every other route keeps the 350 KB default. The SQL Workbench editor (CodeMirror 6) and formatter load on demand under separate `lazyChunks` budgets; the FILE_PREVIEW lightbox is a lazy chunk.
|
||||
|
||||
## Verification checkpoint
|
||||
|
||||
PR checks on the RC13 preparation source (typecheck, lint, unit tests and coverage, mocked e2e, bundle budgets, Storybook, image builds and the full real-backend acceptance workflow) are recorded on the preparation PR. During integration the WebKit/tablet NAV-016, NAV-023 and NAV-056 access-control diagnostics and one Firefox image-decode diagnostic (SEC-026) recurred intermittently and passed on rerun; they belong to the tracked [#985](https://github.com/QRun-IO/qqq/issues/985) / [#951](https://github.com/QRun-IO/qqq/issues/951) investigations and no assertion was relaxed. Tag acceptance and public-artifact verification are recorded in the [RC13 GitHub release](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.13) after delivery. No RC12 test count or artifact digest is attributed to RC13.
|
||||
|
||||
## Remaining RC limits
|
||||
|
||||
The RC12 limits carry forward unchanged: Material parity, sub-path hosting, real Auth0/Drive/QuickSight verification, visual review and application soak remain tracked in [#713](https://github.com/QRun-IO/qqq/issues/713), [#711](https://github.com/QRun-IO/qqq/issues/711), [#712](https://github.com/QRun-IO/qqq/issues/712) and [#904](https://github.com/QRun-IO/qqq/issues/904). The image backend origin remains fixed at build time to `host.docker.internal:8000`; use the JAR or rebuild for another origin.
|
||||
@@ -1,25 +1,10 @@
|
||||
# Next UI — unreleased
|
||||
|
||||
[RC12 preparation](1.0.0-RC.12.md) collects every change merged since [RC11](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.11): the always-available record Actions menu ([#45](https://github.com/QRun-IO/qqq-frontend-next/issues/45)), private query-state clearing between users, standalone branding and CSP derivation, grid and report focus fixes, and browser-testing hardening. RC11 is published and immutable. Changes merged after the RC12 preparation commit are listed below. Central tracking: [qqq#713](https://github.com/QRun-IO/qqq/issues/713), [browser investigations #985](https://github.com/QRun-IO/qqq/issues/985), and [QQQ Roadmap Project 12](https://github.com/orgs/QRun-IO/projects/12).
|
||||
|
||||
## New features
|
||||
|
||||
- SQL Workbench: render the `SQL_WORKBENCH` widget of the [qbit-sql-workbench](https://github.com/QRun-IO/qbit-sql-workbench) QBit as a super-user SQL notebook.
|
||||
- Personal and shared notebooks with SQL and markdown cells, `:parameters`, versions and pinned results.
|
||||
- A CodeMirror 6 editor with dialect highlighting, schema and QQQ-name autocomplete, and formatting.
|
||||
- A typed, sortable, paged result grid with CSV/XLSX/JSON export, quick charts, and EXPLAIN plan trees.
|
||||
- Read-only runs by default, and a two-step write confirmation for users with the write permission.
|
||||
- CodeMirror and the formatter load only on that page (new `lazyChunks` budgets).
|
||||
|
||||
## Features
|
||||
|
||||
- Show `FILE_PREVIEW`-adorned fields as thumbnails, and open a preview on click or Enter. Thumbnails appear in the query grid and card view, the record view (a larger thumbnail with the file name), associated-record tables, child-record list widgets and record-grid widgets. They load lazily at a fixed size, so loading does not shift the layout, with a loading pulse and a file-type icon fallback. The preview is an accessible dialog: focus is trapped and restored, Escape closes it, and Left/Right/Home/End step through the other previews on the same screen. It offers zoom and fit, open-original and download links, and shows type, size, dimensions and caption. PDFs served by the backend (same-origin `/qqq/` paths) and video/audio play inline; other types, and other or cross-origin PDFs (frames are limited to `'self'`), get a file card or open in a new tab. Thumbnails are shown only for explicitly adorned fields; plain URL values are never auto-detected. URLs are limited to http(s), same-origin paths and inline images. The lightbox loads on demand (about 3.5 KB gzip). Requires the QQQ backend adornment from [qqq#1056](https://github.com/QRun-IO/qqq/issues/1056).
|
||||
[RC13 preparation](1.0.0-RC.13.md) collects every change merged since [RC12](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.12): the SQL Workbench, FILE_PREVIEW thumbnails and lightbox, responsive layouts with readable child lists and compact dates and the instance home app. RC12 is published and immutable. Changes merged after the RC13 preparation commit are listed below. Central tracking: [qqq#713](https://github.com/QRun-IO/qqq/issues/713), [browser investigations #985](https://github.com/QRun-IO/qqq/issues/985), and [QQQ Roadmap Project 12](https://github.com/orgs/QRun-IO/projects/12).
|
||||
|
||||
## Product fixes
|
||||
|
||||
- Size record-view cards, dashboard widgets and form fields by the width of their own grid (container queries) rather than the viewport: table panels sit side by side only when each is readable and otherwise stack, cards keep their own height, fields share a row only when they fit, and very wide screens use the room. No horizontal page scroll from 360px up.
|
||||
- Make child-record lists in record-view panels readable: they show the columns that matter most and fit (the record name, status and boolean columns, the most recently changed date; the key and long text last), with `+N more columns` and View All for the rest. Cells align to the top and long text is clamped with the full text on hover. A widget's `onlyIncludeFieldNames` order is respected.
|
||||
- Show dates compactly everywhere: `6:33 PM` today, `Jul 5, 6:33 PM` this year, `Jul 5, 2025` before, on one line with the full zoned timestamp on hover, in the viewer's zone and locale. DATE values are no longer shifted a day west of UTC. Exports are unchanged.
|
||||
None yet.
|
||||
|
||||
## Browser testing and infrastructure
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "qqq-frontend-next",
|
||||
"version": "1.0.0-RC.12",
|
||||
"version": "1.0.0-RC.13",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@9.15.9",
|
||||
"scripts": {
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
</developers>
|
||||
|
||||
<properties>
|
||||
<revision>1.0.0-RC.12</revision>
|
||||
<revision>1.0.0-RC.13</revision>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<project.build.outputTimestamp>2026-09-24T00:00:00Z</project.build.outputTimestamp>
|
||||
</properties>
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright 2026 QRun.IO, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// Tests for the /app home page: Next's overview, or the instance's home app (QRun-IO/qqq#1059)
|
||||
|
||||
import React from 'react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
|
||||
const replace = vi.fn()
|
||||
vi.mock('next/navigation', () => ({
|
||||
usePathname: () => '/app',
|
||||
useRouter: () => ({ push: vi.fn(), replace }),
|
||||
}))
|
||||
vi.mock('@/lib/context/q-context', () => ({ useQContext: () => ({ setPageHeader: vi.fn() }) }))
|
||||
vi.mock('@/lib/auth/use-auth', () => ({ useAuth: () => ({ user: { name: 'Avery Example' } }) }))
|
||||
const metaData = vi.hoisted(() => ({ value: {} as Record<string, unknown> }))
|
||||
vi.mock('@/lib/api/metadata', () => ({ loadMetaData: vi.fn(async () => metaData.value) }))
|
||||
|
||||
import DashboardPage from './page'
|
||||
|
||||
/**
|
||||
* Instance metadata with two apps, optionally designating a home app.
|
||||
*
|
||||
* @param homeAppName - The home app.
|
||||
* @returns The metadata.
|
||||
*/
|
||||
function instance(homeAppName?: string): Record<string, unknown> {
|
||||
return {
|
||||
apps: { home: { name: 'home', label: 'Home' }, cms: { name: 'cms', label: 'CMS' } },
|
||||
appTree: [{ name: 'home', label: 'Home', type: 'APP', children: [] }, { name: 'cms', label: 'CMS', type: 'APP', children: [] }],
|
||||
tables: {}, processes: {}, reports: {}, widgets: {},
|
||||
...(homeAppName ? { homeAppName } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
/** Renders the page with a fresh query client. */
|
||||
function renderPage() {
|
||||
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } })
|
||||
return render(<QueryClientProvider client={client}><DashboardPage /></QueryClientProvider>)
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
replace.mockReset()
|
||||
})
|
||||
|
||||
describe('/app home page', () => {
|
||||
it('shows the overview when the instance designates no home app', async () => {
|
||||
metaData.value = instance()
|
||||
renderPage()
|
||||
await waitFor(() => expect(screen.getByText('CMS')).toBeInTheDocument())
|
||||
expect(document.querySelector('[data-qqq-id="dashboard-home"]')).toBeInTheDocument()
|
||||
expect(replace).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('goes to the home app dashboard instead of the overview when the instance designates one', async () => {
|
||||
metaData.value = instance('home')
|
||||
renderPage()
|
||||
await waitFor(() => expect(replace).toHaveBeenCalledWith('/app/home'))
|
||||
expect(document.querySelector('[data-qqq-id="dashboard-home"]')).not.toBeInTheDocument()
|
||||
expect(document.querySelector('[data-qqq-id="dashboard-home-redirect"]')).toHaveAttribute('aria-busy', 'true')
|
||||
})
|
||||
|
||||
it('keeps the overview when the designated home app is not one the user may open', async () => {
|
||||
metaData.value = { ...instance(), homeAppName: 'secret' }
|
||||
renderPage()
|
||||
await waitFor(() => expect(screen.getByText('CMS')).toBeInTheDocument())
|
||||
expect(replace).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import {
|
||||
Workflow,
|
||||
@@ -44,6 +45,7 @@ import { canInsertRecords, canRunProcess } from '@/lib/auth/permissions'
|
||||
import { useAppTreeRoutes } from '@/lib/hooks/use-routes'
|
||||
import type { SidebarRoute } from '@/lib/hooks/use-routes'
|
||||
import { getRecentRecords } from '@/lib/utils/recent-records'
|
||||
import { DEFAULT_HOME_PATH } from '@/lib/utils/home-app'
|
||||
import type { RecentRecord } from '@/lib/utils/recent-records'
|
||||
import { MetadataIcon, type MetadataIconKind } from '@/components/layout/MetadataIcon'
|
||||
|
||||
@@ -102,7 +104,15 @@ export default function DashboardPage() {
|
||||
staleTime: 1000 * 60 * 30,
|
||||
})
|
||||
|
||||
const { sidebarRoutes, navTargets } = useAppTreeRoutes(metaData)
|
||||
const { sidebarRoutes, navTargets, homePath } = useAppTreeRoutes(metaData)
|
||||
const router = useRouter()
|
||||
const hasHomeApp = homePath !== DEFAULT_HOME_PATH
|
||||
|
||||
// An instance with a home app (homeAppName) shows that app's dashboard as its home page
|
||||
// (the backend redirects full page loads of /app; this covers client-side navigation).
|
||||
useEffect(() => {
|
||||
if (hasHomeApp) router.replace(homePath)
|
||||
}, [hasHomeApp, homePath, router])
|
||||
const appRoutes = sidebarRoutes.filter((route) => route.nodeType === 'APP')
|
||||
|
||||
const appName = metaData?.branding?.appName || 'QQQ Admin'
|
||||
@@ -145,6 +155,10 @@ export default function DashboardPage() {
|
||||
{ id: 'widgets', label: 'Widgets', value: widgetNames.size, hint: 'Dashboard components', icon: <TrendingUp className="h-4 w-4 text-muted-foreground" aria-hidden="true" /> },
|
||||
]
|
||||
|
||||
if (hasHomeApp) {
|
||||
return <div aria-busy="true" data-qqq-id="dashboard-home-redirect" />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-8" data-qqq-id="dashboard-home">
|
||||
{/* Welcome header */}
|
||||
|
||||
@@ -99,7 +99,7 @@ function DashboardLayoutContent({ children }: { children: React.ReactNode }) {
|
||||
})
|
||||
|
||||
// Generate sidebar routes and path map from app tree
|
||||
const { sidebarRoutes, pathToLabelMap, ancestorAppMap, navTargets } = useAppTreeRoutes(metaData)
|
||||
const { sidebarRoutes, pathToLabelMap, ancestorAppMap, navTargets, homePath } = useAppTreeRoutes(metaData)
|
||||
|
||||
// Every route change starts at the top of the page (Material App scrolls to 0 on each pathname)
|
||||
useEffect(() => {
|
||||
@@ -273,6 +273,7 @@ function DashboardLayoutContent({ children }: { children: React.ReactNode }) {
|
||||
<Sidebar
|
||||
routes={sidebarRoutes}
|
||||
branding={metaData?.branding}
|
||||
homePath={homePath}
|
||||
logout={logout}
|
||||
userName={user?.name}
|
||||
userEmail={user?.email}
|
||||
@@ -283,6 +284,7 @@ function DashboardLayoutContent({ children }: { children: React.ReactNode }) {
|
||||
<Sidebar
|
||||
routes={sidebarRoutes}
|
||||
branding={metaData?.branding}
|
||||
homePath={homePath}
|
||||
logout={logout}
|
||||
userName={user?.name}
|
||||
userEmail={user?.email}
|
||||
@@ -306,6 +308,7 @@ function DashboardLayoutContent({ children }: { children: React.ReactNode }) {
|
||||
helpButtonRef={helpButtonRef}
|
||||
pathToLabelMap={pathToLabelMap}
|
||||
ancestorAppMap={ancestorAppMap}
|
||||
homePath={homePath}
|
||||
navTargets={navTargets}
|
||||
searchTables={searchTables}
|
||||
/>
|
||||
|
||||
@@ -178,3 +178,14 @@ describe('Breadcrumbs', () => {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('Breadcrumbs home crumb', () => {
|
||||
it('links to the instance home app when there is one, and to the overview otherwise', () => {
|
||||
currentPathname = '/app/person'
|
||||
const { unmount } = render(<Breadcrumbs pathToLabelMap={pathToLabelMap} ancestorAppMap={ancestorAppMap} homePath="/app/home" />)
|
||||
expect(screen.getByRole('link', { name: 'Home' })).toHaveAttribute('href', '/app/home')
|
||||
unmount()
|
||||
render(<Breadcrumbs pathToLabelMap={pathToLabelMap} ancestorAppMap={ancestorAppMap} />)
|
||||
expect(screen.getByRole('link', { name: 'Home' })).toHaveAttribute('href', '/app')
|
||||
})
|
||||
})
|
||||
@@ -35,6 +35,8 @@ export interface BreadcrumbsProps {
|
||||
pathToLabelMap: Record<string, string>
|
||||
/** Maps node paths to their enclosing apps, outermost first. */
|
||||
ancestorAppMap?: Record<string, ParentAppInfo[]>
|
||||
/** Where the home crumb links: the instance's home app, or the `/app` overview (default). */
|
||||
homePath?: string
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -158,7 +160,7 @@ export function buildDocumentTitle(crumbs: Breadcrumb[], pageTitle: string | und
|
||||
* `<Link>` crumbs (ancestor pages) followed by a `<span aria-current="page">`
|
||||
* for the current page, or `null` on the dashboard root and before labels load.
|
||||
*/
|
||||
export default function Breadcrumbs({ pathToLabelMap, ancestorAppMap = {} }: BreadcrumbsProps) {
|
||||
export default function Breadcrumbs({ pathToLabelMap, ancestorAppMap = {}, homePath = '/app' }: BreadcrumbsProps) {
|
||||
const pathname = usePathname()
|
||||
const breadcrumbs = buildBreadcrumbs(pathname, pathToLabelMap, ancestorAppMap)
|
||||
const trailRef = useRef<HTMLElement>(null)
|
||||
@@ -189,9 +191,9 @@ export default function Breadcrumbs({ pathToLabelMap, ancestorAppMap = {} }: Bre
|
||||
aria-label="Breadcrumb"
|
||||
data-qqq-id="breadcrumbs"
|
||||
>
|
||||
{/* Home crumb (Material's home icon), back to the dashboard */}
|
||||
{/* Home crumb (Material's home icon), back to the home page (the home app, or the overview) */}
|
||||
<Link
|
||||
href="/app"
|
||||
href={homePath}
|
||||
aria-label="Home"
|
||||
className="inline-flex items-center justify-center text-muted-foreground hover:text-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-ring pointer-coarse:min-h-11 pointer-coarse:min-w-11 pointer-coarse:flex-shrink-0"
|
||||
data-qqq-id="breadcrumb-home"
|
||||
|
||||
@@ -53,6 +53,8 @@ export interface HeaderProps {
|
||||
pathToLabelMap?: Record<string, string>
|
||||
/** Maps node paths to their enclosing apps, passed through to Breadcrumbs. */
|
||||
ancestorAppMap?: Record<string, ParentAppInfo[]>
|
||||
/** Where the breadcrumb home crumb links (the instance's home app, or `/app`). */
|
||||
homePath?: string
|
||||
/** Navigable app-tree nodes for the header search. */
|
||||
navTargets?: NavTarget[]
|
||||
/** Tables the backend record search covers, for the header search (empty: local search only). */
|
||||
@@ -75,7 +77,7 @@ export interface HeaderProps {
|
||||
* {@link GlobalSearch} + notifications bell on the right. The hamburger and
|
||||
* GlobalSearch are each conditionally visible based on the `md` breakpoint.
|
||||
*/
|
||||
export default function Header({ onMenuOpen, menuOpen = false, menuButtonRef, onSearchOpen, onCommandOpen, onHelpOpen, helpButtonRef, pathToLabelMap = {}, ancestorAppMap = {}, navTargets = [], searchTables }: HeaderProps) {
|
||||
export default function Header({ onMenuOpen, menuOpen = false, menuButtonRef, onSearchOpen, onCommandOpen, onHelpOpen, helpButtonRef, pathToLabelMap = {}, ancestorAppMap = {}, homePath, navTargets = [], searchTables }: HeaderProps) {
|
||||
const [notificationCount] = useState(0)
|
||||
// toasts appear below the header, never over its controls
|
||||
const headerRef = useRef<HTMLElement>(null)
|
||||
@@ -103,7 +105,7 @@ export default function Header({ onMenuOpen, menuOpen = false, menuButtonRef, on
|
||||
>
|
||||
<Menu className="h-5 w-5" aria-hidden="true" />
|
||||
</button>
|
||||
<Breadcrumbs pathToLabelMap={pathToLabelMap} ancestorAppMap={ancestorAppMap} />
|
||||
<Breadcrumbs pathToLabelMap={pathToLabelMap} ancestorAppMap={ancestorAppMap} homePath={homePath} />
|
||||
</div>
|
||||
|
||||
{/* Right section: search + help + notifications */}
|
||||
|
||||
@@ -500,3 +500,13 @@ describe('Sidebar', () => {
|
||||
expect(document.querySelector('[data-qqq-id="sidebar-user-avatar"]')).toHaveTextContent('A')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Sidebar logo link', () => {
|
||||
it('links to the instance home app when there is one, and to the overview otherwise', () => {
|
||||
const { container, unmount } = render(<Sidebar routes={leafRoutes} homePath="/app/home" />)
|
||||
expect(container.querySelector('[data-qqq-id="sidebar-logo-link"]')).toHaveAttribute('href', '/app/home')
|
||||
unmount()
|
||||
const plain = render(<Sidebar routes={leafRoutes} />)
|
||||
expect(plain.container.querySelector('[data-qqq-id="sidebar-logo-link"]')).toHaveAttribute('href', '/app')
|
||||
})
|
||||
})
|
||||
@@ -106,6 +106,8 @@ export interface SidebarProps {
|
||||
routes: SidebarRoute[]
|
||||
/** Application branding metadata (logo, icon, app name, banners). */
|
||||
branding?: QBrandingMetaData
|
||||
/** Where the logo links: the instance's home app, or the `/app` overview (default). */
|
||||
homePath?: string
|
||||
/** Called when the mouse enters the sidebar (desktop hover expansion). */
|
||||
onMouseEnter?: () => void
|
||||
/** Called when the mouse leaves the sidebar (desktop hover collapse). */
|
||||
@@ -132,21 +134,22 @@ export interface SidebarProps {
|
||||
*
|
||||
* @param props - Component properties.
|
||||
* @param props.branding - Branding metadata.
|
||||
* @param props.href - The home page the logo links to.
|
||||
* @returns The branding link element.
|
||||
*/
|
||||
function SidebarBranding({ branding }: { branding?: QBrandingMetaData }) {
|
||||
function SidebarBranding({ branding, href }: { branding?: QBrandingMetaData; href: string }) {
|
||||
const appName = branding?.appName || 'QQQ Admin'
|
||||
const linkClass = 'flex min-w-0 flex-1 items-center rounded focus:outline-none focus-visible:ring-2 focus-visible:ring-ring'
|
||||
if (branding?.logo) {
|
||||
return (
|
||||
<Link href="/app" className={linkClass} data-qqq-id="sidebar-logo-link">
|
||||
<Link href={href} className={linkClass} data-qqq-id="sidebar-logo-link">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element -- branding logos are arbitrary backend assets */}
|
||||
<img src={branding.logo} alt={appName} title={appName} className="max-h-12 w-full object-contain object-left" data-qqq-id="sidebar-logo" />
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Link href="/app" className={linkClass} data-qqq-id="sidebar-logo-link">
|
||||
<Link href={href} className={linkClass} data-qqq-id="sidebar-logo-link">
|
||||
{branding?.icon ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element -- branding icons are arbitrary backend assets
|
||||
<img src={branding.icon} alt="" className="h-12 w-12 flex-shrink-0 object-contain" data-qqq-id="sidebar-icon" />
|
||||
@@ -182,6 +185,7 @@ function SidebarBranding({ branding }: { branding?: QBrandingMetaData }) {
|
||||
export default function Sidebar({
|
||||
routes,
|
||||
branding,
|
||||
homePath = '/app',
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
logout,
|
||||
@@ -238,7 +242,7 @@ export default function Sidebar({
|
||||
<div className="flex min-h-0 flex-1 flex-col" data-qqq-id="sidenav-root">
|
||||
{/* Logo / App Branding — height matches header so border lines up */}
|
||||
<div className="flex items-center gap-2 border-b border-border px-4" style={{ height: 'var(--qqq-header-height)' }} data-qqq-id="sidenav-logo-area">
|
||||
<SidebarBranding branding={branding} />
|
||||
<SidebarBranding branding={branding} href={homePath} />
|
||||
{/* Close button for mobile drawer */}
|
||||
{isMobileDrawer && (
|
||||
<button
|
||||
|
||||
@@ -280,3 +280,60 @@ describe('useAppTreeRoutes', () => {
|
||||
expect(JSON.stringify(sidebarRoutes)).not.toContain('qryItem')
|
||||
})
|
||||
})
|
||||
|
||||
describe('buildRouteMap home app', () => {
|
||||
const tree: QAppTreeNode[] = [
|
||||
{ name: 'home', label: 'Home', type: 'APP', children: [] },
|
||||
{ name: 'cms', label: 'CMS', type: 'APP', children: [] },
|
||||
]
|
||||
|
||||
/**
|
||||
* Metadata with the tree's apps, optionally designating a home app.
|
||||
*
|
||||
* @param homeAppName - The instance's home app.
|
||||
* @returns The metadata.
|
||||
*/
|
||||
function withHome(homeAppName?: string): QInstance {
|
||||
const metaData = makeMetaData(tree)
|
||||
metaData.apps = { home: { name: 'home', label: 'Home' }, cms: { name: 'cms', label: 'CMS' } } as unknown as QInstance['apps']
|
||||
return homeAppName === undefined ? metaData : { ...metaData, homeAppName }
|
||||
}
|
||||
|
||||
it('keeps the Dashboard overview as home when the instance designates no home app', () => {
|
||||
const routes = buildRouteMap(withHome())
|
||||
expect(routes.homePath).toBe('/app')
|
||||
expect(routes.sidebarRoutes.map((route) => route.path)).toEqual(['/app', '/app/home', '/app/cms'])
|
||||
})
|
||||
|
||||
it('makes the home app the home page and drops the separate Dashboard entry', () => {
|
||||
const routes = buildRouteMap(withHome('home'))
|
||||
expect(routes.homePath).toBe('/app/home')
|
||||
expect(routes.sidebarRoutes.map((route) => route.path)).toEqual(['/app/home', '/app/cms'])
|
||||
})
|
||||
|
||||
it('ignores a home app that is not among the apps the user may open', () => {
|
||||
const routes = buildRouteMap(withHome('secret'))
|
||||
expect(routes.homePath).toBe('/app')
|
||||
expect(routes.sidebarRoutes[0].path).toBe('/app')
|
||||
expect(buildRouteMap(undefined).homePath).toBe('/app')
|
||||
})
|
||||
})
|
||||
|
||||
describe('buildRouteMap single landing page', () => {
|
||||
it('has no duplicate landing entry when homeAppName is set: the home app is the single top entry', () => {
|
||||
// kof22 labels its home app "Dashboard", and the backend may order it after other apps
|
||||
const metaData = makeMetaData([
|
||||
{ name: 'cms', label: 'CMS', type: 'APP', children: [] },
|
||||
{ name: 'home', label: 'Dashboard', type: 'APP', children: [] },
|
||||
])
|
||||
metaData.apps = { home: { name: 'home', label: 'Dashboard' }, cms: { name: 'cms', label: 'CMS' } } as unknown as QInstance['apps']
|
||||
const routes = buildRouteMap({ ...metaData, homeAppName: 'home' })
|
||||
expect(routes.homePath).toBe('/app/home')
|
||||
expect(routes.sidebarRoutes.map((route) => route.path)).toEqual(['/app/home', '/app/cms'])
|
||||
expect(routes.sidebarRoutes.filter((route) => route.name === 'Dashboard')).toHaveLength(1)
|
||||
expect(routes.sidebarRoutes.some((route) => route.key === 'dashboard' || route.path === '/app')).toBe(false)
|
||||
|
||||
// without homeAppName the built-in Dashboard overview entry stays first, as before
|
||||
expect(buildRouteMap(metaData).sidebarRoutes.map((route) => route.path)).toEqual(['/app', '/app/cms', '/app/home'])
|
||||
})
|
||||
})
|
||||
@@ -23,6 +23,7 @@ import { useMemo } from 'react'
|
||||
|
||||
import type { QInstance, QAppTreeNode, QIcon } from '@/types'
|
||||
import type { QAppNodeType } from '@/types/enums'
|
||||
import { DEFAULT_HOME_PATH, homePath as instanceHomePath } from '@/lib/utils/home-app'
|
||||
|
||||
/**
|
||||
* A single entry in the sidebar navigation tree.
|
||||
@@ -78,7 +79,7 @@ export interface NavTarget {
|
||||
* command palette, header search and landing page.
|
||||
*/
|
||||
export interface RouteMap {
|
||||
/** Ordered sidebar nodes: the Dashboard entry, then every top-level APP node. */
|
||||
/** Ordered sidebar nodes: the Dashboard entry (or, with a home app, that app as the single top entry), then every top-level APP node. */
|
||||
sidebarRoutes: SidebarRoute[]
|
||||
/** Maps every known route path to a human-readable label for breadcrumbs. */
|
||||
pathToLabelMap: Record<string, string>
|
||||
@@ -88,6 +89,11 @@ export interface RouteMap {
|
||||
navTargets: NavTarget[]
|
||||
/** The first accessible app route, used as the post-login redirect target. */
|
||||
defaultRoute: string
|
||||
/**
|
||||
* Where "home" goes (sidebar logo, breadcrumb root, `/app` itself): the instance's home app
|
||||
* (`homeAppName`) when it designates one the user may open, otherwise the `/app` overview.
|
||||
*/
|
||||
homePath: string
|
||||
}
|
||||
|
||||
/** Fixed pages under `/app` that are not app-tree nodes. */
|
||||
@@ -143,7 +149,7 @@ export function isHiddenNode(node: QAppTreeNode, metaData: QInstance): boolean {
|
||||
*/
|
||||
export function buildRouteMap(metaData: QInstance | undefined): RouteMap {
|
||||
if (!metaData) {
|
||||
return { sidebarRoutes: [], pathToLabelMap: {}, ancestorAppMap: {}, navTargets: [], defaultRoute: '/no-apps' }
|
||||
return { sidebarRoutes: [], pathToLabelMap: {}, ancestorAppMap: {}, navTargets: [], defaultRoute: '/no-apps', homePath: DEFAULT_HOME_PATH }
|
||||
}
|
||||
|
||||
const pathToLabelMap: Record<string, string> = { ...STATIC_PAGE_LABELS }
|
||||
@@ -212,11 +218,14 @@ export function buildRouteMap(metaData: QInstance | undefined): RouteMap {
|
||||
}
|
||||
}
|
||||
}
|
||||
const sidebarRoutes: SidebarRoute[] = [
|
||||
{ name: 'Dashboard', key: 'dashboard', path: '/app', icon: { name: 'dashboard' }, type: 'item' },
|
||||
...appRoutes,
|
||||
]
|
||||
return { sidebarRoutes, pathToLabelMap, ancestorAppMap, navTargets, defaultRoute: defaultRoute ?? '/no-apps' }
|
||||
// One landing page: with a home app, /app shows that app, which becomes the single top entry in
|
||||
// place of the built-in Dashboard (no separate overview entry beside it).
|
||||
const homePath = instanceHomePath(metaData)
|
||||
const homeRoute = appRoutes.find((route) => route.path === homePath)
|
||||
const sidebarRoutes: SidebarRoute[] = homeRoute
|
||||
? [homeRoute, ...appRoutes.filter((route) => route !== homeRoute)]
|
||||
: [{ name: 'Dashboard', key: 'dashboard', path: '/app', icon: { name: 'dashboard' }, type: 'item' }, ...appRoutes]
|
||||
return { sidebarRoutes, pathToLabelMap, ancestorAppMap, navTargets, defaultRoute: defaultRoute ?? '/no-apps', homePath }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2026 QRun.IO, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import type { QInstance } from '@/types'
|
||||
import { DEFAULT_HOME_PATH, homeAppName, homePath } from './home-app'
|
||||
|
||||
const apps = { home: { name: 'home', label: 'Home' }, 'my app': { name: 'my app', label: 'Mine' } } as unknown as QInstance['apps']
|
||||
|
||||
describe('home app', () => {
|
||||
it('is the designated app when the user may open it', () => {
|
||||
expect(homeAppName({ apps, homeAppName: 'home' })).toBe('home')
|
||||
expect(homePath({ apps, homeAppName: 'home' })).toBe('/app/home')
|
||||
expect(homePath({ apps, homeAppName: 'my app' })).toBe('/app/my%20app')
|
||||
})
|
||||
|
||||
it('falls back to the overview without a usable home app', () => {
|
||||
expect(DEFAULT_HOME_PATH).toBe('/app')
|
||||
expect(homePath(undefined)).toBe('/app')
|
||||
expect(homePath({ apps })).toBe('/app')
|
||||
expect(homePath({ apps, homeAppName: '' })).toBe('/app')
|
||||
expect(homePath({ apps, homeAppName: 'notThere' })).toBe('/app')
|
||||
expect(homeAppName({ apps: undefined as unknown as QInstance['apps'], homeAppName: 'home' })).toBeUndefined()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2026 QRun.IO, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file home-app — the instance's home page: the dashboard of the app the backend designates
|
||||
* as its home (`homeAppName`, QRun-IO/qqq#1059), or Next's own overview at `/app`.
|
||||
*/
|
||||
|
||||
import type { QInstance } from '@/types'
|
||||
|
||||
/** Next's own overview page, the home when the instance designates no home app. */
|
||||
export const DEFAULT_HOME_PATH = '/app'
|
||||
|
||||
/**
|
||||
* The instance's home app, when the backend designates one the user may open.
|
||||
*
|
||||
* The backend publishes `homeAppName` only to users who may access that app; it is also checked
|
||||
* against the apps in the metadata, so an older or inconsistent backend never sends the user to
|
||||
* an app that is not there.
|
||||
*
|
||||
* @param instance - Instance metadata, or undefined while loading.
|
||||
* @returns The app name, or undefined.
|
||||
*/
|
||||
export function homeAppName(instance: Pick<QInstance, 'homeAppName' | 'apps'> | undefined): string | undefined {
|
||||
const name = instance?.homeAppName
|
||||
return typeof name === 'string' && name !== '' && instance?.apps?.[name] ? name : undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Where "home" goes (the sidebar logo, the breadcrumb root, `/app` itself): the home app's
|
||||
* dashboard, or Next's overview.
|
||||
*
|
||||
* @param instance - Instance metadata, or undefined while loading.
|
||||
* @returns `/app/{homeApp}` or `/app`.
|
||||
*/
|
||||
export function homePath(instance: Pick<QInstance, 'homeAppName' | 'apps'> | undefined): string {
|
||||
const name = homeAppName(instance)
|
||||
return name ? `${DEFAULT_HOME_PATH}/${encodeURIComponent(name)}` : DEFAULT_HOME_PATH
|
||||
}
|
||||
@@ -65,6 +65,12 @@ export interface QInstance {
|
||||
* e.g. from a table under an app the user may not open to the same table under one they may.
|
||||
*/
|
||||
redirects?: Record<string, string>
|
||||
/**
|
||||
* The app whose dashboard is the instance's home page (QRun-IO/qqq#1059): `/app`, the sidebar
|
||||
* logo and the breadcrumb root go there. Sent only when the user may open that app; absent,
|
||||
* Next's own overview is the home page.
|
||||
*/
|
||||
homeAppName?: string
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user