Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
787a227602
|
||
|
|
30f4f9d586
|
||
|
|
24bbe4a038
|
||
|
|
8903f272c4
|
||
|
|
2345039911
|
||
|
|
4347924658
|
||
|
|
523f59cf38
|
||
|
|
fefd5be0d4
|
||
|
|
c0e5b285db
|
||
|
|
42b509f35c
|
||
|
|
37c4234324
|
||
|
|
412d884402
|
||
|
|
27e8754f32
|
||
|
|
00c00b0e71
|
||
|
|
3a72c8b88a
|
||
|
|
46e9f1212c | ||
|
|
bfcc008ca2 | ||
|
|
fdf12d3c40 | ||
|
|
04ab9ad6c9 | ||
|
|
70618bd143 | ||
|
|
aaa77d7e6e | ||
|
|
73b1ea16d7 | ||
|
|
94726bb52c | ||
|
|
56ab7fe21c | ||
|
|
0f2eb173c1 | ||
|
|
22f5749e36 | ||
|
|
090f5febea
|
||
|
|
afb9d75c75
|
||
|
|
119f1211d0
|
||
|
|
aba43263b2 | ||
|
|
ddc7de731b | ||
|
|
3d162dce74
|
||
|
|
d186ad7086 | ||
|
|
2186561950 | ||
|
|
40ee257847 | ||
|
|
b67fb5fd20 | ||
|
|
d04cfa4b58
|
||
|
|
94424b3f46
|
||
|
|
e31716aa52 | ||
|
|
9756b9d93b | ||
|
|
f205992878 | ||
|
|
5f503fa7e9 | ||
|
|
09828ab1cc | ||
|
|
c39748b1a0 | ||
|
|
829ef6b384 | ||
|
|
a94e1e39c8 | ||
|
|
9bea8cc63f | ||
|
|
ada9946eea | ||
|
|
fa960580f6
|
||
|
|
596bee88cc | ||
|
|
df764362b8 | ||
|
|
07b4a973de
|
||
|
|
e36afde222 | ||
|
|
69836fc62f
|
||
|
|
3cb3659a41 | ||
|
|
b30ff4e916
|
||
|
|
d558c1378d
|
||
|
|
f1c5f09b4a | ||
|
|
c9a56b0d9f
|
||
|
|
3bab5596ab
|
||
|
|
ae2498868d
|
||
|
|
ea21505e96
|
||
|
|
1b37e5efa9 | ||
|
|
ba20307fa0
|
||
|
|
81a03eab2d | ||
|
|
ab9cf837df
|
||
|
|
5c9307015c
|
||
|
|
55c150136b | ||
|
|
d644995c85
|
||
|
|
6f95554362
|
||
|
|
ba38367244 | ||
|
|
c623b3bc87
|
||
|
|
17f605261a | ||
|
|
0db8ab0bfa
|
||
|
|
68a2b9de0c | ||
|
|
af42c7613f
|
||
|
|
f5170ed130
|
||
|
|
77fc832f9e
|
||
|
|
627ad7ae34
|
||
|
|
03f67def30
|
||
|
|
cc64297d83
|
||
|
|
1d7c1a5ebf
|
||
|
|
960538ebc0 | ||
|
|
67eca54900
|
||
|
|
b4b59cee6e | ||
|
|
28fdb2a781 | ||
|
|
524adff0d5
|
||
|
|
893bbf26df
|
||
|
|
1df522097f
|
||
|
|
f5a56eaf7e
|
||
|
|
9b475797a0
|
||
|
|
5283503e89
|
||
|
|
bb9e083539
|
||
|
|
d5003ebdd5
|
||
|
|
9a63173efe | ||
|
|
93e0fd7303
|
||
|
|
b24b74e30d | ||
|
|
ea028dc86f
|
||
|
|
3e5b097667
|
||
|
|
98229202f3
|
||
|
|
9df3324687
|
||
|
|
ae6649f8e1 | ||
|
|
a504a21718
|
||
|
|
9aff198a60 | ||
|
|
2d125e4f90
|
||
|
|
ec974fa121
|
||
|
|
4f1641cb72
|
||
|
|
154be4f827
|
||
|
|
04fc4d7692
|
||
|
|
81fa126305
|
||
|
|
1fc984a3c6
|
||
|
|
a5fb8dd8d5
|
||
|
|
692d0cb166 | ||
|
|
e9208fc34c | ||
|
|
2cbfb6d962 | ||
|
|
b2a41b3eb9
|
||
|
|
35d937ed30 | ||
|
|
a00473c305
|
||
|
|
e76f0325a5
|
||
|
|
de1d4a5295
|
||
|
|
42ee6b87ce
|
||
|
|
a73e4e245c
|
||
|
|
e0ca5b1bb4
|
||
|
|
c9773cf45f
|
||
|
|
15cdffe7f3
|
||
|
|
41462cc4f6
|
||
|
|
039fa56a0b
|
||
|
|
f93a1d3c8f
|
||
|
|
600af361f4
|
||
|
|
0969231c95
|
||
|
|
99170dbff8
|
||
|
|
74cf6e88d7
|
||
|
|
0f6c37c998 | ||
|
|
4306da9cdd
|
||
|
|
abfbbb2e55 | ||
|
|
4139316789
|
||
|
|
aa0bc65774
|
||
|
|
86b54181f7 | ||
|
|
b32141cf65
|
||
|
|
16699aedfa | ||
|
|
e0ff899cb6
|
||
|
|
e807d30209
|
||
|
|
11d0f34639
|
||
|
|
45928e8abf
|
||
|
|
1a381c2a68
|
||
|
|
ee3682e859 | ||
|
|
62ec386035
|
||
|
|
427a037d53 | ||
|
|
2381579a52
|
||
|
|
112bbc557a | ||
|
|
463e9cc8e9
|
||
|
|
20fd1fa3d3 | ||
|
|
4f685d200b
|
||
|
|
9fc9cab48f
|
||
|
|
aafeabfb33
|
||
|
|
4663557ca4 | ||
|
|
3cf64ce24c
|
||
|
|
b19d4c09b4
|
||
|
|
caa89dc24f | ||
|
|
5f3b7c697d | ||
|
|
bebb4f2444
|
||
|
|
5afd785a22
|
||
|
|
2d6a33ea9f
|
||
|
|
70594c40ba | ||
|
|
9f3c48f4c7 | ||
|
|
38d2c62f3a
|
||
|
|
3ab4815526 | ||
|
|
f0b4c775f0 | ||
|
|
f29bedd437
|
||
|
|
336affb9e2
|
||
|
|
92b165eb9c
|
||
|
|
42fa4c3c5f
|
||
|
|
72872f5ccd
|
||
|
|
b89029c0ff
|
||
|
|
bb81e3c9a4
|
||
|
|
413c736050
|
||
|
|
c07fba1d4b | ||
|
|
8437aeb0eb
|
||
|
|
e39d4b847b | ||
|
|
9b596289af
|
||
|
|
e434716cbd
|
||
|
|
dcc3a47522 | ||
|
|
90fc22327c
|
||
|
|
39b8f8b7ae | ||
|
|
8f90df5ae9
|
||
|
|
e8bcb0bbe3
|
||
|
|
28b2de402c | ||
|
|
acd324e410 | ||
|
|
4c9d0ab9e1 | ||
|
|
cd2754b93c | ||
|
|
be72703bd7 | ||
|
|
219db989ed | ||
|
|
4cfb03c6b7 | ||
|
|
496cb0a14c | ||
|
|
996b0271de
|
||
|
|
2580cf95a6
|
||
|
|
a07213f633
|
||
|
|
17203f9876
|
||
|
|
e30c23dcbf
|
||
|
|
3fb7002a52
|
||
|
|
05ceeb1925
|
||
|
|
305d85bf61
|
||
|
|
4bd2c95d01
|
||
|
|
863099b95f
|
||
|
|
9d57861672
|
||
|
|
ce8da2d2a4
|
||
|
|
4c40679023
|
||
|
|
40cc60b1d4 | ||
|
|
292a2a9aea
|
||
|
|
771e46ecc3
|
||
|
|
51775ab0cb
|
||
|
|
4b8226a4cc
|
||
|
|
db941a8cc5 | ||
|
|
65db5355af
|
||
|
|
085430e17f
|
||
|
|
daf4005186
|
||
|
|
de51fcbf83
|
||
|
|
a521be57cc | ||
|
|
4e6473aae2
|
||
|
|
34efebb434
|
||
|
|
439c4b00e4
|
||
|
|
648d2d8bf8
|
||
|
|
f95baaf8c7
|
||
|
|
e78537ad36
|
||
|
|
8478404802
|
||
|
|
4ba1bb4cae | ||
|
|
c50675109a
|
||
|
|
ab9245e66c | ||
|
|
be2d0fc66e | ||
|
|
652ade887f | ||
|
|
3330f1bb63
|
||
|
|
1bfebd09fb
|
||
|
|
fb8b4efa6a
|
||
|
|
d3629d72b1
|
||
|
|
48e81b4d12 | ||
|
|
94494fc34f
|
||
|
|
2bdd7d980b
|
||
|
|
ff1ea58cf1
|
||
|
|
58cca84fda
|
||
|
|
91b335a42d
|
||
|
|
617bd1962d
|
||
|
|
cfba697339
|
||
|
|
3db5a0507d | ||
|
|
36c07c7aa7 | ||
|
|
2ba63346f0
|
||
|
|
a1c4a7de1a | ||
|
|
4d4ba49e8f | ||
|
|
4f2cf6eb64
|
||
|
|
4ad866286b
|
||
|
|
dc6b1e2373
|
||
|
|
96b789088e
|
||
|
|
a75fd8b5f6
|
||
|
|
635def9ba8
|
||
|
|
54a2db37b7
|
||
|
|
3dfc93612f
|
||
|
|
1e82e8696d
|
||
|
|
e2838496b6
|
||
|
|
05be095a8d
|
||
|
|
3fcefdb6b4
|
||
|
|
a0a26b547b
|
||
|
|
8408f6c445
|
||
|
|
ebcd8aec30
|
||
|
|
ed0f056c85
|
||
|
|
acfd28eea1
|
||
|
|
365f57c21a
|
||
|
|
b0e30cfe36
|
||
|
|
9aacbf0052
|
||
|
|
3e140cb6c6
|
||
|
|
d7266d2951
|
||
|
|
b2a9e28ce0
|
||
|
|
c6f0fc87f6
|
||
|
|
f1bff85157
|
||
|
|
33f67fa8a8
|
||
|
|
69aef5f05c
|
||
|
|
d8bc9282db
|
||
|
|
67cf8ed57e
|
||
|
|
7c755d5741
|
||
|
|
38204cdacc
|
||
|
|
622ae28ceb
|
||
|
|
9d2ece4228
|
||
|
|
1bb7db1924
|
||
|
|
cce505e5a5
|
||
|
|
2625355d88
|
||
|
|
6fa2ccf301
|
||
|
|
ac5aa368d7
|
||
|
|
3509ff8452
|
||
|
|
46436866be
|
||
|
|
1a7b8e371f
|
||
|
|
d011c546c0
|
||
|
|
236fa03923
|
||
|
|
68eb31d18d | ||
|
|
7a75f5762c |
No files matched your search
@@ -1,80 +1,14 @@
|
||||
version: 2.1
|
||||
|
||||
setup: true
|
||||
orbs:
|
||||
qqq-orb: kingsrook/qqq-orb@0.6
|
||||
|
||||
# ─── Jobs ────────────────────────────────────────────────────────────────────
|
||||
# The dashboard jar (static export under next-dashboard/) is published to Maven
|
||||
# Central on release tags, with the same orb commands and credentials as the
|
||||
# Material Dashboard. QApplicationJavalinServer serves it at / when it is present.
|
||||
|
||||
commands:
|
||||
configure-package-install:
|
||||
steps:
|
||||
- run:
|
||||
name: Keep package installation unattended
|
||||
command: |
|
||||
# These build machines are discarded after the job; avoid the interactive needrestart prompt.
|
||||
sudo sed -i 's|http://security.ubuntu.com/ubuntu|https://archive.ubuntu.com/ubuntu|g' /etc/apt/sources.list
|
||||
sudo install -d /etc/needrestart/conf.d
|
||||
printf '%s\n' '$nrconf{restart} = "l";' | sudo tee /etc/needrestart/conf.d/99-qqq-ci.conf >/dev/null
|
||||
|
||||
jobs:
|
||||
publish_dashboard_jar:
|
||||
executor: qqq-orb/java
|
||||
steps:
|
||||
- configure-package-install
|
||||
- checkout
|
||||
- qqq-orb/install_java:
|
||||
install_node: true
|
||||
node_version: "22"
|
||||
- run:
|
||||
name: Verify the immutable release version
|
||||
command: |
|
||||
release_version="${CIRCLE_TAG#v}"
|
||||
frontend_version=$(node -p "require('./package.json').version")
|
||||
maven_version=$(mvn -B -q help:evaluate -Dexpression=project.version -DforceStdout)
|
||||
test "$frontend_version" = "$release_version"
|
||||
test "$maven_version" = "$release_version"
|
||||
- run:
|
||||
name: Build the static export
|
||||
command: |
|
||||
corepack enable
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm typecheck
|
||||
pnpm lint
|
||||
pnpm test
|
||||
pnpm build:export
|
||||
- qqq-orb/mvn_jar_deploy
|
||||
|
||||
# ─── Workflows ───────────────────────────────────────────────────────────────
|
||||
# Trunk-based: main is the only long-lived branch (see CONTRIBUTING.md).
|
||||
# test_only → every branch except main (the PR check ci/circleci: qqq-orb/node_app_test_only)
|
||||
# publish_release → main builds; v* version tags publish the dashboard jar
|
||||
|
||||
qqq-orb: kingsrook/qqq-orb@0.8
|
||||
workflows:
|
||||
|
||||
test_only:
|
||||
setup:
|
||||
jobs:
|
||||
- qqq-orb/node_app_test_only:
|
||||
filters:
|
||||
branches:
|
||||
ignore: main
|
||||
tags:
|
||||
only: []
|
||||
|
||||
publish_release:
|
||||
jobs:
|
||||
- qqq-orb/node_app_build:
|
||||
filters:
|
||||
branches:
|
||||
only: [ main ]
|
||||
tags:
|
||||
only: [ /v.*/ ]
|
||||
- publish_dashboard_jar:
|
||||
context: [qqq-maven-registry-credentials]
|
||||
filters:
|
||||
branches:
|
||||
ignore: /.*/
|
||||
tags:
|
||||
only: /^v[0-9]+\.[0-9]+\.[0-9]+(-RC\.[0-9]+)?$/
|
||||
- qqq-orb/generate_pipeline:
|
||||
orb_version: '0.8'
|
||||
filters:
|
||||
branches:
|
||||
ignore: /.*/
|
||||
tags:
|
||||
only: /.*/
|
||||
@@ -6,3 +6,5 @@ playwright-report
|
||||
coverage
|
||||
.env*
|
||||
*.log
|
||||
|
||||
.pnpm-store
|
||||
@@ -0,0 +1 @@
|
||||
dynamic-components/** -text
|
||||
@@ -0,0 +1,13 @@
|
||||
# Copyright 2026 QRun.IO, Inc.
|
||||
# Licensed under the Apache License, Version 2.0.
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
name: build
|
||||
on:
|
||||
push:
|
||||
jobs:
|
||||
maven:
|
||||
uses: self:KofTwentyTwo/ci-workflows/.gitea/workflows/maven.yaml@v1
|
||||
secrets: inherit
|
||||
with:
|
||||
node-version: '22.20.0'
|
||||
prebuild: corepack enable && pnpm install --frozen-lockfile && pnpm typecheck && pnpm lint && pnpm test && pnpm build:export
|
||||
@@ -1,281 +0,0 @@
|
||||
name: Real-backend acceptance
|
||||
|
||||
# One production build feeds independent, serial-worker backends. Require "acceptance gate".
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, labeled, unlabeled]
|
||||
push:
|
||||
branches: [main]
|
||||
tags: ['v*']
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
qqq_ref:
|
||||
description: QRun-IO/qqq branch or tag to build the sample backend from
|
||||
required: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: acceptance-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
QQQ_REF: ${{ inputs.qqq_ref || vars.QQQ_ACCEPTANCE_REF || 'develop' }}
|
||||
NEXT_TELEMETRY_DISABLED: '1'
|
||||
MAVEN_ARGS: -Dmaven.wagon.http.retryHandler.count=5 -Daether.transport.http.retryHandler.count=5 -Daether.connector.http.retryHandler.count=5
|
||||
|
||||
jobs:
|
||||
plan:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 5
|
||||
outputs:
|
||||
mode: ${{ steps.select.outputs.mode }}
|
||||
matrix: ${{ steps.select.outputs.matrix }}
|
||||
specs: ${{ steps.select.outputs.specs }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Select projects, shards and changed areas
|
||||
id: select
|
||||
run: node scripts/acceptance-select.mjs
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
repository: QRun-IO/qqq
|
||||
ref: ${{ env.QQQ_REF }}
|
||||
path: qqq-backend
|
||||
- name: Resolve QQQ source revision
|
||||
id: qqq
|
||||
run: echo "sha=$(git -C qqq-backend rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
||||
- uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: '21'
|
||||
cache: maven
|
||||
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
id: qqq-cache
|
||||
with:
|
||||
path: ~/.m2/repository/com/kingsrook
|
||||
key: qqq-source-${{ runner.os }}-java21-v1-${{ steps.qqq.outputs.sha }}
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: pnpm
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Bundled UI API document matches the server's (src/lib/api/openapi/qqq-v1.json)
|
||||
run: node scripts/check-ui-api-spec.mjs --server-spec qqq-backend/qqq-middleware-javalin/src/main/resources/openapi/v1/openapi.yaml
|
||||
- name: Build QQQ from source
|
||||
if: steps.qqq-cache.outputs.cache-hit != 'true'
|
||||
run: bash scripts/ci/retry.sh 4 mvn -B -q -f qqq-backend/pom.xml install -DskipTests -Dspotbugs.skip=true -Dpmd.skip=true -Dcheckstyle.skip=true -Djacoco.skip=true
|
||||
- name: Build and install the dashboard jar (static export)
|
||||
run: |
|
||||
# Equivalent to build:jar, with retries around its Maven invocation too.
|
||||
pnpm build:export
|
||||
bash scripts/ci/retry.sh 4 mvn -B -q clean package
|
||||
bash scripts/ci/retry.sh 4 mvn -B -q install
|
||||
# Failed-attempt Maven stdout must not become part of the version value.
|
||||
bash scripts/ci/retry.sh 4 mvn -B -q help:evaluate -Dexpression=project.version -Doutput="$RUNNER_TEMP/acceptance-next-version"
|
||||
next_version=$(cat "$RUNNER_TEMP/acceptance-next-version")
|
||||
echo "NEXT_JAR_VERSION=$next_version" >> "$GITHUB_ENV"
|
||||
- name: Build the sample application jar
|
||||
working-directory: qqq-backend
|
||||
run: |
|
||||
# QQQ checkstyle/pmd paths are relative to this checkout.
|
||||
bash "$GITHUB_WORKSPACE/scripts/ci/retry.sh" 4 mvn -B -q -f qqq-sample-project/pom.xml package -DskipTests -Djacoco.skip=true \
|
||||
-Dqqq.frontend.next.version="$NEXT_JAR_VERSION"
|
||||
- name: Stage runtime artifacts
|
||||
run: |
|
||||
sample_jars=(qqq-backend/qqq-sample-project/target/*-jar-with-dependencies.jar)
|
||||
test "${#sample_jars[@]}" -eq 1
|
||||
sample_version="${sample_jars[0]##*/}"
|
||||
sample_version="${sample_version#qqq-sample-project-}"
|
||||
sample_version="${sample_version%-jar-with-dependencies.jar}"
|
||||
mkdir -p test-results/acceptance-jars
|
||||
cp "${sample_jars[0]}" test-results/acceptance-jars/
|
||||
# javac and the security fixture need the API JAR in addition to the fat sample JAR.
|
||||
cp "$HOME/.m2/repository/com/kingsrook/qqq/qqq-middleware-api/$sample_version/qqq-middleware-api-$sample_version.jar" \
|
||||
test-results/acceptance-jars/qqq-middleware-api.jar
|
||||
tar -cf test-results/acceptance-build.tar out test-results/acceptance-jars
|
||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: acceptance-build
|
||||
path: test-results/acceptance-build.tar
|
||||
if-no-files-found: error
|
||||
retention-days: 30
|
||||
|
||||
shards:
|
||||
name: acceptance ${{ matrix.project }} ${{ matrix.shard }}/${{ matrix.total }}
|
||||
needs: [plan, build]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJSON(needs.plan.outputs.matrix) }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 40
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: '21'
|
||||
cache: maven
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- name: Installed Playwright version
|
||||
id: playwright
|
||||
run: echo "version=$(node -p "require('@playwright/test/package.json').version")" >> "$GITHUB_OUTPUT"
|
||||
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
id: browser-cache
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: playwright-${{ runner.os }}-${{ steps.playwright.outputs.version }}-${{ matrix.browser }}
|
||||
- run: pnpm exec playwright install-deps ${{ matrix.browser }}
|
||||
- if: steps.browser-cache.outputs.cache-hit != 'true'
|
||||
run: pnpm exec playwright install ${{ matrix.browser }}
|
||||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: acceptance-build
|
||||
path: test-results
|
||||
- name: Restore runtime artifacts
|
||||
run: |
|
||||
tar -xf test-results/acceptance-build.tar
|
||||
echo "QQQ_SAMPLE_JAR=$(ls "$PWD"/test-results/acceptance-jars/*-jar-with-dependencies.jar)" >> "$GITHUB_ENV"
|
||||
echo "QQQ_MIDDLEWARE_API_JAR=$PWD/test-results/acceptance-jars/qqq-middleware-api.jar" >> "$GITHUB_ENV"
|
||||
- name: Run shard
|
||||
env:
|
||||
QQQ_ACCEPTANCE_BROWSERS: ${{ matrix.project }}
|
||||
QQQ_ACCEPTANCE_SPEC_PATHS: ${{ needs.plan.outputs.specs }}
|
||||
QQQ_ACCEPTANCE_BLOB: '1'
|
||||
# Environment-selected projects must not produce colliding default blob names.
|
||||
QQQ_ACCEPTANCE_BLOB_NAME: report-${{ matrix.project }}-${{ matrix.shard }}.zip
|
||||
QQQ_ACCEPTANCE_TRANSPORT_TRACE: '1'
|
||||
QQQ_ACCEPTANCE_BROWSER_TRACE: ${{ (matrix.project == 'webkit' || matrix.project == 'mobile' || matrix.project == 'tablet') && 'navigation' || '0' }}
|
||||
DEBUG: pw:browser
|
||||
run: node scripts/acceptance.mjs --skip-build --shard=${{ matrix.shard }}/${{ matrix.total }}
|
||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
if: always()
|
||||
with:
|
||||
name: acceptance-blob-${{ matrix.project }}-${{ matrix.shard }}
|
||||
path: test-results/acceptance/blob-report/
|
||||
if-no-files-found: error
|
||||
retention-days: 30
|
||||
|
||||
gate:
|
||||
name: acceptance gate
|
||||
if: always()
|
||||
needs: [plan, build, shards]
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
pattern: acceptance-blob-*
|
||||
path: test-results/acceptance/blobs
|
||||
merge-multiple: true
|
||||
- name: Merge shard reports
|
||||
env:
|
||||
PLAYWRIGHT_JSON_OUTPUT_NAME: test-results/acceptance/report.json
|
||||
PLAYWRIGHT_HTML_OUTPUT_DIR: test-results/acceptance/html
|
||||
PLAYWRIGHT_HTML_OPEN: never
|
||||
run: npx playwright merge-reports --reporter=json,html test-results/acceptance/blobs
|
||||
- name: Evaluate acceptance matrix
|
||||
if: always()
|
||||
env:
|
||||
ACCEPTANCE_MODE: ${{ needs.plan.outputs.mode }}
|
||||
run: |
|
||||
args=()
|
||||
if [[ "$ACCEPTANCE_MODE" == pr ]]; then args+=(--partial); fi
|
||||
node scripts/acceptance-gate.mjs "${args[@]}"
|
||||
- name: Require every planned shard and successful build
|
||||
if: always()
|
||||
env:
|
||||
PLAN_RESULT: ${{ needs.plan.result }}
|
||||
BUILD_RESULT: ${{ needs.build.result }}
|
||||
SHARDS_RESULT: ${{ needs.shards.result }}
|
||||
PLANNED_MATRIX: ${{ needs.plan.outputs.matrix }}
|
||||
run: |
|
||||
test "$PLAN_RESULT" = success
|
||||
test "$BUILD_RESULT" = success
|
||||
test "$SHARDS_RESULT" = success
|
||||
node --input-type=module -e 'import { existsSync } from "node:fs"; for (const { project, shard } of JSON.parse(process.env.PLANNED_MATRIX).include) { const file = "test-results/acceptance/blobs/report-" + project + "-" + shard + ".zip"; if (!existsSync(file)) throw new Error("Missing shard report: " + file); }'
|
||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
if: always()
|
||||
with:
|
||||
name: acceptance
|
||||
path: test-results/acceptance/
|
||||
if-no-files-found: error
|
||||
retention-days: 30
|
||||
|
||||
quarantine:
|
||||
name: acceptance quarantine (chromium)
|
||||
needs: [plan, build]
|
||||
if: needs.plan.outputs.mode == 'full'
|
||||
continue-on-error: true
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: '21'
|
||||
cache: maven
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- name: Installed Playwright version
|
||||
id: playwright
|
||||
run: echo "version=$(node -p "require('@playwright/test/package.json').version")" >> "$GITHUB_OUTPUT"
|
||||
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
id: browser-cache
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: playwright-${{ runner.os }}-${{ steps.playwright.outputs.version }}-chromium
|
||||
- run: pnpm exec playwright install-deps chromium
|
||||
- if: steps.browser-cache.outputs.cache-hit != 'true'
|
||||
run: pnpm exec playwright install chromium
|
||||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: acceptance-build
|
||||
path: test-results
|
||||
- name: Restore runtime artifacts
|
||||
run: |
|
||||
tar -xf test-results/acceptance-build.tar
|
||||
echo "QQQ_SAMPLE_JAR=$(ls "$PWD"/test-results/acceptance-jars/*-jar-with-dependencies.jar)" >> "$GITHUB_ENV"
|
||||
echo "QQQ_MIDDLEWARE_API_JAR=$PWD/test-results/acceptance-jars/qqq-middleware-api.jar" >> "$GITHUB_ENV"
|
||||
- name: Run quarantined scenarios
|
||||
env:
|
||||
QQQ_ACCEPTANCE_BROWSERS: chromium
|
||||
QQQ_ACCEPTANCE_QUARANTINE: only
|
||||
QQQ_ACCEPTANCE_TRANSPORT_TRACE: '1'
|
||||
QQQ_ACCEPTANCE_BROWSER_TRACE: '0'
|
||||
DEBUG: pw:browser
|
||||
run: node scripts/acceptance.mjs --skip-build
|
||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
if: always()
|
||||
with:
|
||||
name: acceptance-quarantine
|
||||
path: test-results/acceptance/
|
||||
retention-days: 30
|
||||
@@ -1,158 +1,41 @@
|
||||
name: CI
|
||||
|
||||
# Fast PR checks (QRun-IO/qqq#709, bundle budget #710). The real-backend gate, including the
|
||||
# runtime performance budgets (specs/performance), runs in acceptance.yml.
|
||||
# Each job installs on its own so they run in parallel; none needs a backend.
|
||||
|
||||
on:
|
||||
name: Next centralized CI
|
||||
'on':
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- reopened
|
||||
- labeled
|
||||
- unlabeled
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- v*
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
qqq_ref:
|
||||
description: QQQ source ref for acceptance
|
||||
type: string
|
||||
required: false
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
NEXT_TELEMETRY_DISABLED: '1'
|
||||
STORYBOOK_DISABLE_TELEMETRY: '1'
|
||||
|
||||
jobs:
|
||||
unit:
|
||||
name: Typecheck, lint, unit tests and coverage
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: pnpm
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pnpm install --frozen-lockfile
|
||||
- run: pnpm typecheck
|
||||
- run: pnpm lint
|
||||
- name: Unit tests with src/lib coverage thresholds (vitest.config.ts)
|
||||
run: pnpm test:coverage
|
||||
|
||||
e2e:
|
||||
name: Mocked e2e (Playwright, chromium)
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: pnpm
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pnpm install --frozen-lockfile
|
||||
- name: Installed Playwright version
|
||||
id: playwright
|
||||
run: echo "version=$(node -p "require('@playwright/test/package.json').version")" >> "$GITHUB_OUTPUT"
|
||||
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
id: browser-cache
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: playwright-${{ runner.os }}-${{ steps.playwright.outputs.version }}-chromium
|
||||
- run: pnpm exec playwright install-deps chromium
|
||||
- if: steps.browser-cache.outputs.cache-hit != 'true'
|
||||
run: pnpm exec playwright install chromium
|
||||
- name: Production build
|
||||
run: pnpm build
|
||||
- name: Mocked e2e suite
|
||||
env:
|
||||
QQQ_E2E_SERVER: production
|
||||
run: pnpm test:e2e
|
||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
if: failure()
|
||||
with:
|
||||
name: e2e-report
|
||||
path: |
|
||||
playwright-report/
|
||||
test-results/
|
||||
retention-days: 14
|
||||
|
||||
collaboration-e2e:
|
||||
name: Collaboration e2e (MSW; chromium, firefox, webkit, mobile, tablet)
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: pnpm
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pnpm install --frozen-lockfile
|
||||
- name: Installed Playwright version
|
||||
id: playwright
|
||||
run: echo "version=$(node -p "require('@playwright/test/package.json').version")" >> "$GITHUB_OUTPUT"
|
||||
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
id: browser-cache
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: playwright-${{ runner.os }}-${{ steps.playwright.outputs.version }}-all
|
||||
- run: pnpm exec playwright install-deps chromium firefox webkit
|
||||
- if: steps.browser-cache.outputs.cache-hit != 'true'
|
||||
run: pnpm exec playwright install chromium firefox webkit
|
||||
- name: Collaboration suite (mock API, every browser project)
|
||||
run: pnpm exec playwright test --config playwright.collaboration.config.ts
|
||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
if: always()
|
||||
with:
|
||||
name: collaboration-e2e
|
||||
path: |
|
||||
test-results/collaboration/
|
||||
test-results/collab-shots/
|
||||
retention-days: 14
|
||||
|
||||
storybook:
|
||||
name: Storybook build
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: pnpm
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pnpm install --frozen-lockfile
|
||||
- run: pnpm build-storybook --quiet
|
||||
|
||||
performance:
|
||||
name: Bundle budget (static export)
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: pnpm
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pnpm install --frozen-lockfile
|
||||
- name: Static export
|
||||
run: pnpm build:export
|
||||
- name: First-load JS per route, total JS and largest chunk against perf-budget.json
|
||||
run: pnpm perf:budget
|
||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
if: always()
|
||||
with:
|
||||
name: bundle-budget
|
||||
path: test-results/performance/bundle.json
|
||||
retention-days: 30
|
||||
ci:
|
||||
if: (github.event_name == 'pull_request' && !contains(fromJSON('["labeled","unlabeled"]'),
|
||||
github.event.action)) || (github.event_name == 'push' && github.ref == 'refs/heads/main')
|
||||
uses: QRun-IO/qqq-orb/.github/workflows/next-ci.yml@0f33ddadf88b9a1b940b8883782fe2000ee1d2bd
|
||||
acceptance:
|
||||
if: github.event_name != 'pull_request' || !contains(fromJSON('["labeled","unlabeled"]'),
|
||||
github.event.action) || github.event.label.name == 'full-acceptance'
|
||||
uses: QRun-IO/qqq-orb/.github/workflows/next-acceptance.yml@0f33ddadf88b9a1b940b8883782fe2000ee1d2bd
|
||||
with:
|
||||
qqq_ref: ${{ inputs.qqq_ref || '' }}
|
||||
dashboard-image:
|
||||
if: (github.event_name == 'pull_request' && !contains(fromJSON('["labeled","unlabeled"]'),
|
||||
github.event.action)) || (github.event_name == 'push' && startsWith(github.ref,
|
||||
'refs/tags/v'))
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
uses: QRun-IO/qqq-orb/.github/workflows/next-dashboard-image.yml@0f33ddadf88b9a1b940b8883782fe2000ee1d2bd
|
||||
@@ -1,75 +0,0 @@
|
||||
name: Dashboard image
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
tags: ['v*']
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
IMAGE: ghcr.io/qrun-io/qqq-frontend-next
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- arch: amd64
|
||||
runner: ubuntu-24.04
|
||||
- arch: arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
runs-on: ${{ matrix.runner }}
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
|
||||
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4
|
||||
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4
|
||||
if: github.event_name == 'push'
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7
|
||||
with:
|
||||
context: .
|
||||
file: docker/quickstart/Dockerfile
|
||||
platforms: linux/${{ matrix.arch }}
|
||||
push: ${{ github.event_name == 'push' }}
|
||||
load: ${{ github.event_name == 'pull_request' }}
|
||||
tags: ${{ env.IMAGE }}:${{ github.sha }}-${{ matrix.arch }}
|
||||
cache-from: type=gha,scope=dashboard-${{ matrix.arch }}
|
||||
cache-to: type=gha,mode=max,scope=dashboard-${{ matrix.arch }}
|
||||
- name: Verify runnable dashboard
|
||||
if: github.event_name == 'pull_request'
|
||||
env:
|
||||
TAG: ${{ env.IMAGE }}:${{ github.sha }}-${{ matrix.arch }}
|
||||
run: |
|
||||
container=$(docker run -d --rm -p 127.0.0.1:3000:3000 "$TAG")
|
||||
trap 'docker rm -f "$container"' EXIT
|
||||
curl -fsS --max-time 3 --retry 30 --retry-delay 1 --retry-all-errors \
|
||||
--retry-max-time 90 http://127.0.0.1:3000/app/person >/dev/null \
|
||||
|| { docker logs "$container"; exit 1; }
|
||||
|
||||
publish:
|
||||
if: github.event_name == 'push'
|
||||
needs: build
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
packages: write
|
||||
steps:
|
||||
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4
|
||||
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Publish versioned multi-architecture image
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
--tag "$IMAGE:${GITHUB_REF_NAME#v}" \
|
||||
"$IMAGE:$GITHUB_SHA-amd64" "$IMAGE:$GITHUB_SHA-arm64"
|
||||
@@ -10,6 +10,8 @@
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/.next-collaboration/
|
||||
/.next-presence-live/
|
||||
/out/
|
||||
|
||||
# production
|
||||
@@ -41,3 +43,6 @@ target/
|
||||
/log/
|
||||
/.playwright-mcp/
|
||||
/storybook-static/
|
||||
/public/scripting-generated/
|
||||
/public/pdf-generated/
|
||||
/public/dynamic-qfmd-components/
|
||||
@@ -0,0 +1,5 @@
|
||||
|
||||
|
||||
# Reviewed findings committed before the Gitea migration; exact historical fingerprints only.
|
||||
0969231c95f876bb0a41529e8c6c4621a0257aee:src/lib/qa-capture/sanitizers.test.ts:jwt:33
|
||||
23a855297de0f84b564b20d782e01180a68b5257:tests/acceptance/specs/security/restrictions.spec.ts:generic-api-key:248
|
||||
@@ -0,0 +1,3 @@
|
||||
profile: frontend-next
|
||||
orb_version: '0.8'
|
||||
default_branch: main
|
||||
@@ -0,0 +1,9 @@
|
||||
<!-- BEGIN:nextjs-agent-rules -->
|
||||
|
||||
## This is NOT the Next.js you know
|
||||
|
||||
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices.
|
||||
|
||||
This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.
|
||||
|
||||
<!-- END:nextjs-agent-rules -->
|
||||
@@ -1,8 +1,11 @@
|
||||
# QQQ Frontend Next
|
||||
|
||||
> **QBit integration branch (2026-11-04).** File delivery/preview, TypeScript editing and generic process controls use the QQQ UIUX APIs. See [current verification and dependency setup](docs/QBIT-RELEASE-STATUS.md) and [remaining release requirements](docs/QBIT-KNOWN-ISSUES.md). These changes are source/local snapshots, not part of the published RC.36 baseline described below.
|
||||
|
||||
|
||||
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.19**, which adds the related-record picker (single, multi-child and link modes) and the assistant Read-only / Write switch to RC18. Confirm availability in the [RC19 GitHub release](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.19) before installing; see the [RC19 notes](docs/releases/1.0.0-RC.19.md) for the preparation checkpoint and verification scope. [RC18](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.18) was the previously verified public candidate at that checkpoint and remains immutable. Final 1.0 remains held for application soak and explicit owner approval.
|
||||
**Published baseline: Next UI 1.0.0-RC.36**, which adds accessibility fixes: readable link text, 24px clear targets, h2 widget titles and skip-link focus to RC35. Confirm availability in the [RC36 GitHub release](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.36) before installing; see the [RC36 notes](docs/releases/1.0.0-RC.36.md) for the preparation checkpoint and verification scope. [RC35](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.35) 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 +17,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.21</version>
|
||||
<version>1.0.0-RC.36</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
The explicit version overrides an older BOM-managed frontend. RC21 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); **4.1.0-RC.4 or later** for record developer panels (QQQ #1076); **4.1.0-RC.5 or later** for the record picker (QQQ #1099), form validation limits (QQQ #1100) and date filter presets (QQQ #1115). The Alerts bell appears only with qbit-alerts (0.1.0-RC.1 or later); the notification center needs qbit-alerts 0.1.0-RC.2 or later. The assistant panel appears only with the qbit-assistant backend (0.1.0-RC.3 or later). 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 explicit version overrides an older BOM-managed frontend. RC36 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); **4.1.0-RC.4 or later** for record developer panels (QQQ #1076); **4.1.0-RC.5 or later** for the record picker (QQQ #1099), form validation limits (QQQ #1100) and date filter presets (QQQ #1115). The Alerts bell appears only with qbit-alerts (0.1.0-RC.1 or later); the notification center needs qbit-alerts 0.1.0-RC.2 or later. The assistant panel appears only with the qbit-assistant backend (0.1.0-RC.3 or later). The SQL Workbench needs the qbit-sql-workbench QBit (1.0.0-RC.1 or later); QA capture appears only with qbit-qa-capture (the `qa-capture` UI API extension); 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>`.
|
||||
|
||||
@@ -63,6 +66,18 @@ To upgrade between RCs, change only the `qqq-frontend-next` version and check th
|
||||
|
||||
Served from the jar there is nothing to configure in the frontend: it calls the API on the same origin at `/qqq/v1`, and the Javalin server sends the security headers and Content-Security-Policy. External embeds (for example QuickSight frames) need an allowed CSP source, not a disabled policy. The build-time variables under [Environment Variables](#environment-variables) apply only to frontend development and the standalone container image.
|
||||
|
||||
### App home navigation grids
|
||||
|
||||
App homes show automatic section ("Actions", "Reports", "Data") and child-app ("Apps")
|
||||
card grids. When the app declares one or more widgets, the "Apps" grid is left out (even if
|
||||
a declared widget is unavailable to the current user), so widget-based navigation is not
|
||||
duplicated; the section grids stay.
|
||||
|
||||
Set `supplementalAppMetaData.nextUi.showChildGrid` on the app to override this default:
|
||||
`true` shows both grids alongside widgets; `false` hides both grids, for a home whose
|
||||
widgets already cover its sections and child apps.
|
||||
Only boolean values override the default. Existing visibility and permission rules still apply.
|
||||
|
||||
### Known limitations
|
||||
|
||||
- Release candidates only; final 1.0 is held for application soak and owner approval ([#712](https://github.com/QRun-IO/qqq/issues/712), tracker [#713](https://github.com/QRun-IO/qqq/issues/713)).
|
||||
@@ -178,3 +193,35 @@ src/
|
||||
- **Charts:** Recharts
|
||||
- **Testing:** Vitest (unit), Playwright (E2E), axe-core (a11y)
|
||||
- **Package Manager:** pnpm
|
||||
|
||||
## Default query columns
|
||||
|
||||
A new query view shows at most six fields: the visible primary key first, then
|
||||
eligible fields from T1 sections followed by T2 sections, preserving section and
|
||||
field order within each tier. Tables without sections use metadata field order.
|
||||
Hidden fields, heavy fields, TEXT/HTML/BLOB/JSON-like types, and CODE_EDITOR or
|
||||
RENDER_HTML adornments are excluded from the automatic selection (the primary key
|
||||
is retained unless hidden). Fields outside T1/T2 are initially unchecked.
|
||||
Other available columns remain selectable in the column picker.
|
||||
|
||||
Applications can override this selection with a string-array hint:
|
||||
|
||||
```json
|
||||
{
|
||||
"supplementalMetaData": {
|
||||
"nextUi": {
|
||||
"defaultColumns": ["id", "title", "status"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The legacy `supplementalTableMetaData.nextUi.defaultColumns` path is also accepted;
|
||||
the modern `supplementalMetaData.nextUi` object takes precedence. A valid hint
|
||||
selects exactly the named available columns, without the six-field cap or automatic
|
||||
primary-key inclusion. Unknown or unavailable fields are ignored; an empty array
|
||||
selects no columns, and malformed hints fall back to the automatic selection.
|
||||
Saved views and legacy stored visibility (including `{}`, meaning all base columns)
|
||||
are preserved. New View and Reset All Changes on an unsaved view restore these
|
||||
metadata defaults. Quick search searches only visible string fields. Responsive
|
||||
card/grid behavior is unchanged.
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM registry-1.docker.io/library/node:22-bookworm-slim AS frontend
|
||||
FROM registry-1.docker.io/library/node:22-alpine@sha256:fe4f6d74e20370acb04ad889077d14d0f64a9c8ea6a42bc60cddf1e844699914 AS frontend
|
||||
WORKDIR /app
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
RUN corepack enable && corepack prepare pnpm@9.15.9 --activate
|
||||
@@ -9,8 +9,12 @@ ENV QQQ_BACKEND_URL=http://host.docker.internal:8000
|
||||
ENV NEXT_PUBLIC_MOCK_API=false
|
||||
RUN pnpm build
|
||||
|
||||
FROM registry-1.docker.io/library/node:22-bookworm-slim AS runtime
|
||||
FROM registry-1.docker.io/library/node:22-alpine@sha256:fe4f6d74e20370acb04ad889077d14d0f64a9c8ea6a42bc60cddf1e844699914 AS runtime
|
||||
LABEL org.opencontainers.image.source="https://github.com/QRun-IO/qqq-frontend-next"
|
||||
# Standalone production starts node directly; package managers are build-only.
|
||||
RUN rm -rf /usr/local/lib/node_modules/npm /usr/local/lib/node_modules/corepack /opt/yarn-* \
|
||||
&& rm -f /usr/local/bin/npm /usr/local/bin/npx /usr/local/bin/corepack \
|
||||
/usr/local/bin/pnpm /usr/local/bin/pnpx /usr/local/bin/yarn /usr/local/bin/yarnpkg
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production NEXT_TELEMETRY_DISABLED=1 HOSTNAME=0.0.0.0 PORT=3000
|
||||
COPY --from=frontend --chown=node:node /app/.next/standalone ./
|
||||
|
||||
@@ -23,10 +23,12 @@ Axios interceptor
|
||||
▼
|
||||
TanStack Query cache (onError)
|
||||
│ 401 → silently ignored (already handled)
|
||||
│ 403 → toast "Permission denied" / "You do not have permission…"
|
||||
│ 403 → toast the server's message, or "Permission denied"
|
||||
│ 404 → toast "Resource not found"
|
||||
│ 5xx → toast "Server error — retrying…" / "Server error — please try again"
|
||||
│ network → toast "Something went wrong"
|
||||
│ other status → toast the server's message, or "Request failed (HTTP 409)"
|
||||
│ no response → toast "No response from the server" / "The server took too long to respond"
|
||||
│ every toast names the request, and a repeat of the same failure updates it
|
||||
▼
|
||||
Component (optional per-query onError)
|
||||
│ Only needed when generic toast is wrong for the specific UX
|
||||
@@ -66,52 +68,38 @@ The `unauthorizedCallback` is wired to the auth provider's logout/redirect funct
|
||||
|
||||
**Location:** `src/lib/query-client.ts` — `handleQueryError` function
|
||||
|
||||
```typescript
|
||||
function handleQueryError(error: unknown, context: 'query' | 'mutation'): void {
|
||||
const status = getErrorStatusCode(error)
|
||||
It is registered on both `QueryCache.onError` and `MutationCache.onError`. A query or mutation whose
|
||||
component renders its own error opts out with `meta: HANDLES_OWN_ERRORS`.
|
||||
|
||||
if (status === 401) return // handled by axios interceptor
|
||||
| Failure | Banner text |
|
||||
|---|---|
|
||||
| Cancelled request (an axios cancel or an aborted `fetch`), 401 | none |
|
||||
| 403 | the server's message (of a JSON body), or `Permission denied` |
|
||||
| 404 | `Resource not found` |
|
||||
| 5xx | `Server error — retrying…` (query), `Server error — please try again` (mutation) |
|
||||
| Any other status (400, 409, 422, …) | the server's message (`userFacingError`, `error` or `message` of a JSON body), or `Request failed (HTTP 409)` |
|
||||
| No response (connection failed) | `No response from the server` |
|
||||
| No response (the 30 s timeout) | `The server took too long to respond` |
|
||||
| An error the application raised (`throw new Error('Invalid audit response')`) | that sentence |
|
||||
| A runtime error (`TypeError`, a schema error, …) | `The page hit an unexpected error (TypeError)` |
|
||||
|
||||
if (status === 403) {
|
||||
toast.error(
|
||||
context === 'mutation'
|
||||
? 'You do not have permission to perform this action'
|
||||
: 'Permission denied'
|
||||
)
|
||||
return
|
||||
}
|
||||
Under the text, every banner names what failed: the request as `POST /qqq/v1/table/order/query (HTTP 409)`,
|
||||
or `While loading records` (the query key's namespace in plain words: `possibleValues` reads
|
||||
`possible values`) for a query that failed outside a request. The
|
||||
request is written without its query string, and every path segment that is not a plain word (a record
|
||||
id, a UUID, an encoded value) is shown as `{id}`. Nothing else of the request or response is shown.
|
||||
|
||||
if (status === 404) {
|
||||
toast.error('Resource not found')
|
||||
return
|
||||
}
|
||||
**One banner per failure.** The banner's sonner `id` is its text plus the request, so a failure that
|
||||
repeats (a polling query that fails on every attempt) updates the banner already on screen instead of
|
||||
adding one. A different failure gets its own banner. A lost connection is one cause whichever request
|
||||
met it, so it has a single banner, which names the request that failed most recently. Without the id
|
||||
every failed poll added a banner, and sonner pauses a toast's timer while the tab is hidden or the
|
||||
pointer rests on the stack, so the banners could pile up.
|
||||
|
||||
if (status !== undefined && status >= 500) {
|
||||
toast.error(
|
||||
context === 'mutation'
|
||||
? 'Server error — please try again'
|
||||
: 'Server error — retrying…'
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Network failure or unexpected status
|
||||
toast.error('Something went wrong')
|
||||
}
|
||||
```
|
||||
|
||||
This handler is registered on both `QueryCache` and `MutationCache`:
|
||||
|
||||
```typescript
|
||||
export const queryClient = new QueryClient({
|
||||
queryCache: new QueryCache({
|
||||
onError: (error) => handleQueryError(error, 'query'),
|
||||
}),
|
||||
mutationCache: new MutationCache({
|
||||
onError: (error) => handleQueryError(error, 'mutation'),
|
||||
}),
|
||||
})
|
||||
```
|
||||
**Console.** Each banner is also logged once with `console.warn('[API] Request failed:', { context,
|
||||
status, code, name, request, area })`: the HTTP status, the axios error code (`ERR_NETWORK`,
|
||||
`ECONNABORTED`, `ERR_BAD_REQUEST`), the error's name, the request as above and the query namespace.
|
||||
It never logs a message, body, header or query string, because those can carry record data.
|
||||
|
||||
**Implication for components:** For most queries and mutations, you do not need `onError` at all. The global handler covers toasting. Add per-query handling only when you need to do something the global handler cannot — such as mapping a 400 error's field messages back to form fields.
|
||||
|
||||
@@ -139,7 +127,7 @@ Returns `undefined` for anything else (non-object errors, null, network failures
|
||||
|
||||
### 400 — Validation Error
|
||||
|
||||
The global handler does NOT toast on 400 because 400s typically carry field-level validation messages that must be mapped back to individual form fields. Handle 400 explicitly wherever you submit a form.
|
||||
The global handler toasts the server's message for a 400 (or `Request failed (HTTP 400)` when it sent none). 400s typically carry field-level validation messages that belong beside individual form fields, so a form handles 400 explicitly, and sets `meta: HANDLES_OWN_ERRORS` on the mutation when the toast would repeat what the form shows.
|
||||
|
||||
```typescript
|
||||
import { getErrorStatusCode } from '@/lib/utils/error-utils'
|
||||
@@ -207,7 +195,7 @@ No additional per-component handling is needed for 5xx unless you want a custom
|
||||
|
||||
### Network Failure (no response)
|
||||
|
||||
Treated as non-4xx by the retry logic — queries retry up to 3 times. The global handler toasts "Something went wrong" after all retries are exhausted. No per-component handling needed for the common case.
|
||||
Treated as non-4xx by the retry logic — queries retry up to 3 times. After all retries are exhausted the global handler toasts "No response from the server" (or "The server took too long to respond" for the 30 s timeout) with the request that failed. No per-component handling needed for the common case.
|
||||
|
||||
---
|
||||
|
||||
@@ -274,8 +262,8 @@ export function EditOrderForm({ orderId, schema, defaultValues }: Props) {
|
||||
onError: (error) => {
|
||||
if (getErrorStatusCode(error) === 400) {
|
||||
// Map field errors from the server response body to form fields.
|
||||
// The global handler does NOT toast on 400, so we must do it here or
|
||||
// rely on inline field error messages in the form UI.
|
||||
// The global handler toasts the server's message for a 400; the form
|
||||
// places each field's message beside it.
|
||||
const body = (error as AxiosError<{ errors: Record<string, string> }>).response?.data
|
||||
if (body?.errors) {
|
||||
for (const [field, message] of Object.entries(body.errors)) {
|
||||
@@ -305,6 +293,6 @@ export function EditOrderForm({ orderId, schema, defaultValues }: Props) {
|
||||
| Layer | File | Handles |
|
||||
|---|---|---|
|
||||
| Axios interceptor | `src/lib/api/client.ts` | 401 → redirect to login |
|
||||
| Global query handler | `src/lib/query-client.ts` | 403, 404, 5xx, network → toast |
|
||||
| Global query handler | `src/lib/query-client.ts` | Every status but 401, no response, page errors → one toast per failure, naming the request |
|
||||
| Per-component `onError` | Your component | 400 field errors, custom empty states, navigation |
|
||||
| `getErrorStatusCode` | `src/lib/utils/error-utils.ts` | Normalize `AxiosError` and plain-object errors to HTTP status code |
|
||||
@@ -28,6 +28,7 @@ The ID may be on a layout-neutral wrapper around a native button or menu item. S
|
||||
- Record view: `.recordView`, `record-sidebar`, `record-view-header-{table}`, `record-view-title-{table}`, `record-view-button-bar-{table}`, `record-view-actions-menu`, and `delete-confirmation-*` with `button-delete-yes`/`button-delete-no`.
|
||||
- File previews: a `FILE_PREVIEW` value keeps its cell or value ID (`grid-cell-{field}`, `field-value-{field}`) on the thumbnail button with `data-preview-kind`; the box is `{id}-thumbnail`. The dialog uses `file-preview-dialog`, `file-preview-image`/`-pdf`/`-video`/`-audio`/`-unavailable`, `file-preview-details`, and `file-preview-{previous,next,zoom-in,zoom-out,zoom-fit,open,download,close}`.
|
||||
- Forms: `.entityForm`, `record-create-{table}`/`record-edit-{table}`, `form-section-{section}`, `.field-wrapper.is-visible`, and `.stickyBottomButtonBar`. Default action buttons expose `data-button-variant` values such as `gradient`, `contained`, and `outlined`.
|
||||
- Jobs: `button-jobs`, `jobs-active-badge`, `jobs-popover`, `link-job-{jobId}`, `link-all-jobs`, `jobs-page`, `tab-jobs-{active,recent,all}`, `job-progress-view`, `job-status`, `job-phases`, `job-progress-bar`, `job-progress-stats`, `job-progress-message`, `job-log-panel`, `job-log-scroll`, `button-job-log-{toggle,copy,latest}`, `job-background-countdown`, `button-job-{stay,background,cancel,retry,done}`, and `link-job-record`. Jobs motion is confined to `.qqq-job-{fill,shimmer,pulse,ring}` and is static under reduced motion.
|
||||
|
||||
The record view has a phone accordion and action sheet, a tablet tab list without the large-screen sidebar, and a large-screen tab list with the sidebar. A selector can be present in the DOM while its control is hidden at a narrower width. Test custom CSS at all three widths, including focus and contrast, before shipping it.
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# Help everywhere — implementation and verification
|
||||
|
||||
Backend: QRun-IO/qqq#1177 (`docs/HELP-CONTENT.md`).
|
||||
|
||||
## Implementation
|
||||
|
||||
- Vendored the supplied help-core `openapi.yaml` as the existing one-space-indented JSON document. The prior vendoring commits are `01dd7e9` and `8a0fbbd`; neither contains a generator script. Conversion uses the installed ESLint dependency `js-yaml`, then `JSON.stringify(document, null, 1)` with a final newline. The full core document is retained, including its other changes within UI API 1.2.0.
|
||||
- Added `summary` to `QHelpContent`, reused that type for widget slots, and added app/process/report slot maps. `src/lib/api/help.ts` uses the typed API client and checks the advertised `help` feature before calling search. No off-spec exception was added. Search and coverage are optional spec operations; coverage is not called.
|
||||
- `src/components/help/HelpIcon.tsx` selects role-specific help, shows the plain summary, and opens the shared long-help panel by click, Enter, or Space. Existing customization IDs are retained on the corresponding control or tooltip. Input descriptions remain available while the tooltip is closed.
|
||||
- `HelpPanelProvider` is mounted in the authenticated dashboard. One `next/dynamic` panel renders sanitized long content using the existing `HelpContent` renderer, with related help, safe navigation, Escape dismissal, focus trapping, and return focus. Help controls use theme tokens and coarse-pointer touch targets.
|
||||
- Existing field, section, grid-column, widget-title, widget-column, and process-step help now uses the shared icon. New placements cover app, table query/view/edit/create/copy, process, report, and record/table action headers or labels. Action help is a sibling of the action control; Radix menu help participates in roving focus.
|
||||
- `HelpSearchResults` provides 200 ms debouncing, a two-character minimum, feature gating, and loading/error/empty states. Both `CommandMenu` and the global `SearchPage` use it. The page's existing grouped-results area is a natural place for help; the compact header/search-dialog listbox retains its current record/navigation model. The palette also offers **Search help…**.
|
||||
- Author mode retains key suffixes. **Add help** / **Edit help** links require the core table's insert permission and use `/app/helpContent/create#/defaultValues=…` with `key` and the most specific `role`. `HELP_CONTENT_TABLE` is the one production constant for that core-defined table.
|
||||
|
||||
## Tests and fixtures
|
||||
|
||||
- New Vitest tests cover the API gate, role precedence, summaries, keyboard controls, author URL presets and permission checks, panel sanitization/focus, and palette help search.
|
||||
- `tests/acceptance/fixture/HelpFixtures.java` defines synthetic Help Lab metadata, Markdown bodies and distinct summaries, an owned data table, and the core authoring table. It is wired into the normal define/reset lifecycle.
|
||||
- `tests/acceptance/specs/help/help.spec.ts` and `matrix/help.json` add HLP-001 through HLP-006, light/dark cases, mobile tags, and 320/1920-pixel panel checks. Column/table-menu coverage remains desktop-only because mobile query results use cards. The area is registered in `acceptance-select.mjs`.
|
||||
- Existing REC-039/040, PRC-015, and widget-column assertions now focus the help button and inspect formatted long content in the panel. This is necessary because tooltips now contain plain summaries and touch opens the panel. Existing scenarios and matrix requirements remain; none was waived.
|
||||
- Historical results in `docs/acceptance/feature-matrix.md` are preserved. The new help rows are appended as **not run**.
|
||||
@@ -0,0 +1,29 @@
|
||||
# Isolated scripting editor
|
||||
|
||||
ScriptViewer routes explicit server-controlled ISOLATED projects before legacy hooks mount. Unknown nonempty modes fail closed. The native scripting extension must be advertised; otherwise isolated source stays unavailable. Developer page provides semantic project creation and opens the same IDE; project names never imply execution mode or actor identity.
|
||||
|
||||
The isolated CodeMirror editor supports multi-file TypeScript, immutable source history, explicit save/build/test/publish, server compiler diagnostics, scoped API declarations, JSON context tests and exact-commit GitHub imports with retained provenance. Edits invalidate build/test evidence. Saves and imports carry the observed latest revision. No browser credential fields or background repository synchronization.
|
||||
|
||||
## Semantic language service
|
||||
|
||||
Pinned editor-only typescript-editor alias6.0.3 powers a same-origin classic worker. It receives a bounded virtual filesystem and host-generated SDK declarations, with57 trusted ES2022 standard-library files bundled at build time. Diagnostics, completion, hover, go-to-definition(F12), formatting(Ctrl+Shift+F), file navigation and compiler diagnostic selection operate on the same workspace. Text remains ordinary undoable editor state. Tab moves focus.
|
||||
|
||||
There is no source execution, compiler emit, runtime fetch, arbitrary package loading, filesystem, DOM/Node declarations or user npm hooks. Browser analysis is advisory; authoritative strict builds still use the qualified isolated server. Worker cold deadline10s/warm3s terminates it on timeout; bounded request/result sizes, four pending calls, idle30s termination and recycling after200 requests limit work. The next explicit request may start a new worker; no hidden retry of timed-out source analysis. A browser worker supplies termination responsiveness, not an OS-enforced memory sandbox. Debugging and automatic imports are not implemented.
|
||||
|
||||
scripts/build-scripting-worker.mjs assembles only trusted package/compiler and our own session source into public/scripting-generated/typescript-6.0.3.worker.js, with license/notices. Generated9.6MB artifact is ignored and built before dev/build/build:export. Runtime worker requests no other resource. Exact compiler and library dependencies remain independent from server runtime qualification.
|
||||
|
||||
The alias package also exports tsc and would shadow the existing compiler under pnpm exec tsc. Use **pnpm typecheck** or **pnpm tsc**: both explicitly call node_modules/typescript/bin/tsc (the existing5.9 build compiler). Existing CI typecheck scripts follow that path. Do not replace frontend build TypeScript with the editor compiler. Compatible CodeMirror versions are pinned through pnpm overrides to avoid duplicate nominal EditorView/state classes.
|
||||
|
||||
## Authorization and uncertainty
|
||||
|
||||
Server permissions and worker availability control lifecycle actions, with backend checks authoritative. Use the matching Core `4.1.0-SNAPSHOT` dependency set described in [current integration status](QBIT-RELEASE-STATUS.md); the old mixed private-artifact/RC20 setup is superseded. Project creation and publication use typed core authority; isolated revision/file content and ownership are immutable. Real native HTTP lifecycle succeeds with protocol-only fixture worker; no production provider is implied.
|
||||
|
||||
Stop waiting or network timeout makes mutation outcome uncertain and prevents another mutation. History refresh does not prove completion; inspect history/reopen. Creation uncertainty directs the user to Scripts before retrying. Abort does not undo committed work. GitHub import needs a server-installed managed access provider and full immutable commit.
|
||||
|
||||
## Verification
|
||||
|
||||
2026-10-30: **114 focused tests across12 files**, existing build-compiler typecheck and changed-file ESLint passed. Real Playwright Chromium worker check exercises strict diagnostics, multi-file completion/hover/definition/format, rejects remote types and proves source does not execute or fetch. Final optimized production build with typecheck, static generation and standalone packaging passed at 21:36, including the creation panel (parent run20209, editor-build-verification.log). Native server tests separately cover create/save/build/context-test/publish and permission/association/legacy denials. No live browser-to-deployed-worker end-to-end or production sandbox qualification claim.
|
||||
|
||||
Run pnpm scripting:worker:check after generating the worker; this environment uses PLAYWRIGHT_BROWSERS_PATH=C:/Users/james/AppData/Local/ms-playwright. The harness serves only a local trusted page/worker with connect-src none and closes browser/server on completion.
|
||||
|
||||
Current integration verification (2026-11-04): the real Chromium offline-worker check passes strict diagnostics, multi-file completion/hover/definition and formatting, with no source execution or external requests. Typecheck, lint and production worker packaging pass on the canonical integration branch. See [current status](QBIT-RELEASE-STATUS.md) for the final full-suite evidence. This frontend result does not qualify the server's untrusted-code runtime.
|
||||
@@ -0,0 +1,47 @@
|
||||
# PDF preview boundary
|
||||
|
||||
The generic file lightbox uses pinned Mozilla `pdfjs-dist` **6.4.299**, loaded only for
|
||||
PDFs, and a locally packaged worker. It no longer embeds uploaded PDF responses in an
|
||||
application-origin iframe or offers a PDF "Open original" navigation. Downloads retain
|
||||
the existing owner authorization callback.
|
||||
|
||||
Retained attachment previews reach it only after the tracked-download helper verifies
|
||||
server receipt, byte length and SHA-256, and assigns `application/pdf` from the fixed
|
||||
PDF signature. The generic renderer accepts local same-origin blob URLs or native
|
||||
file endpoints at the exact configured API origin and base path. Both dashboard hosts must permit `blob:` in CSP `connect-src` so the verified local bytes can be read; the current Javalin and standalone defaults include it. It never forwards API
|
||||
credentials to arbitrary external URLs. Uploaded MIME labels alone are insufficient.
|
||||
|
||||
The renderer supplies bytes (not a document URL) to an explicitly created parser
|
||||
Worker, preventing the library's fake-worker fallback. It creates a canvas and an
|
||||
optional plain-text view only: no PDF annotation/action, form, XFA, scripting or HTML
|
||||
viewer layer. Auxiliary resource requests are rejected, worker fetching and WASM are
|
||||
disabled, and no document-selected external URL is followed. Missing auxiliary fonts
|
||||
or unsupported resources can limit fidelity; the retained original is downloadable.
|
||||
|
||||
Limits: 20 MiB input, 1000 pages, one visible page at a time, 8 million canvas pixels,
|
||||
8192 pixels per edge, 30 seconds to load/parse, 15 seconds per page render/text read.
|
||||
Text is streamed and stopped at 10,000 items or 100,000 characters, slicing each
|
||||
string before aggregation. The stream is cancelled on page change, close or timeout;
|
||||
page controls remain busy through text completion.
|
||||
Closing the dialog aborts input loading, cancels rendering, destroys the loading task
|
||||
and terminates the worker. These are application limits, not a browser-process memory
|
||||
quota: compressed PDF complexity can allocate parser memory before page checks.
|
||||
Canvas rendering and a Web Worker do **not** constitute a full hostile-parser security
|
||||
sandbox. Browser/library patching, CSP and deployment qualification remain necessary.
|
||||
|
||||
Primary references checked on 2026-10-30:
|
||||
|
||||
- [Mozilla release 6.4.299](https://github.com/mozilla/pdf.js/releases/tag/v6.4.299)
|
||||
- [Mozilla PDF.js API](https://mozilla.github.io/pdf.js/api/draft/module-pdfjsLib.html)
|
||||
- [Mozilla prior arbitrary-JavaScript advisory](https://github.com/mozilla/pdf.js/security/advisories/GHSA-wgrm-67xf-hhpq)
|
||||
|
||||
The prior advisory affected versions through 4.1.392 and was fixed in 4.2.67. The current
|
||||
6.4.299 API no longer exposes the old `isEvalSupported` option; this integration does
|
||||
not pretend an obsolete option supplies isolation. It uses the pinned current release,
|
||||
no PDF scripting manager, and the application's restrictive script CSP.
|
||||
|
||||
## Current verification (2026-11-04)
|
||||
|
||||
The production export and local Maven JAR include the pinned worker. Twelve Chromium browser scenarios pass against that packaged UI, including actual PDF worker/canvas pixels, page navigation, no executed PDF OpenAction, separate retained preview/download receipts, refusal of incomplete receipts, and ordered-file submission. Real Core/Collaboration API sessions separately pass ordinary signed-PDF canvas preview, page controls and PDF download.
|
||||
|
||||
Signed attachment PDFs are read with credentials omitted and redirects refused, bounded to 20 MiB and 30 seconds, and checked by byte signature before a local preview URL is exposed. Their storage origin needs host CSP/CORS configuration. Tracked deliveries retain the receipt/hash path. Cancellation and streaming-limit tests cover both paths. See [current status](QBIT-RELEASE-STATUS.md) for the full suite and [release requirements](QBIT-KNOWN-ISSUES.md) for remaining deployment qualifications.
|
||||
@@ -0,0 +1,13 @@
|
||||
# Metadata-driven process result fields
|
||||
|
||||
`ViewFormComponent` delegates adorned result fields to the shared `FieldValue` component. Hidden fields remain in the result value map so native metadata can supply display companions; they are not rendered as separate controls. Ordinary process values and validation errors keep their existing behavior.
|
||||
|
||||
`LINK` supports either `toRecordFromTable` (a static table name) or `toRecordFromTableField` (the name of another result field containing the table name). The field value is the record identity. Navigation uses the native encoded table/record route. A missing companion table produces no link. Possible-value sources that identify a known instance table also retain native record navigation.
|
||||
|
||||
`FILE_DOWNLOAD` can specify `fileNameField` and `deliveryModeField`. If the latter is present, its companion value must be `SIGNED_URL` or `AUTHENTICATED_FETCH`; missing or unknown modes render no access action. Older fields without that metadata retain their existing behavior.
|
||||
|
||||
For `AUTHENTICATED_FETCH`, the issued URL is never placed in an `href`, image source or iframe. Download uses the shared authenticated transfer helper, which checks the configured origin, response bounds, exact digest/length and completed durable receipt. An additional `FILE_PREVIEW` adornment enables an explicit preview action. Only verified PDF/raster blobs reach the shared preview component; component-owned blob URLs are revoked when closed or unmounted. Unsupported content and transfer/audit errors remain visible and never fall back to direct access.
|
||||
|
||||
`CODE_EDITOR` result fields use the shared read-only source display, including renderer-supplied plain catalog help and HTML/JSON examples. No template or script evaluation occurs in a process result.
|
||||
|
||||
Verification for this increment: 178 focused tests across 18 files, owned ESLint and the explicit build-compiler typecheck passed. Each actual transfer creates a fresh correlation UUID only after validating the configured endpoint, preserving the immutable file coordinate and using that same fresh attempt for its receipt. Source changes and unmount abort active body/receipt work. Final integrated production-build/browser acceptance remains pending. These generic components do not embed a QBit name or depend on its backend implementation.
|
||||
@@ -0,0 +1,106 @@
|
||||
# QA capture (Jam)
|
||||
|
||||
Optional QA bug capture for the Next UI. The backend is the `qbit-qa-capture` QBit, which publishes the `qa-capture` UI API extension (1.0.0). [Jam](https://jam.dev) is the first provider. The UI calls only the two operations in [`src/lib/api/openapi/extensions/qa-capture.json`](../src/lib/api/openapi/extensions/qa-capture.json):
|
||||
|
||||
| Operation | Call | Used for |
|
||||
|---|---|---|
|
||||
| `qaCaptureCapability` | `GET /qqq/v1/qbit/qa-capture/capability` | Whether capture is on for the signed-in user, and the Jam team, sanitizer rules, blur selectors and backend context |
|
||||
| `qaCaptureReportCreate` | `POST /qqq/v1/qbit/qa-capture/report` | "Report a bug": creates a report and returns the Recording Link `jam-*` parameters |
|
||||
|
||||
The vendored document is hand-written from the shared contract until the QBit publishes its generated `openapi/qqq-v1/qa-capture.yaml`. Re-vendor it from the QBit before release.
|
||||
|
||||
## When anything loads
|
||||
|
||||
Capture is off unless all of the following hold. When it is off, the UI loads no Jam chunk, adds no Jam meta tags and makes no Jam request:
|
||||
|
||||
1. `GET /qqq/v1/version` lists the `qa-capture` extension. Otherwise the capability is never requested.
|
||||
2. The user is signed in. `QaCaptureLoader` in the root providers then requests the capability once (TanStack Query, `['qqq', 'qa-capture', 'capability']`).
|
||||
3. The capability has `enabled: true`, provider `jam`, a team id and sanitizer rules. Anything else counts as disabled.
|
||||
|
||||
When capture is enabled, `src/lib/qa-capture/jam-loader.ts` (the only module that imports Jam, and only through `import()`) does the following:
|
||||
|
||||
- adds `<meta name="jam:team">` and `<meta name="jam:blur">` (the server's `blurSelectors` plus `[data-qqq-secure]`);
|
||||
- dynamic-imports `@jam.dev/recording-links/sdk` (0.4.0) and `@jam.dev/sdk` (1.0.1);
|
||||
- calls `jam.initialize({ teamId, blurSelectors, network, console })` once, with the sanitizers;
|
||||
- registers `jam.metadata(...)`.
|
||||
|
||||
A Vitest check fails if any other file imports Jam at runtime.
|
||||
|
||||
What the SDKs fetch was checked in their published source. `initialize` loads `https://js.jam.dev/recorder.js` only in two cases: the page URL has `jam-recording` (checked on load and on every `pushState`, `replaceState` or `popstate`), or `loadRecorder()` is called. It loads `https://js.jam.dev/capture.js` only while some tab has an active recorder, which it tracks with the `jam:numRecorders` localStorage counter. `jam.metadata` only stores a callback on `window.__jam__`.
|
||||
|
||||
The browser cannot undo the Jam SDK, so turning capture off for a user (QA mode off) takes effect on the next page load. Request-id collection stops immediately.
|
||||
|
||||
## QA mode
|
||||
|
||||
When `capability.qaMode.preferenceSection` is set, the Preferences page renders that section like any other. Saving it refetches the capability, so turning QA mode on loads Jam without a reload. While `reason` is `QA_MODE`, the header shows a "QA mode on" badge that links to the section.
|
||||
|
||||
## Report a bug
|
||||
|
||||
The user menu shows **Report a bug** when `canReport` is true, and **QA reports** when `reportsHref` is an in-app path. The UI follows that link as given and never builds table paths itself. The Next UI has no help menu (the header help button opens the keyboard shortcuts), so both entries are in the user menu only.
|
||||
|
||||
Report a bug sends `POST /report` with:
|
||||
|
||||
- the page URL, with the sanitizer `queryParamNames`, the OAuth `code`, `state` and token parameters, and every `jam-*` parameter removed;
|
||||
- the title `Bug: <page title>`;
|
||||
- the app, table, record id or process, resolved from `/app/<slug>[/<id>[/<process>]]` against the instance metadata;
|
||||
- the last request ids;
|
||||
- the frontend version, user agent and viewport.
|
||||
|
||||
When the response has `recordingParams`, the UI opens the recorder with `await jam.loadRecorder({ openImmediately: false })` and then `Recorder.open(params['jam-recording'], { jamParams: { 'jam-title', 'jam-reference', ... } })`. The SDK puts those parameters on the URL while recording and removes them when the recorder closes, which lets the QBit's `jam.created` webhook match the capture to the report by `jam-reference`.
|
||||
|
||||
Without `recordingParams`, a toast explains that an administrator must set the Recording Link in the QA capture settings. It includes a QA reports action when `reportsHref` is present.
|
||||
|
||||
## Sanitizers
|
||||
|
||||
`src/lib/qa-capture/sanitizers.ts` builds the four Jam sanitizers from `capability.sanitizer`. They are pure, synchronous and deterministic, and they never log or fetch. The server's rules always get a baseline added: the `authorization`, `proxy-authorization`, `cookie` and `set-cookie` headers; the `password`, `sessionUUID`, `accessToken`, `refreshToken`, `idToken`, `clientSecret` and `code_verifier` fields; the `code`, `state`, `access_token` and `id_token` parameters; and JWTs.
|
||||
|
||||
| Data | Rule |
|
||||
|---|---|
|
||||
| Request and response headers | `headerNames` (lowercase) are deleted |
|
||||
| JSON bodies | values of `bodyFieldNames` keys are replaced at any depth (case-insensitive); `valuePatterns` are applied to every string; the body stays valid JSON |
|
||||
| Form-encoded bodies | the same, field by field |
|
||||
| Other text bodies, WebSocket data | `valuePatterns` |
|
||||
| Request URLs | `queryParamNames` values are redacted; `valuePatterns` are applied |
|
||||
| Same-origin paths under `dropPathPrefixes` | request and response bodies are dropped |
|
||||
| Console arguments | object keys and string patterns as for JSON |
|
||||
|
||||
A `valuePatterns` entry that is not a valid RegExp makes the affected sanitizers throw. Jam then redacts that data instead of sending it (fail closed).
|
||||
|
||||
## Secure fields
|
||||
|
||||
The UI knows a value is secure from its field metadata:
|
||||
|
||||
- the field type is `PASSWORD`;
|
||||
- the field has the `REVEAL` adornment;
|
||||
- secured data restricts the field (`accessTier` is `MASKED` or `REVEALABLE`).
|
||||
|
||||
Elements that show or edit such a value carry `data-jam-blur` and `data-qqq-secure`. This covers `MaskedValue`, a revealed restricted value, grid password and reveal cells, record view reveal and password values, password inputs (including while shown as text) and secure text inputs. The attributes are always present and have no effect when capture is off.
|
||||
|
||||
## Request ids
|
||||
|
||||
While capture is enabled, an axios interceptor keeps the last 50 `{ id, method, path, status, at }` entries from the response header named by `capability.requestIdHeader` (default `X-Request-Id`). The path is stored without its query string. They go into the Jam metadata and the report. Disabling capture clears the buffer.
|
||||
|
||||
## Jam metadata
|
||||
|
||||
At capture time Jam receives the following, kept under 10 KB by dropping the oldest request ids first:
|
||||
|
||||
- the app, table, record id and process;
|
||||
- the frontend version (`NEXT_PUBLIC_APP_VERSION`);
|
||||
- the backend `environment`, `backendVersion` and `node` from the capability;
|
||||
- the request ids;
|
||||
- the report reference;
|
||||
- the page path, filtered as above.
|
||||
|
||||
## Recording Links and sign-in
|
||||
|
||||
A Recording Link can open a protected page as `?jam-recording=...&jam-title=...`. The login redirect, the OIDC/OAuth2 callback and the 401 redirect already carry the query in `returnTo`. Every page load also stashes the `jam-*` parameters in sessionStorage. After sign-in they are put back on the URL with `history.replaceState`, unless the page already has a `jam-recording`. The stash expires after 30 minutes. The Jam SDK watches `replaceState`, so the recorder then opens.
|
||||
|
||||
The public `/qa/recorder` page for Jam domain verification is served by the QBit, not by Next. The standalone server forwards `/qa/*` to the backend.
|
||||
|
||||
## Content-Security-Policy
|
||||
|
||||
Next adds no CSP of its own. In a jar deployment the dashboard CSP comes from the QQQ server (`NextDashboardSecurityHeaders`), and the QBit or application must add the Jam origins listed in `capability.cspOrigins`. The standalone container server (`standalone/qqq-server.mjs`) adds them only when `QQQ_QA_CAPTURE_CSP=true`:
|
||||
|
||||
- `script-src https://js.jam.dev`
|
||||
- `frame-src https://*.jam.dev`
|
||||
- `connect-src https://*.jam.dev`
|
||||
@@ -0,0 +1,11 @@
|
||||
# QBit integration known issues and release requirements
|
||||
|
||||
Updated: 2026-11-05.
|
||||
|
||||
- The Core, Collaboration and Next UI changes are verified source branches and local snapshots, not a published dependency set. Public Core RC.21 / Collaboration RC.6 lack the required retention contracts. Merge and publication through the existing release process remain separate from local verification.
|
||||
- The frontend checks include real Core/Collaboration sessions and file storage, plus browser protocol fixtures for retained receipts. They do not establish end-to-end document generation/printing or deployed untrusted-script execution. Those QBits and workers retain their own qualification requirements.
|
||||
- External signed PDF storage needs CORS for the dashboard origin and its origin in CSP `connect-src`. Application credentials are never sent to those URLs. Hosts replacing dashboard CSP must retain `blob:` in `connect-src` for verified local previews.
|
||||
- Lint reports six existing warnings in unchanged files. The unit suite has one existing skipped test. Canonical Next 16.4.1 and Zod 3 are preserved; dependency patch upgrades remain a separate release review.
|
||||
- The local artifact is `qqq-frontend-next:1.0.0-SNAPSHOT`, built with an explicit Maven revision override. It includes generated PDF and TypeScript workers. The prepared release-managed version is `1.0.0-RC.38`; RC.36 and RC.37 already exist in Central and must never be overwritten.
|
||||
- Next publisher support and the shared producer are merged (Praetor PR238, platform-ci PR67); the local caller pins327e9b3a9d45a08665fa9468f0f9741e972d8e3e. Runtime qualification passed736 Linux tests and its vulnerability scan. GitOps PR269 deployed the qualified runtime and exact workflow/GAV enrollment; ArgoCD adoption and the scheduled publisher startup are verified. The signed application tag and resulting Central artifact still remain. The existing tag, package.json and POM version agreement, typecheck/lint/tests/static export and signed Central delivery are preserved. Neither the local JAR nor source merge proves publication.
|
||||
- The full application acceptance matrix across all browsers and deployment-specific storage/printer/scanner behavior is not claimed by the scoped QBit integration checks.
|
||||
@@ -0,0 +1,58 @@
|
||||
# Next UI QBit integration status
|
||||
|
||||
Updated: 2026-11-05. Source integration is prepared; no published release or deployment is claimed.
|
||||
|
||||
Current main `090f5febeaacf3f9c39a22952e5c93c859cdbeb7` (permission editor and companion Tools workspace) is integrated. The latest merge passed all 110 affected Authentik/widget/process tests, typecheck and the copyright-header gate. Both branches supplied the same worker limit; the merged configuration retains one setting. Prior focused Tools/UI-API checks covered 61 cases. These are integration checks, not completed release CI.
|
||||
|
||||
Core PR1343 merged as7825261ab after full CI4482 passed; promotion PR1345 merged as1bef44cf and immutable publication4629 is running. Collaboration must consume the resulting published version. Next publisher support merged in Praetor PR238 and platform-ci PR67. GitOps PR269 merged as883e1ce3; ArgoCD adopted the exact configuration and a scheduled publisher is running image sha256:ea068b2a02147280cacedd095fbc55114daa9e3db72f1b0ca8511bc43bfd12c7 with the exact Next enrollment. Its 736 Linux tests and vulnerability scan passed. The dashboard candidate version is 1.0.0-RC.38; this source preparation is not a published release.
|
||||
|
||||
Candidate CI4602 passed 3,800 tests but hit the unchanged five-second timeout in two `RecordQuery` filter tests. These also timed out on an earlier baseline; the newer main baseline's Node job passed. Profiling the focused cases identified repeated whole-page accessibility lookups. Scoping those lookups to the existing filter controls and confirmation dialog reduced their sampled CPU cost from 387 to 261 samples; the same assertions, interactions and timeouts are preserved. All 17 tests in the affected file and focused lint pass after the change. Release CI for this correction remains required; these focused results do not replace it.
|
||||
|
||||
## Changes
|
||||
|
||||
The canonical Next UI integration renders file preview/download adornments, retained-transfer receipts, isolated TypeScript editing and generic process results through QQQ metadata and UIUX APIs. It adds no table-specific application routes. The completion pass fixes Windows static-export execution, local PDF reads under dashboard CSP, and ordinary signed-storage PDF previews. Signed storage reads omit credentials, refuse redirects and enforce byte/time limits before canvas rendering. Retained transfers preserve their receipt/hash checks. Tests now use four workers by default to avoid high-core host contention; no timeout or coverage threshold was relaxed.
|
||||
|
||||
## Verified dependencies
|
||||
|
||||
- Core `feat/standalone-scripting-api`, code commit `d384d1c753cb0ac2ee4d0f6ccd564a3e47488a1c`, locally installed as `4.1.0-SNAPSHOT`.
|
||||
- Collaboration `feat/record-files-standalone`, `53bd8f00d8a180b0fa4eb6cd029dc31044b6d4b3`, locally installed as `0.1.0-SNAPSHOT`.
|
||||
- Next UI retains canonical Next 16.4.1, Zod 3 and existing dependencies. Local packaging explicitly overrides Maven revision to `1.0.0-SNAPSHOT`; it does not replace public RC.36.
|
||||
|
||||
## Completed verification
|
||||
|
||||
| Check | Result |
|
||||
| --- | --- |
|
||||
| Full `pnpm test:coverage` | 359 files; **3,784 passed**, 1 existing skip; all coverage gates passed |
|
||||
| Coverage | 93.14% statements, 87.00% branches, 94.57% functions, 94.95% lines; API/utility sub-gates also passed |
|
||||
| Lint and license headers | Zero errors, six existing warnings in unchanged files; Apache headers passed |
|
||||
| Typecheck | Passed |
|
||||
| Offline TypeScript worker in Chromium | Strict diagnostics, multi-file completion/hover/definition and formatting passed; no source execution or external fetch |
|
||||
| Windows production static export | Passed using `pnpm build:export` |
|
||||
| Local Maven snapshot JAR | Built successfully with PDF/TypeScript workers included |
|
||||
| Browser file/process regressions | **12 passed** against the packaged production UI; API fixtures exercise PDF canvas, separate audit receipts, incomplete-receipt refusal and keyboard ordering |
|
||||
| Live Core + Collaboration file workflow | Upload, thumbnail, preview, open, exact-byte download, rename/describe, reload and delete passed with synthetic data |
|
||||
| Live PDF and authorization | Signed PDF canvas/page controls and download passed; another session was denied access to the parent record |
|
||||
| Live UI API specification check | Bundled 1.8.0 compatible with running Core 1.10.0 |
|
||||
|
||||
The live tests used the existing disposable `CollaborationDevServer`, synthetic Alice/Bob sessions and the current UI JAR on loopback. No API mocks were used for those file/permission checks. The separate 12-case browser suite uses its existing API fixtures, not a deployed Record Files or scripting worker. Test-owned services and browsers were stopped after verification.
|
||||
|
||||
## Reproduce
|
||||
|
||||
```shell
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm test:coverage
|
||||
pnpm lint
|
||||
pnpm typecheck
|
||||
pnpm scripting:worker:check
|
||||
pnpm build:export
|
||||
mvn -B -Drevision=1.0.0-SNAPSHOT package
|
||||
pnpm ui-api:check --server-url http://localhost:18098
|
||||
```
|
||||
|
||||
Start the matching Collaboration demo using its README and place the resulting UI JAR first on its classpath. The live check used port 18098. The standard `pnpm test:e2e` owns its frontend fixture for the browser scenarios. Maven was invoked through the sibling Collaboration wrapper during this Windows run.
|
||||
|
||||
Local JAR SHA-256: `0626B33DF49569476E17FA080E2777A7AD9C7AF717CB75DFABF9EB40BE0A46A8`.
|
||||
|
||||
## Release boundary
|
||||
|
||||
See [known issues and release requirements](QBIT-KNOWN-ISSUES.md). Compatible public artifact publication, the complete application acceptance matrix and deployed document/script worker qualification are not implied by these source and local integration results.
|
||||
@@ -0,0 +1,21 @@
|
||||
# Record Files client integration
|
||||
|
||||
Updated: 2026-11-04. Current canonical integration branch: `feat/standalone-qbits-ui`. See [verification and dependencies](QBIT-RELEASE-STATUS.md).
|
||||
|
||||
## Standard controls and APIs
|
||||
|
||||
The Files tab uses standard themed buttons for Preview, Open, Download and Rename / describe. Navigation uses Next links with keyboard focus. Attachment metadata supplies independent `canEdit` and `canDelete` permissions; an explicitly denied edit capability never falls back to deletion permission. The server remains authoritative.
|
||||
|
||||
Every download requests a fresh owner-issued descriptor. `AUTHENTICATED_FETCH` deliveries use the configured native Record Files route, a new correlation identifier, and a separately verified completion receipt, byte count and SHA-256. Preview and save are separate attempts. A failed receipt never becomes a direct link or a successful save. Generic `FILE_DOWNLOAD` and `FILE_PREVIEW` adornments use the same controls; they do not require table-specific pages.
|
||||
|
||||
Tracked raster/PDF previews expose a local object URL only after verification. Ordinary signed PDF previews read bounded bytes with credentials omitted, redirects refused and a 30-second deadline. Both use the same canvas PDF renderer; uploaded PDFs are never framed. Closing the preview aborts work and revokes local URLs. Hosts serving signed files from another origin must allow that storage origin in CSP `connect-src` and configure storage CORS for the dashboard origin. This never forwards QQQ credentials to storage.
|
||||
|
||||
Open previews supported media; unsupported types use the authorized download action. The UI does not claim that a browser saved bytes or that a printer physically printed them. Retention, upload sessions, collections and durable audits belong to their server QBits.
|
||||
|
||||
## Dependency order
|
||||
|
||||
Install matching Core, then Collaboration, then Record Files and other feature QBits before the matching frontend. Public Core RC.21 and Collaboration RC.6 do not contain the new retention contracts. Local verification uses Core `4.1.0-SNAPSHOT` and Collaboration `0.1.0-SNAPSHOT`; see their branch status files for exact source commits. Core's dashboard CSP must include `blob:` in `connect-src` for local PDF-byte reads.
|
||||
|
||||
## Verification scope
|
||||
|
||||
The full component/API suite and coverage gates, production export, browser PDF/receipt/ordering scenarios and live Collaboration file workflow are recorded in [current status](QBIT-RELEASE-STATUS.md). Live tests use disposable synthetic data and real Core/Collaboration APIs. Browser mocks of the retained-transfer protocol do not establish production storage or worker qualification.
|
||||
@@ -0,0 +1,17 @@
|
||||
# UI API 1.6 adoption
|
||||
|
||||
Adopts QQQ UI API 1.6.0 (QQQ 4.1.0-RC.15): table clientCapabilities, field validation, BasicErrorResponseV1, /qqq/v1/branding.
|
||||
|
||||
- Vendors the supplied core, api-aware, ESB, alerts, collaboration, data-analysis and EDI documents; regenerates EDI types. Assistant, quick-search, secured-data and sql-workbench documents are unchanged.
|
||||
- Saved views, audits, sharing and bulk actions use per-user table capabilities. Legacy discovery is used only when `clientCapabilities` is absent; a missing or unavailable feature in a present capability object does not enable legacy access.
|
||||
- Forms use `field.validation` for required values, length policies, inclusive/exclusive bounds and whole-value patterns. Existing record-write error mapping remains intact.
|
||||
- Errors prefer `message` over legacy error fields. Global error toasts and process error details expose the trace identifier. Session-expiration handling remains in place.
|
||||
- Standalone branding aliases call `/qqq/v1/branding/{role}` and retry the old public route only after a 404. Instance consumers prefer `supplementalInstanceMetaData.uiSettings`, with deprecated settings as a fallback.
|
||||
|
||||
The app version is unchanged. No commit, push, PR or deployment was made.
|
||||
|
||||
## Verification
|
||||
|
||||
The supplied server specification comparison passes at 1.6.0. `OFF_SPEC_CALLS` remains empty and `OFF_SPEC_CEILING` remains 0.
|
||||
|
||||
Vitest startup is blocked by this session's Windows sandbox (`spawn EPERM` launching esbuild), so no Vitest pass count is claimed. The production bundle compiles, but `pnpm build` stops at its TypeScript child-process launch with `spawn EPERM`; no completed build or bundle-budget delta is available. Standalone `pnpm typecheck` passes. `pnpm lint` passes with six pre-existing warnings. Twelve direct Node smoke checks pass: seven lossless JSON vendoring comparisons and five branding status/fallback cases. The focused contract/extension Vitest invocation and full `pnpm test` both stop before running tests.
|
||||
@@ -91,7 +91,7 @@ The additional ESB integration scenarios and their focused three-browser results
|
||||
| PRC-016 | Back to the backend back step with entered values | Back restarts at the back step with values (possible-value label shown); resubmitting saves again | no Back without a backend backStep | prcComponents review, prcWizard longDetails, greetInteractive | pass (chromium) | [#655](https://github.com/QRun-IO/qqq/issues/655) [#667](https://github.com/QRun-IO/qqq/issues/667) |
|
||||
| PRC-017 | Dynamic step list from processMetaDataAdjustment.updatedFrontendStepList | short route drops a screen from the stepper; values logged (SQL) | | prcWizard | pass (chromium) | [#662](https://github.com/QRun-IO/qqq/issues/662) |
|
||||
| PRC-018 | Updated field metadata from processMetaDataAdjustment.updatedFields | field relabelled and required on the later screen | empty relabelled required field blocks | prcWizard long route | pass (chromium) | [#662](https://github.com/QRun-IO/qqq/issues/662) |
|
||||
| PRC-019 | Async job progress (message, N of M, progress bar, updated time) then continue | RUNNING progress shown while polling; finished screen and SQL log | | prcProgress | pass (chromium) | [#657](https://github.com/QRun-IO/qqq/issues/657) |
|
||||
| PRC-019 | Async job progress in the live job view (message, N / M, determinate progress bar, elapsed) then continue | RUNNING progress shown while polling; finished screen and SQL log | | prcProgress | pass (chromium) | [#657](https://github.com/QRun-IO/qqq/issues/657) |
|
||||
| PRC-020 | Cancel from a screen and mid-job runs the cancel step and leaves | confirm dialog; cancel step row (SQL) from a screen and while working | Stay on Page keeps the run | prcProgress cancelStep | pass (chromium) | |
|
||||
| PRC-021 | Step errors: user-facing messages directly, internal errors behind a detail toggle, Close | user-facing message; internal detail toggle; expanded details meet contrast requirements in light, dark and application themes; simpleThrow "I always throw."; Close returns to the app | | prcFailures, prcComponents, sample simpleThrow | pass (chromium) | |
|
||||
| PRC-022 | Retry after an error restarts with the original record selection | retry loads the selected records 2 and 4 | | prcFlaky (fails once) | pass (chromium) | [#658](https://github.com/QRun-IO/qqq/issues/658) |
|
||||
@@ -190,7 +190,7 @@ The additional ESB integration scenarios and their focused three-browser results
|
||||
| QRY-066 | Variant options come from the v1 variants route | the variant picker lists the stores from GET /qqq/v1/table/{table}/variants; the v1 route lists every option with its type and id; a table without variants lists none | an unknown table is refused (403) | qryStock (qryStore options) | pass (chromium) | [#699](https://github.com/QRun-IO/qqq/issues/699) [#406](https://github.com/QRun-IO/qqq/issues/406) |
|
||||
| QRY-067 | Go To opens a record by its primary key or a configured unique key (Material gotoFieldNames) | v1 table metadata carries supplementalMetaData.materialDashboard.gotoFieldNames (API); Go To... on the query toolbar and the record view header offers the primary key, then each key, labelled by field labels; primary key, single-field and two-field keys each open the one matching record (EQUALS query, id from SQL); no match shows "Record not found."; a partial key matching two bins shows "More than 1 record was found..." (counts from SQL); Enter in an input submits its option; Escape closes and returns focus to the Go To button; a table with GET but no QUERY capability opens Go To automatically and it cannot be dismissed | a table without gotoFieldNames has no Go To button on its query or record screen; no navigation on no match or several matches | QueryFixtures.java qryBin (gotoFieldNames [[code], [aisle, shelf]]), qryLocker (GET without QUERY), qryItem (no Go To) | pass (chromium) | [#714](https://github.com/QRun-IO/qqq/issues/714) |
|
||||
| QRY-068 | Default grid columns follow the table sections (section order, each field once), not the metadata field order (Material bySection order) | Headers read Id, Name, Code, Price, Quantity; Rows equal SQL select ordered by id desc | The metadata field-map order (price first) is not used | QueryFixtures.java qryOrdered (qry_item rows; fields declared price, quantity, code, id, name; sections identity [id, name, code], details [price], stock [quantity]) | pass (chromium) | [#714](https://github.com/QRun-IO/qqq/issues/714) |
|
||||
| QRY-069 | Pagination numbers are locale formatted in the shown range and the total | Page 5 at 250 per page reads Showing 1,001–1,234 of 1,234; The page shows exactly the last 234 ids by SQL | No unformatted range such as 1001–1234 | QueryFixtures.java qryManyRow (1,234 rows) | pass (chromium) | [#714](https://github.com/QRun-IO/qqq/issues/714) |
|
||||
| QRY-069 | Pagination numbers are locale formatted in the shown range and the total | Page 5 at 250 per page reads Showing 1,001–1,234 of 1,234; The page shows exactly the last 234 ids by SQL | No unformatted range such as 1001–1234 | QueryFixtures.java qryManyRow (1,234 rows) | pass (chromium) | [#714](https://github.com/QRun-IO/qqq/issues/714) |
|
||||
| QRY-070 | Query screen keyboard shortcuts n (new record), r (refresh the query), f (open the filter builder) | r sends a new query request and shows a value changed through the API (SQL confirms the change); f opens the Advanced Filters panel and a second f keeps it open; n opens the create form | keys typed in quick search are text (no filter panel, no navigation); viewer persona: n does nothing (f still works); the backend refuses the insert (403, SQL unchanged) | sample person | pass (chromium) | [#714](https://github.com/QRun-IO/qqq/issues/714) |
|
||||
| QRY-071 | Processes launched from a variant table run with the chosen variant (tableVariant on init and every step) and return to the query | Bulk Delete of the South store's full query result counts its one record; Every init and step request carries tableVariant {type: qryStore, id: 2}; Return comes back to the variant query, which is empty for South while North keeps its two rows | Without the variant the backend could not read the variant table's records | QueryFixtures.java qryStock (memory backend keyed by qryStore variants) | pass (chromium) | [#714](https://github.com/QRun-IO/qqq/issues/714) |
|
||||
| QRY-080 | Basic mode displays default quick filters from table metadata and applies their criteria | First Name quick filter finds the SQL-matching records; Type-specific defaults gate Boolean until chosen, default possible values to any-of and date-time to after; A multi-value chip summarizes extra values; A saved Basic view restores its criterion and SQL rows | No application-specific field names are embedded in the UI; Cancel discards an un-applied quick-filter draft | sample person and QueryFixtures.java qryItem | pass (chromium) | [#715](https://github.com/QRun-IO/qqq/issues/715) |
|
||||
@@ -348,6 +348,7 @@ The additional ESB integration scenarios and their focused three-browser results
|
||||
| INT-012 | Stale state: changes and deletions made elsewhere are shown after refresh/navigation, not stale cache | record edited through the API shows the new value on revisit | record deleted through the API shows not found | qqq-sample-project (stock sample, mock auth, sharing demo) | pass (chromium) | [#683](https://github.com/QRun-IO/qqq/issues/683) |
|
||||
| INT-013 | Empty state: a query with no results explains it and offers the create action | no-match filter shows the empty message and Create | | qqq-sample-project (stock sample, mock auth, sharing demo) | pass (chromium) | |
|
||||
| INT-014 | Backend unavailable at sign-in: the login page explains the failure and recovers with Try again | Try again after recovery signs in | network failure shows "Sign-in failed" | qqq-sample-project (stock sample, mock auth, sharing demo) | pass (chromium) | [#669](https://github.com/QRun-IO/qqq/issues/669) |
|
||||
| SEC-050 | Optional QA capture (Jam, qbit-qa-capture qa-capture extension): Jam loads only for enabled users; Report a bug opens the recorder; secure values blurred; captures sanitized | extension absent or capability disabled: no Jam chunk, meta or request; enabled: jam:team meta, one initialize with sanitizers, jam.metadata; Report a bug: POST report then Recorder.open with jam-title and jam-reference; Recording Link jam-* parameters survive the OIDC sign-in; QA mode preference refetches the capability | no Recording Link configured: toast, no recorder; Authorization/Cookie headers, secure body fields and JWTs redacted | qbit-qa-capture (not yet released); Vitest: src/lib/qa-capture/*.test.ts, src/components/qa-capture/QaCapture.test.tsx | excluded (Pending: not yet approved by the owner. No real-backend fixture until qbit-qa-capture is released; the row is covered by Vitest until then (QA capture PR, 2026-10-15).) | |
|
||||
|
||||
## Dashboards, widgets, reports, saved and scheduled reports, and sharing
|
||||
|
||||
@@ -442,3 +443,24 @@ The additional ESB integration scenarios and their focused three-browser results
|
||||
| RPT-020 | Report files download from the v1 download and report routes | a process report links its file at /qqq/v1/download/... and the file has every row; a report without a process streams from /qqq/v1/reports/{name}?format=csv with every row | an unregistered server file or storage reference is refused (403); a restricted report is 403, an unknown one 404, a missing format 400 | accPersonReport, accStreamedReport, accRestrictedReport | pass (chromium) | [#699](https://github.com/QRun-IO/qqq/issues/699) [#406](https://github.com/QRun-IO/qqq/issues/406) |
|
||||
| WID-074 | Standalone composite scalar controls and metadata editors | Seeded text, number, date, boolean and native file controls; currency prefix, named possible values and multiline code editor; Choice width and center-click behavior with system and monospace fonts, including a 320px viewport; Backend uppercase/lowercase metadata preserves middle-of-value edits and caret position in standalone blocks; Multiline TEXT case conversion preserves the middle-edit caret and inserted newline; Desktop wheel scrolling preserves plain and shared numeric block values; intentional ArrowUp edits still work and text inputs retain focus; Native standalone TIME controls increment by one second and retain second-level values. | Code-editor Enter leaves the widget on the same page; Multiline Enter does not submit or navigate away | widgetInputEditors / accTypedInputs | pass (chromium) | [#728](https://github.com/QRun-IO/qqq/issues/728) |
|
||||
| WID-075 | Standalone DATE_TIME blocks localize seeded instants without losing seconds | UTC timestamp displays the correct local date, time and seconds | Do not slice UTC digits into a local-time field | accTypedInputs in America/New_York | pass (chromium) | [#728](https://github.com/QRun-IO/qqq/issues/728) |
|
||||
|
||||
## Contextual help, help search and authoring
|
||||
|
||||
| ID | Feature | Scenarios | Negative | Fixture | Result | Issues |
|
||||
|---|---|---|---|---|---|---|
|
||||
| HLP-001 | App and widget help | summary tooltip; keyboard panel; sanitized long markdown; focus restoration; light and dark; desktop and mobile | Strict diagnostics; no off-spec API calls | HelpFixtures.java | not run | |
|
||||
| HLP-002 | Table, field and section help | view screen; edit screen; create screen; role selection | Strict diagnostics; no off-spec API calls | HelpFixtures.java | not run | |
|
||||
| HLP-003 | Query columns and table action help | column summary; query table header; action is not invoked by help | Strict diagnostics; no off-spec API calls | HelpFixtures.java | not run | |
|
||||
| HLP-004 | Process, step and record action help | process header; step header; record menu keyboard control | Strict diagnostics; no off-spec API calls | HelpFixtures.java | not run | |
|
||||
| HLP-005 | Help search | static Search help command; debounced help query; result summary and panel; Go to link | Strict diagnostics; no off-spec API calls | HelpFixtures.java | not run | |
|
||||
| HLP-006 | Help author affordance | key and specific role URL presets; standard create form; read-only users cannot author | Strict diagnostics; no off-spec API calls | HelpFixtures.java | not run | |
|
||||
|
||||
The help rows above have not been executed locally; historical results above predate this change.
|
||||
|
||||
## Release notes UI API extension
|
||||
|
||||
`release-notes` 1.0.0 (qbit-release-notes 0.1.0-RC.1+) provides What's new at
|
||||
`/app/whats-new`: category groups, new badges, append paging, safe links/screenshots,
|
||||
release deep links, seen tracking and the user-menu unread count. Extension discovery
|
||||
hides the menu and prevents requests on unsupported servers. Covered by the release-notes
|
||||
client/page and Sidebar Vitest suites; real-backend acceptance remains pending.
|
||||
@@ -545,7 +545,7 @@ Process and report launch, the step lifecycle, process form fields, step compone
|
||||
| Load instance/process/table metadata, with load-error messages | `material:src/qqq/pages/processes/ProcessRun.tsx:1858-1897` | `src/app/(dashboard)/app/[slug]/page.tsx` ("Failed to load process metadata.") | none | Done |
|
||||
| Init, then handle JobComplete / Started / Running / Error | `material:src/qqq/pages/processes/ProcessRun.tsx:1595-1740` | `src/lib/hooks/use-process.ts:handle` | PRC-001, PRC-019 | Done |
|
||||
| Async polling: 1.5 s, ×1.5 backoff to 12 s on 5xx, then "Could not connect to server" | `material:src/qqq/pages/processes/ProcessRun.tsx:98-100, 1745-1795` | `src/lib/hooks/use-process.ts` pollRef / POLL_* | PRC-019 | Done |
|
||||
| Working screen: spinner, message, "N of M", progress bar, "Updated at" | `material:src/qqq/pages/processes/ProcessRun.tsx:631-670` | `src/components/process/ProcessRun.tsx` working block | PRC-019 | Done |
|
||||
| Working screen: spinner, message, "N of M", progress bar, "Updated at" | `material:src/qqq/pages/processes/ProcessRun.tsx:631-670` | `src/components/jobs/InlineJobProgress.tsx` (live job view: phases, message, N / M, eased bar, ETA, elapsed and live log in place of "Updated at") | PRC-019 | Done |
|
||||
| Loading shown before the first step | `material:src/qqq/pages/processes/ProcessRun.tsx:631, 2079` | `src/components/process/ProcessRun.tsx` (phase ≠ step) | PRC-040 | Done |
|
||||
| userFacingError vs internal error | `material:src/qqq/pages/processes/ProcessRun.tsx:1720-1734` | `src/lib/hooks/use-process.ts` ERROR case | PRC-021 | Done |
|
||||
| Error screen: "An error occurred…", show/hide detail toggle, Close/Cancel | `material:src/qqq/pages/processes/ProcessRun.tsx:589-629` | `src/components/process/ProcessErrorState.tsx` | PRC-021, PRC-022 | Done: Next adds Retry |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Next UI agent guide
|
||||
|
||||
> **This checkout targets RC21.** Confirm availability in the [RC21 GitHub release](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.21) before installing. [RC21 notes](../releases/1.0.0-RC.21.md) record the preparation checkpoint and verification scope; RC20 was the previously verified public candidate at that checkpoint. Earlier versioned receipts remain historical evidence; final 1.0 remains held.
|
||||
> **This checkout targets RC36.** Confirm availability in the [RC36 GitHub release](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.36) before installing. [RC36 notes](../releases/1.0.0-RC.36.md) record the preparation checkpoint and verification scope; RC35 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.
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# Next UI developer guide
|
||||
|
||||
> **This checkout targets RC21.** Confirm availability in the [RC21 GitHub release](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.21) before installing. [RC21 notes](../releases/1.0.0-RC.21.md) record the preparation checkpoint and verification scope; RC20 was the previously verified public candidate at that checkpoint. Earlier versioned receipts remain historical evidence; final 1.0 remains held.
|
||||
> **This checkout targets RC36.** Confirm availability in the [RC36 GitHub release](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.36) before installing. [RC36 notes](../releases/1.0.0-RC.36.md) record the preparation checkpoint and verification scope; RC35 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 RC21 preparation checkpoint was [1.0.0-RC.20](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.20) (commit `60d0b0f`); 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 RC36 preparation checkpoint was [1.0.0-RC.35](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.35) (commit `55c1501`); 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.
|
||||
|
||||
@@ -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.21</version>
|
||||
<version>1.0.0-RC.36</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
|
||||
@@ -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 RC21 preparation checkpoint: **[1.0.0-RC.20](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.20)**. Its public release entry records exact artifact verification, backend requirements and known limits. RC1 through RC19 remain available and unchanged.
|
||||
Previously verified public candidate at the RC36 preparation checkpoint: **[1.0.0-RC.35](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.35)**. Its public release entry records exact artifact verification, backend requirements and known limits. RC1 through RC20 and RC22 through RC35 remain available and unchanged (RC21 was never published).
|
||||
|
||||
## Fixes during testing
|
||||
|
||||
This checkout targets [RC21](1.0.0-RC.21.md); confirm availability in the [RC21 GitHub release](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.21) before installing. RC21 adds file attach in the assistant panel (bulk load) ([PR125](https://github.com/QRun-IO/qqq-frontend-next/pull/125) (attach a file in the assistant panel for bulk load)). RC20 added readable assistant answers (GFM and presentation blocks), data-analysis profile panels, and favorites and recents ([PR92](https://github.com/QRun-IO/qqq-frontend-next/pull/92) (UI API version contract), [PR104](https://github.com/QRun-IO/qqq-frontend-next/pull/104) (UI API contract picker paths), [PR109](https://github.com/QRun-IO/qqq-frontend-next/pull/109) (acceptance ESB extension fix), [PR103](https://github.com/QRun-IO/qqq-frontend-next/pull/103) (data-analysis column and table profile panels), [PR105](https://github.com/QRun-IO/qqq-frontend-next/pull/105) (favorites, server recents and command-palette favorites), [PR111](https://github.com/QRun-IO/qqq-frontend-next/pull/111) (readable assistant answers: GFM markdown and presentation blocks)). RC19 added the related-record picker (single, multi-child and link modes) and the assistant Read-only / Write switch ([PR96](https://github.com/QRun-IO/qqq-frontend-next/pull/96) (related-record picker: single, multi-child and link modes), [PR101](https://github.com/QRun-IO/qqq-frontend-next/pull/101) (assistant Read-only / Write switch per conversation)). RC18 added the Authentik policy editor widget and the assistant pending-action card with persona ([PR91](https://github.com/QRun-IO/qqq-frontend-next/pull/91) (bundle budget report-only (owner decision)), [PR97](https://github.com/QRun-IO/qqq-frontend-next/pull/97) (faster sharded acceptance CI), [PR98](https://github.com/QRun-IO/qqq-frontend-next/pull/98) (budget script honors "enforce": false), [PR95](https://github.com/QRun-IO/qqq-frontend-next/pull/95) (Authentik policy editor widget), [PR94](https://github.com/QRun-IO/qqq-frontend-next/pull/94) (assistant pending-action card, persona and refresh after a write)). RC17 added the notification center (bell inbox, notification list and preferences for qbit-alerts user notifications) ([PR89](https://github.com/QRun-IO/qqq-frontend-next/pull/89) (notification center)). RC16 added the record picker for large possible-value fields, metadata-driven form validation, date-first filters with presets, the Alerts bell and record panel, resizable navigation and search preview thumbnails ([PR79](https://github.com/QRun-IO/qqq-frontend-next/pull/79) (resizable desktop navigation), [PR84](https://github.com/QRun-IO/qqq-frontend-next/pull/84) (search preview thumbnails), [PR86](https://github.com/QRun-IO/qqq-frontend-next/pull/86) (CSS budget 24 KB), [PR87](https://github.com/QRun-IO/qqq-frontend-next/pull/87) (range-error wording follows the backend), [PR85](https://github.com/QRun-IO/qqq-frontend-next/pull/85) (Alerts bell and record panel), [PR82](https://github.com/QRun-IO/qqq-frontend-next/pull/82) (date-first filters), [PR81](https://github.com/QRun-IO/qqq-frontend-next/pull/81) (quiet, metadata-driven form validation), [PR80](https://github.com/QRun-IO/qqq-frontend-next/pull/80) (record picker for large possible-value fields)). RC15 added the QQQ Assistant panel, the record developer view, search-as-you-type quick search and a search page, readable dashboard widget widths, one page scroll container and SQL Workbench dark mode ([PR58](https://github.com/QRun-IO/qqq-frontend-next/pull/58) (README adoption guide), [PR55](https://github.com/QRun-IO/qqq-frontend-next/pull/55) (readable dashboard widget widths), [PR54](https://github.com/QRun-IO/qqq-frontend-next/pull/54) (one page scroll container), [PR77](https://github.com/QRun-IO/qqq-frontend-next/pull/77) (Dependabot limited to three open PRs per ecosystem), [PR78](https://github.com/QRun-IO/qqq-frontend-next/pull/78) (SQL Workbench dark mode and standard controls), [PR61](https://github.com/QRun-IO/qqq-frontend-next/pull/61) (record developer view), [PR60](https://github.com/QRun-IO/qqq-frontend-next/pull/60) (QQQ Assistant panel, phase 1), [PR59](https://github.com/QRun-IO/qqq-frontend-next/pull/59) (search-as-you-type quick search, richer results and a search page)). RC14 was an urgent fix: record search no longer fails when one result has no label ([PR57](https://github.com/QRun-IO/qqq-frontend-next/pull/57)). RC13 added 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; the RC14 search fix isLine truncated
|
||||
This checkout targets [RC36](1.0.0-RC.36.md); confirm availability in the [RC36 GitHub release](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.36) before installing. RC36 adds accessibility fixes: readable link text, 24px clear targets, h2 widget titles and skip-link focus ([PR175](https://github.com/QRun-IO/qqq-frontend-next/pull/175) (a11y: readable link text, 24px clear target, h2 widget titles, skip link focus)). RC35 added a profile photo uploader and avatars everywhere (UI API 1.8.0) ([PR164](https://github.com/QRun-IO/qqq-frontend-next/pull/164) (profile photo uploader and avatars everywhere, UI API 1.8.0)). RC34 added fail-soft error handling: layered error boundaries, validated assistant payloads and streams that can no longer lock the panel ([PR172](https://github.com/QRun-IO/qqq-frontend-next/pull/172) (layered error boundaries, zod payload repair, streaming can no longer lock the assistant panel)). RC33 added the new navigation (sidebar sections and favorites, one search, a new header, single-key shortcuts) and an assistant crash fix ([PR168](https://github.com/QRun-IO/qqq-frontend-next/pull/168) (sidebar sections and favorites, one search, new header, single-key shortcut switch), [PR170](https://github.com/QRun-IO/qqq-frontend-next/pull/170) (empty assistant query results no longer crash the page)). RC32 added a What's new page and user-menu entry for release notes ([PR165](https://github.com/QRun-IO/qqq-frontend-next/pull/165) (What's new page and user-menu entry, release-notes UI API 1.0.0)). RC31 added UI API 1.6.0 adoption: client capabilities, field validation, one error schema and v1 branding ([PR162](https://github.com/QRun-IO/qqq-frontend-next/pull/162) (adopt QQQ UI API 1.6.0: clientCapabilities, field validation, error schema, v1 branding)). RC30 added optional Jam QA capture on the qbit-qa-capture UI API extension ([PR160](https://github.com/QRun-IO/qqq-frontend-next/pull/160) (optional Jam QA capture on the qbit-qa-capture qa-capture extension)). RC29 added the EDI mapper page on the qbit-edi UI API extension ([PR136](https://github.com/QRun-IO/qqq-frontend-next/pull/136) (EDI mapper page on the qbit-edi edi extension, all calls on spec)). RC28 added zero off-spec UI API calls, so every backend call is a /qqq/v1 operation in the spec (minimum UI API 1.4.0) ([PR141](https://github.com/QRun-IO/qqq-frontend-next/pull/141) (zero off-spec UI API calls; OFF_SPEC_CEILING 0; minimum UI API 1.4.0)). RC27 added the secured-data UI (masked values, reveal, restricted filters, break-glass banner) and the live job view with a header Jobs tray and Jobs page ([PR124](https://github.com/QRun-IO/qqq-frontend-next/pull/124) (secured-data UI: masked values, reveal, restricted filters, break-glass banner), [PR155](https://github.com/QRun-IO/qqq-frontend-next/pull/155) (live job view, header Jobs tray and Jobs page), [PR156](https://github.com/QRun-IO/qqq-frontend-next/pull/156) (large-table acceptance fixture declares its default columns)). RC26 added tables that open with a short default column set instead of every field ([PR142](https://github.com/QRun-IO/qqq-frontend-next/pull/142) (tables open with a short default column set)). RC25 added an assistant panel that never blocks asking, re-attaches after refresh, shows live progress and can always Stop ([PR151](https://github.com/QRun-IO/qqq-frontend-next/pull/151) (assistant: never block asking, re-attach after refresh, live progress and Stop)). RC24 added the tablet record-tab fix, empty tables that explain themselves, a Full access checkbox in the policy editor and homes without duplicate card grids ([PR149](https://github.com/QRun-IO/qqq-frontend-next/pull/149) (record section tabs wrap instead of folding into More; tablet regression from PR144), [PR150](https://github.com/QRun-IO/qqq-frontend-next/pull/150) (empty tables explain what they are for and how rows get created), [PR148](https://github.com/QRun-IO/qqq-frontend-next/pull/148) (Full access checkbox in the authentik policy editor), [PR145](https://github.com/QRun-IO/qqq-frontend-next/pull/145) (no automatic card grids on homes that declare widgets)). RC23 added help everywhere, the Preferences page, contingent possible values in forms, search typeahead and System/Light/Dark theming ([PR127](https://github.com/QRun-IO/qqq-frontend-next/pull/127) (help everywhere: help icons, long-help panel, help search, author links), [PR121](https://github.com/QRun-IO/qqq-frontend-next/pull/121) (Preferences page, notification settings drawer and section actions), [PR120](https://github.com/QRun-IO/qqq-frontend-next/pull/120) (contingent possible values across forms, bulk edit, processes and filters), [PR143](https://github.com/QRun-IO/qqq-frontend-next/pull/143) (typeahead suggestions from qbit-quick-search in the global search box), [PR140](https://github.com/QRun-IO/qqq-frontend-next/pull/140) (authentik policy editor feature permissions and member preview), [PR138](https://github.cLine truncated
|
||||
|
||||
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.
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Next UI 1.0.0-RC.21 — preparation
|
||||
|
||||
> **RC21 was never published.** Its tag build failed the UI API off-spec ceiling test, so the Maven Central and GHCR publishes were cancelled. Its changes ship in [RC22](1.0.0-RC.22.md).
|
||||
|
||||
At this preparation checkpoint RC21 **is not yet published**. [RC20](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.20) remains the latest verified public candidate and is immutable. Planned artifacts are `com.kingsrook.qqq:qqq-frontend-next:1.0.0-RC.21` and `ghcr.io/qrun-io/qqq-frontend-next:1.0.0-RC.21`. 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
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
# Next UI 1.0.0-RC.22 — preparation
|
||||
|
||||
At this preparation checkpoint RC22 **is not yet published**. [RC20](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.20) remains the latest verified public candidate and is immutable. RC21 was tagged but never published (its tag build failed the off-spec ceiling test and publishing was cancelled); its content is included here. Planned artifacts are `com.kingsrook.qqq:qqq-frontend-next:1.0.0-RC.22` and `ghcr.io/qrun-io/qqq-frontend-next:1.0.0-RC.22`. 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 `48e81b4` (merge of [PR128](https://github.com/QRun-IO/qqq-frontend-next/pull/128)). Merged since RC20: [PR113](https://github.com/QRun-IO/qqq-frontend-next/pull/113) (collaboration record panel: comments, files, activity, tags and follow), [PR128](https://github.com/QRun-IO/qqq-frontend-next/pull/128) (collaboration: realtime presence, conflict review, sharing, tasks and My work), [PR106](https://github.com/QRun-IO/qqq-frontend-next/pull/106) (data-analysis pivot / group-by explorer), [PR118](https://github.com/QRun-IO/qqq-frontend-next/pull/118) (data-analysis compare, correlation, crosstab and shift explorers), [PR123](https://github.com/QRun-IO/qqq-frontend-next/pull/123) (data-analysis save, share, star, export and watch), [PR126](https://github.com/QRun-IO/qqq-frontend-next/pull/126) (run processes in background, background-job banner, widget refresh), [PR125](https://github.com/QRun-IO/qqq-frontend-next/pull/125) (attach a file in the assistant panel for bulk load), [PR132](https://github.com/QRun-IO/qqq-frontend-next/pull/132) (vendored alerts and sql-workbench extension documents, off-spec 46 to 21), [PR131](https://github.com/QRun-IO/qqq-frontend-next/pull/131) (off-spec ceiling for POST /uploads), [PR129](https://github.com/QRun-IO/qqq-frontend-next/pull/129) (chart data table overflow and doubled widget section headings).
|
||||
|
||||
## Backend requirements
|
||||
|
||||
QQQ **4.1.0-RC.6** or later. The collaboration screens need qbit-collaboration **0.1.0-RC.1** or later. The data-analysis explorers need qbit-data-analysis **0.1.0-RC.1** or later (extension 1.4.0). Assistant file attach uses the bulk-load UI API operations in QQQ **4.1.0-RC.7** and the tools in qbit-assistant **0.1.0-RC.10**; the upload endpoint (qqq#1181) is not yet released, so an attach attempt fails with a toast until it ships. Background process runs appear only when the server advertises `durableJobEligible` (qqq#1178 and qqq#1181); against older servers nothing new shows. RC22 does not publish QQQ 4.1.
|
||||
|
||||
## Features
|
||||
|
||||
- **Collaboration** ([PR113](https://github.com/QRun-IO/qqq-frontend-next/pull/113), [PR128](https://github.com/QRun-IO/qqq-frontend-next/pull/128)): record panel (comments, files, activity), tags and follow; realtime presence, conflict review, sharing, tasks and My work.
|
||||
- **Data analysis** ([PR106](https://github.com/QRun-IO/qqq-frontend-next/pull/106), [PR118](https://github.com/QRun-IO/qqq-frontend-next/pull/118), [PR123](https://github.com/QRun-IO/qqq-frontend-next/pull/123)): pivot / group-by, compare, correlation, crosstab and shift explorers; save, share, star, export and watch.
|
||||
- **Background process runs** ([PR126](https://github.com/QRun-IO/qqq-frontend-next/pull/126)): run in background, background-job banner, widget refresh.
|
||||
- **Assistant file attach** ([PR125](https://github.com/QRun-IO/qqq-frontend-next/pull/125)) for assisted bulk load.
|
||||
|
||||
## Product fixes
|
||||
|
||||
- Chart data table overflow and doubled widget section headings ([PR129](https://github.com/QRun-IO/qqq-frontend-next/pull/129)).
|
||||
|
||||
## UI API contract
|
||||
|
||||
- Vendored alerts and sql-workbench extension documents; the off-spec list drops from 46 to 21 ([PR132](https://github.com/QRun-IO/qqq-frontend-next/pull/132), [PR131](https://github.com/QRun-IO/qqq-frontend-next/pull/131)).
|
||||
|
||||
## Bundle budget
|
||||
|
||||
Report-only (owner decision 2026-10-13): sizes are measured and reported; structural problems still fail.
|
||||
|
||||
## Verification checkpoint
|
||||
|
||||
PR checks on the RC22 preparation source are recorded on the preparation PR; tag acceptance and public-artifact verification are recorded in the RC22 GitHub release after delivery.
|
||||
|
||||
## Remaining RC limits
|
||||
|
||||
The RC20 limits carry forward unchanged; see [#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). Collaboration task assignees show the user id until qbit-collaboration adds a display name.
|
||||
@@ -0,0 +1,43 @@
|
||||
# Next UI 1.0.0-RC.23 — preparation
|
||||
|
||||
At this preparation checkpoint RC23 **is not yet published**. [RC22](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.22) remains the latest verified public candidate and is immutable. Planned artifacts are `com.kingsrook.qqq:qqq-frontend-next:1.0.0-RC.23` and `ghcr.io/qrun-io/qqq-frontend-next:1.0.0-RC.23`. 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 `39b8f8b` (merge of [PR127](https://github.com/QRun-IO/qqq-frontend-next/pull/127)). Merged since RC22: [PR127](https://github.com/QRun-IO/qqq-frontend-next/pull/127) (help everywhere: help icons, long-help panel, help search, author links), [PR121](https://github.com/QRun-IO/qqq-frontend-next/pull/121) (Preferences page, notification settings drawer and section actions), [PR120](https://github.com/QRun-IO/qqq-frontend-next/pull/120) (contingent possible values across forms, bulk edit, processes and filters), [PR143](https://github.com/QRun-IO/qqq-frontend-next/pull/143) (typeahead suggestions from qbit-quick-search in the global search box), [PR140](https://github.com/QRun-IO/qqq-frontend-next/pull/140) (authentik policy editor feature permissions and member preview), [PR138](https://github.com/QRun-IO/qqq-frontend-next/pull/138) (theme defaults to the OS color scheme), [PR144](https://github.com/QRun-IO/qqq-frontend-next/pull/144) (record section tabs fold into a More menu), [PR139](https://github.com/QRun-IO/qqq-frontend-next/pull/139) (breadcrumb without inner horizontal scroll), [PR146](https://github.com/QRun-IO/qqq-frontend-next/pull/146) (app-nested URLs redirect to their canonical path), [PR137](https://github.com/QRun-IO/qqq-frontend-next/pull/137) (realtime client isolated between tests), [PR68](https://github.com/QRun-IO/qqq-frontend-next/pull/68) (rapidoc 10.1.0), [PR99](https://github.com/QRun-IO/qqq-frontend-next/pull/99) (GitHub Actions updates).
|
||||
|
||||
## Backend requirements
|
||||
|
||||
Everything in RC22 needs QQQ **4.1.0-RC.6** or later. The new screens use QQQ **4.1.0-RC.7** features: help everywhere needs core typed help and help bundles (qqq#1177, qqq#1198); the Preferences page needs `/qqq/v1/preferences` (qqq#1162); contingent possible values need qqq#1175. Against RC.6 these screens stay hidden or fall back to the previous behaviour. Search typeahead needs qbit-quick-search **1.1.0-RC.8** or later (the `suggest` endpoint); without it the global search box behaves as before. The authentik policy editor additions need qbit-authentik 0.1.0-RC.4 or later. RC23 does not publish QQQ 4.1.
|
||||
|
||||
## Features
|
||||
|
||||
- **Help everywhere** ([PR127](https://github.com/QRun-IO/qqq-frontend-next/pull/127)): help icons, a long-help panel, help search and author links, driven by core typed help.
|
||||
- **Preferences** ([PR121](https://github.com/QRun-IO/qqq-frontend-next/pull/121)): Preferences page, notification settings drawer and section actions.
|
||||
- **Contingent possible values** ([PR120](https://github.com/QRun-IO/qqq-frontend-next/pull/120)) across forms, bulk edit, processes and filters.
|
||||
- **Search typeahead** ([PR143](https://github.com/QRun-IO/qqq-frontend-next/pull/143)) from the qbit-quick-search `suggest` endpoint.
|
||||
- **Theme** ([PR138](https://github.com/QRun-IO/qqq-frontend-next/pull/138)): defaults to the OS color scheme (System / Light / Dark).
|
||||
- **Authentik** ([PR140](https://github.com/QRun-IO/qqq-frontend-next/pull/140)): policy editor feature permissions and member preview.
|
||||
|
||||
## Product fixes
|
||||
|
||||
- Record section tabs fold into a More menu instead of scrolling ([PR144](https://github.com/QRun-IO/qqq-frontend-next/pull/144)).
|
||||
- Breadcrumb without inner horizontal scroll ([PR139](https://github.com/QRun-IO/qqq-frontend-next/pull/139)).
|
||||
- App-nested URLs redirect to their canonical path ([PR146](https://github.com/QRun-IO/qqq-frontend-next/pull/146)).
|
||||
|
||||
## Browser testing and infrastructure
|
||||
|
||||
- The realtime client is isolated between tests, which fixes the metadata dedupe flake ([PR137](https://github.com/QRun-IO/qqq-frontend-next/pull/137)).
|
||||
- Dependency updates: rapidoc 10.1.0 ([PR68](https://github.com/QRun-IO/qqq-frontend-next/pull/68)), GitHub Actions ([PR99](https://github.com/QRun-IO/qqq-frontend-next/pull/99)).
|
||||
|
||||
## Bundle budget
|
||||
|
||||
Report-only (owner decision 2026-10-13): sizes are measured and reported; structural problems still fail.
|
||||
|
||||
## Verification checkpoint
|
||||
|
||||
PR checks on the RC23 preparation source are recorded on the preparation PR; tag acceptance and public-artifact verification are recorded in the RC23 GitHub release after delivery.
|
||||
|
||||
## Remaining RC limits
|
||||
|
||||
The RC22 limits carry forward unchanged; see [#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).
|
||||
@@ -0,0 +1,37 @@
|
||||
# Next UI 1.0.0-RC.24 — preparation
|
||||
|
||||
At this preparation checkpoint RC24 **is not yet published**. [RC23](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.23) remains the latest public candidate and is immutable. Planned artifacts are `com.kingsrook.qqq:qqq-frontend-next:1.0.0-RC.24` and `ghcr.io/qrun-io/qqq-frontend-next:1.0.0-RC.24`. 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 `3ab4815` (merge of [PR150](https://github.com/QRun-IO/qqq-frontend-next/pull/150)). Merged since RC23: [PR149](https://github.com/QRun-IO/qqq-frontend-next/pull/149) (record section tabs wrap instead of folding into More; tablet regression from PR144), [PR150](https://github.com/QRun-IO/qqq-frontend-next/pull/150) (empty tables explain what they are for and how rows get created), [PR148](https://github.com/QRun-IO/qqq-frontend-next/pull/148) (Full access checkbox in the authentik policy editor), [PR145](https://github.com/QRun-IO/qqq-frontend-next/pull/145) (no automatic card grids on homes that declare widgets).
|
||||
|
||||
## Why RC24
|
||||
|
||||
RC23's tag real-backend acceptance failed on two tablet shards in both attempts (REC-004 record sections by touch and keyboard, REC-019 zoned date-times, NAV-015 document title). [PR149](https://github.com/QRun-IO/qqq-frontend-next/pull/149) restores wrapping section tabs (the More menu from PR144 is removed), deflakes NAV-015 and adds tablet to the full-acceptance label.
|
||||
|
||||
## Backend requirements
|
||||
|
||||
Unchanged from RC23: QQQ **4.1.0-RC.6** or later for everything in RC22, and **4.1.0-RC.7** for help everywhere, Preferences and contingent possible values. The Full access checkbox needs qbit-authentik **0.1.0-RC.5** or later (full-access `*` statement). Empty-table help uses core typed help (QQQ 4.1.0-RC.7); against older cores the table shows the previous empty state. RC24 does not publish QQQ 4.1.
|
||||
|
||||
## Features
|
||||
|
||||
- **Empty tables explain themselves** ([PR150](https://github.com/QRun-IO/qqq-frontend-next/pull/150)): what the table is for and how rows get created, from core help.
|
||||
- **Full access** ([PR148](https://github.com/QRun-IO/qqq-frontend-next/pull/148)): a Full access checkbox in the authentik policy editor.
|
||||
|
||||
## Product fixes
|
||||
|
||||
- Record section tabs wrap instead of folding into a More menu; fixes the RC23 tablet regression ([PR149](https://github.com/QRun-IO/qqq-frontend-next/pull/149)).
|
||||
- Homes that declare widgets no longer get automatic card grids ([PR145](https://github.com/QRun-IO/qqq-frontend-next/pull/145)).
|
||||
|
||||
## Bundle budget
|
||||
|
||||
Report-only (owner decision 2026-10-13).
|
||||
|
||||
## Verification checkpoint
|
||||
|
||||
PR checks on the RC24 preparation source are recorded on the preparation PR; tag acceptance and public-artifact verification are recorded in the RC24 GitHub release after delivery.
|
||||
|
||||
## Remaining RC limits
|
||||
|
||||
The RC23 limits carry forward unchanged; see [#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).
|
||||
@@ -0,0 +1,27 @@
|
||||
# Next UI 1.0.0-RC.25 — preparation
|
||||
|
||||
At this preparation checkpoint RC25 **is not yet published**. [RC24](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.24) remains the latest public candidate and is immutable. Planned artifacts are `com.kingsrook.qqq:qqq-frontend-next:1.0.0-RC.25` and `ghcr.io/qrun-io/qqq-frontend-next:1.0.0-RC.25`. 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 `70594c4` (merge of [PR151](https://github.com/QRun-IO/qqq-frontend-next/pull/151)). Merged since RC24: [PR151](https://github.com/QRun-IO/qqq-frontend-next/pull/151) (assistant: never block asking, re-attach after refresh, live progress and Stop).
|
||||
|
||||
## Backend requirements
|
||||
|
||||
Unchanged from RC24. The assistant changes pair with qbit-assistant **0.1.0-RC.13** (durable turns: lease, heartbeat, cross-pod Stop); with older assistants the panel behaves as before. RC25 does not publish QQQ 4.1.
|
||||
|
||||
## Features
|
||||
|
||||
- **Assistant panel** ([PR151](https://github.com/QRun-IO/qqq-frontend-next/pull/151)): asking is never blocked while a turn runs, the panel re-attaches to a running turn after refresh, shows live progress, and Stop is always available.
|
||||
|
||||
## Bundle budget
|
||||
|
||||
Report-only (owner decision 2026-10-13).
|
||||
|
||||
## Verification checkpoint
|
||||
|
||||
PR checks on the RC25 preparation source are recorded on the preparation PR; tag acceptance and public-artifact verification are recorded in the RC25 GitHub release after delivery.
|
||||
|
||||
## Remaining RC limits
|
||||
|
||||
The RC24 limits carry forward unchanged; see [#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).
|
||||
@@ -0,0 +1,27 @@
|
||||
# Next UI 1.0.0-RC.26 — preparation
|
||||
|
||||
At this preparation checkpoint RC26 **is not yet published**. [RC25](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.25) remains the latest public candidate and is immutable. Planned artifacts are `com.kingsrook.qqq:qqq-frontend-next:1.0.0-RC.26` and `ghcr.io/qrun-io/qqq-frontend-next:1.0.0-RC.26`. 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 `caa89dc` (merge of [PR142](https://github.com/QRun-IO/qqq-frontend-next/pull/142)). Merged since RC25: [PR142](https://github.com/QRun-IO/qqq-frontend-next/pull/142) (tables open with a short default column set).
|
||||
|
||||
## Backend requirements
|
||||
|
||||
Unchanged from RC25. RC26 does not publish QQQ 4.1.
|
||||
|
||||
## Features
|
||||
|
||||
- **Default list columns** ([PR142](https://github.com/QRun-IO/qqq-frontend-next/pull/142)): with no saved view, a table opens with a short default column set (primary key, then the first-tier section fields, capped at 6, skipping heavy, text, HTML, BLOB and JSON fields) instead of every field, so the grid no longer scrolls sideways. Fixes [#114](https://github.com/QRun-IO/qqq-frontend-next/issues/114) (kof22 admin UX audit, High).
|
||||
|
||||
## Bundle budget
|
||||
|
||||
Report-only (owner decision 2026-10-13).
|
||||
|
||||
## Verification checkpoint
|
||||
|
||||
PR checks on the RC26 preparation source are recorded on the preparation PR; tag acceptance and public-artifact verification are recorded in the RC26 GitHub release after delivery.
|
||||
|
||||
## Remaining RC limits
|
||||
|
||||
The RC25 limits carry forward unchanged; see [#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).
|
||||
@@ -0,0 +1,29 @@
|
||||
# Next UI 1.0.0-RC.27 — preparation
|
||||
|
||||
At this preparation checkpoint RC27 **is not yet published**. [RC26](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.26) remains the latest public candidate and is immutable. Planned artifacts are `com.kingsrook.qqq:qqq-frontend-next:1.0.0-RC.27` and `ghcr.io/qrun-io/qqq-frontend-next:1.0.0-RC.27`. 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 `427a037` (merge of [PR155](https://github.com/QRun-IO/qqq-frontend-next/pull/155)). Merged since RC26: [PR124](https://github.com/QRun-IO/qqq-frontend-next/pull/124) (secured-data UI: masked values, reveal, restricted filters, break-glass banner), [PR155](https://github.com/QRun-IO/qqq-frontend-next/pull/155) (live job view, header Jobs tray and Jobs page), [PR156](https://github.com/QRun-IO/qqq-frontend-next/pull/156) (large-table acceptance fixture declares its default columns).
|
||||
|
||||
## Backend requirements
|
||||
|
||||
Unchanged from RC26 for existing screens. The Jobs tray and live job view need QQQ **4.1.0-RC.8** (jobs UI API, UI API 1.4.0); against older cores the Jobs button stays hidden. The secured-data UI renders what QQQ **4.1.0-RC.7** field access tiers enforce and needs qbit-secured-data **0.1.0-RC.1** for reveal and break-glass. RC27 does not publish QQQ 4.1.
|
||||
|
||||
## Features
|
||||
|
||||
- **Secured-data UI** ([PR124](https://github.com/QRun-IO/qqq-frontend-next/pull/124)): masked values shown with a lock chip, reveal with reason, restricted filters, and a break-glass banner. The server masks values; the client never receives the real value.
|
||||
- **Jobs** ([PR155](https://github.com/QRun-IO/qqq-frontend-next/pull/155)): a header Jobs button next to alerts, a live job view with progress, phases and log, the ability to leave a job running, and a Jobs page.
|
||||
- Acceptance: the large-table fixture declares its 40 default columns after PR142 ([PR156](https://github.com/QRun-IO/qqq-frontend-next/pull/156)).
|
||||
|
||||
## Bundle budget
|
||||
|
||||
Report-only (owner decision 2026-10-13).
|
||||
|
||||
## Verification checkpoint
|
||||
|
||||
PR checks on the RC27 preparation source are recorded on the preparation PR; tag acceptance and public-artifact verification are recorded in the RC27 GitHub release after delivery.
|
||||
|
||||
## Remaining RC limits
|
||||
|
||||
The RC26 limits carry forward unchanged; see [#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).
|
||||
@@ -0,0 +1,27 @@
|
||||
# Next UI 1.0.0-RC.28 — preparation
|
||||
|
||||
At this preparation checkpoint RC28 **is not yet published**. [RC27](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.27) remains the latest public candidate and is immutable. Planned artifacts are `com.kingsrook.qqq:qqq-frontend-next:1.0.0-RC.28` and `ghcr.io/qrun-io/qqq-frontend-next:1.0.0-RC.28`. 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 `16699ae` (merge of [PR141](https://github.com/QRun-IO/qqq-frontend-next/pull/141)). Merged since RC27: [PR141](https://github.com/QRun-IO/qqq-frontend-next/pull/141) (zero off-spec UI API calls; OFF_SPEC_CEILING 0; minimum UI API 1.4.0).
|
||||
|
||||
## Backend requirements
|
||||
|
||||
**Requires QQQ 4.1.0-RC.9 or later** (UI API 1.4.0 operations; RC.8's `GET /table/{t}/apis` was shadowed by the record route, fixed in qqq#1216). RC28 does not publish QQQ 4.1.
|
||||
|
||||
## Features
|
||||
|
||||
- **Zero off-spec calls** ([PR141](https://github.com/QRun-IO/qqq-frontend-next/pull/141)): the form adjuster, table API discovery, table API documents and the remaining legacy routes now use spec'd `/qqq/v1` operations. `OFF_SPEC_CEILING` is 0.
|
||||
|
||||
## Bundle budget
|
||||
|
||||
Report-only (owner decision 2026-10-13).
|
||||
|
||||
## Verification checkpoint
|
||||
|
||||
PR checks on the RC28 preparation source are recorded on the preparation PR; tag acceptance and public-artifact verification are recorded in the RC28 GitHub release after delivery.
|
||||
|
||||
## Remaining RC limits
|
||||
|
||||
The RC27 limits carry forward unchanged; see [#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).
|
||||
@@ -0,0 +1,27 @@
|
||||
# Next UI 1.0.0-RC.29 — preparation
|
||||
|
||||
At this preparation checkpoint RC29 **is not yet published**. [RC28](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.28) remains the latest public candidate and is immutable. Planned artifacts are `com.kingsrook.qqq:qqq-frontend-next:1.0.0-RC.29` and `ghcr.io/qrun-io/qqq-frontend-next:1.0.0-RC.29`. 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 `abfbbb2` (merge of [PR136](https://github.com/QRun-IO/qqq-frontend-next/pull/136)). Merged since RC28: [PR136](https://github.com/QRun-IO/qqq-frontend-next/pull/136) (EDI mapper page on the qbit-edi edi extension, all calls on spec).
|
||||
|
||||
## Backend requirements
|
||||
|
||||
Requires QQQ **4.1.0-RC.9** or later (as RC28). The EDI mapper needs qbit-edi with the `edi` UI API extension (qbit-edi#15); without it the Open Mapper action stays hidden. RC29 does not publish QQQ 4.1.
|
||||
|
||||
## Features
|
||||
|
||||
- **EDI mapper** ([PR136](https://github.com/QRun-IO/qqq-frontend-next/pull/136)): an Open Mapper action on `edi_map` records opens a mapper page built on the vendored qbit-edi `edi` UI API extension, with generated types and an API client. Every call is on spec.
|
||||
|
||||
## Bundle budget
|
||||
|
||||
Report-only (owner decision 2026-10-13).
|
||||
|
||||
## Verification checkpoint
|
||||
|
||||
PR checks on the RC29 preparation source are recorded on the preparation PR; tag acceptance and public-artifact verification are recorded in the RC29 GitHub release after delivery.
|
||||
|
||||
## Remaining RC limits
|
||||
|
||||
The RC28 limits carry forward unchanged; see [#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).
|
||||
@@ -0,0 +1,34 @@
|
||||
# Next UI 1.0.0-RC.30 — preparation
|
||||
|
||||
At this preparation checkpoint RC30 **is not yet published**. [RC29](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.29) remains the latest public candidate and is immutable. Planned artifacts are `com.kingsrook.qqq:qqq-frontend-next:1.0.0-RC.30` and `ghcr.io/qrun-io/qqq-frontend-next:1.0.0-RC.30`. 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 after [PR160](https://github.com/QRun-IO/qqq-frontend-next/pull/160), which this preparation depends on. Merged since RC29: [PR160](https://github.com/QRun-IO/qqq-frontend-next/pull/160) (optional Jam QA capture on the qbit-qa-capture `qa-capture` extension, all calls on spec).
|
||||
|
||||
## Backend requirements
|
||||
|
||||
RC30 needs QQQ **4.1.0-RC.9** or later, the same as RC29. QA capture also needs qbit-qa-capture with the `qa-capture` 1.0.0 UI API extension. Without it the UI makes no QA capture call and loads no Jam code. The vendored `qa-capture.json` must match the published QBit fragment before tagging. RC30 does not publish QQQ 4.1.
|
||||
|
||||
## Features
|
||||
|
||||
- **QA capture (Jam)** ([PR160](https://github.com/QRun-IO/qqq-frontend-next/pull/160)), shown only to users the QBit enables:
|
||||
- Jam's Recording Links SDK is loaded on demand and initialized with sanitizers built from the server's rules (headers, JSON and form fields, query parameters, value patterns, dropped paths, fail closed).
|
||||
- Jam receives metadata with the page context and the last request ids.
|
||||
- "Report a bug" and "QA reports" are in the user menu, and a "QA mode on" badge appears in the header.
|
||||
- Password, reveal and secured-data values are blurred in recordings.
|
||||
- Recording Link `jam-*` parameters are kept through sign-in.
|
||||
- The standalone image adds the Jam CSP origins when `QQQ_QA_CAPTURE_CSP=true`.
|
||||
- Dependencies: `@jam.dev/recording-links` 0.4.0 and `@jam.dev/sdk` 1.0.1, pinned. See [QA capture](../QA-CAPTURE.md).
|
||||
|
||||
## Bundle budget
|
||||
|
||||
Report-only (owner decision 2026-10-13). PR160 measured +3.4 to +4.2 KB gzip first-load JS per route and +9.2 KB gzip total JS. The Jam chunks load only for enabled users.
|
||||
|
||||
## Verification checkpoint
|
||||
|
||||
PR checks on the RC30 preparation source are recorded on the preparation PR; tag acceptance and public-artifact verification are recorded in the RC30 GitHub release after delivery. Acceptance row SEC-050 (QA capture) is excluded pending owner approval until qbit-qa-capture has a real-backend fixture; Vitest covers it meanwhile.
|
||||
|
||||
## Remaining RC limits
|
||||
|
||||
The RC29 limits carry forward unchanged; see [#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).
|
||||
@@ -0,0 +1,27 @@
|
||||
# Next UI 1.0.0-RC.31 — preparation
|
||||
|
||||
At this preparation checkpoint RC31 **is not yet published**. [RC30](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.30) remains the latest public candidate and is immutable. Planned artifacts are `com.kingsrook.qqq:qqq-frontend-next:1.0.0-RC.31` and `ghcr.io/qrun-io/qqq-frontend-next:1.0.0-RC.31`. 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 `9aff198` (merge of [PR162](https://github.com/QRun-IO/qqq-frontend-next/pull/162)). Merged since RC30: [PR162](https://github.com/QRun-IO/qqq-frontend-next/pull/162) (adopt QQQ UI API 1.6.0: clientCapabilities, field validation, error schema, v1 branding).
|
||||
|
||||
## Backend requirements
|
||||
|
||||
**Requires QQQ 4.1.0-RC.15 or later** (UI API 1.6.0). RC31 does not publish QQQ 4.1.
|
||||
|
||||
## Features
|
||||
|
||||
- **UI API 1.6.0** ([PR162](https://github.com/QRun-IO/qqq-frontend-next/pull/162)): vendors the core 1.6.0 and API-aware documents and the updated qbit extensions (alerts 1.2.0, assistant 1.3.0, collaboration 1.1.0, data-analysis 1.5.0, edi 1.1.0, esb 1.1.0, quick-search 1.1.0, secured-data 1.1.0, sql-workbench 1.1.0); uses table client capabilities, server field validation, the single error schema and v1 branding. The off-spec ceiling stays 0.
|
||||
|
||||
## Bundle budget
|
||||
|
||||
Report-only (owner decision 2026-10-13).
|
||||
|
||||
## Verification checkpoint
|
||||
|
||||
PR checks on the RC31 preparation source are recorded on the preparation PR; tag acceptance and public-artifact verification are recorded in the RC31 GitHub release after delivery.
|
||||
|
||||
## Remaining RC limits
|
||||
|
||||
The RC30 limits carry forward unchanged; see [#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).
|
||||
@@ -0,0 +1,27 @@
|
||||
# Next UI 1.0.0-RC.32 — preparation
|
||||
|
||||
At this preparation checkpoint RC32 **is not yet published**. [RC31](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.31) remains the latest public candidate and is immutable. Planned artifacts are `com.kingsrook.qqq:qqq-frontend-next:1.0.0-RC.32` and `ghcr.io/qrun-io/qqq-frontend-next:1.0.0-RC.32`. 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 `b24b74e` (merge of [PR165](https://github.com/QRun-IO/qqq-frontend-next/pull/165)). Merged since RC31: [PR165](https://github.com/QRun-IO/qqq-frontend-next/pull/165) (What's new page and user-menu entry, release-notes UI API 1.0.0).
|
||||
|
||||
## Backend requirements
|
||||
|
||||
Requires QQQ **4.1.0-RC.15** or later for the UI API 1.6.0 features (as RC31). What's new needs qbit-release-notes **0.1.0-RC.1** (release-notes UI API extension 1.0.0); without it the feature hides itself. RC32 does not publish QQQ 4.1.
|
||||
|
||||
## Features
|
||||
|
||||
- **What's new** ([PR165](https://github.com/QRun-IO/qqq-frontend-next/pull/165)): an `/app/whats-new` page and a user-menu entry with an unread count, on the release-notes UI API extension 1.0.0 (vendored). The page is a separate lazy route; off-spec calls stay at 0.
|
||||
|
||||
## Bundle budget
|
||||
|
||||
Report-only (owner decision 2026-10-13).
|
||||
|
||||
## Verification checkpoint
|
||||
|
||||
PR checks on the RC32 preparation source are recorded on the preparation PR; tag acceptance and public-artifact verification are recorded in the RC32 GitHub release after delivery.
|
||||
|
||||
## Remaining RC limits
|
||||
|
||||
The RC31 limits carry forward unchanged; see [#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).
|
||||
@@ -0,0 +1,31 @@
|
||||
# Next UI 1.0.0-RC.33 — preparation
|
||||
|
||||
At this preparation checkpoint RC33 **is not yet published**. [RC32](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.32) remains the latest public candidate and is immutable. Planned artifacts are `com.kingsrook.qqq:qqq-frontend-next:1.0.0-RC.33` and `ghcr.io/qrun-io/qqq-frontend-next:1.0.0-RC.33`. 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 `b4b59ce` (merge of [PR170](https://github.com/QRun-IO/qqq-frontend-next/pull/170)). Merged since RC32: [PR168](https://github.com/QRun-IO/qqq-frontend-next/pull/168) (sidebar sections and favorites, one search, new header, single-key shortcut switch), [PR170](https://github.com/QRun-IO/qqq-frontend-next/pull/170) (empty assistant query results no longer crash the page).
|
||||
|
||||
## Backend requirements
|
||||
|
||||
Requires QQQ **4.1.0-RC.15** or later for the UI API 1.6.0 features (as RC32). The navigation works against any 1.x server (it infers section roles when absent); QQQ **4.1.0-RC.16** (UI API 1.7.0) adds the server-declared section roles, section apps and search keywords. RC33 does not publish QQQ 4.1.
|
||||
|
||||
## Features
|
||||
|
||||
- **Navigation** ([PR168](https://github.com/QRun-IO/qqq-frontend-next/pull/168)): sidebar sections and favorites, one search, a header with Search, Ask, Jobs, Notifications, Help and Account, a single-key shortcut switch and Shift+D.
|
||||
|
||||
## Product fixes
|
||||
|
||||
- Empty assistant query results no longer crash the page ([PR170](https://github.com/QRun-IO/qqq-frontend-next/pull/170)).
|
||||
|
||||
## Bundle budget
|
||||
|
||||
Report-only (owner decision 2026-10-13).
|
||||
|
||||
## Verification checkpoint
|
||||
|
||||
PR checks on the RC33 preparation source are recorded on the preparation PR; tag acceptance and public-artifact verification are recorded in the RC33 GitHub release after delivery.
|
||||
|
||||
## Remaining RC limits
|
||||
|
||||
The RC32 limits carry forward unchanged; see [#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).
|
||||
@@ -0,0 +1,27 @@
|
||||
# Next UI 1.0.0-RC.34 — preparation
|
||||
|
||||
At this preparation checkpoint RC34 **is not yet published**. [RC33](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.33) remains the latest public candidate and is immutable. Planned artifacts are `com.kingsrook.qqq:qqq-frontend-next:1.0.0-RC.34` and `ghcr.io/qrun-io/qqq-frontend-next:1.0.0-RC.34`. 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 `68a2b9d` (merge of [PR172](https://github.com/QRun-IO/qqq-frontend-next/pull/172)). Merged since RC33: [PR172](https://github.com/QRun-IO/qqq-frontend-next/pull/172) (layered error boundaries, zod payload repair, streaming can no longer lock the assistant panel).
|
||||
|
||||
## Backend requirements
|
||||
|
||||
Requires QQQ **4.1.0-RC.15** or later for the UI API 1.6.0 features (as RC33). The navigation works against any 1.x server (it infers section roles when absent); QQQ **4.1.0-RC.16** (UI API 1.7.0) adds the server-declared section roles, section apps and search keywords. RC34 does not publish QQQ 4.1.
|
||||
|
||||
## Product fixes
|
||||
|
||||
- **Fail soft** ([PR172](https://github.com/QRun-IO/qqq-frontend-next/pull/172)): layered error boundaries keep a failing widget, section or panel from taking down the page; assistant payloads are validated and repaired with zod; a stream can no longer lock the assistant panel. Pairs with qbit-assistant 0.1.0-RC.22 (fail-soft server, extension 1.5.0).
|
||||
|
||||
## Bundle budget
|
||||
|
||||
Report-only (owner decision 2026-10-13).
|
||||
|
||||
## Verification checkpoint
|
||||
|
||||
PR checks on the RC34 preparation source are recorded on the preparation PR; tag acceptance and public-artifact verification are recorded in the RC34 GitHub release after delivery.
|
||||
|
||||
## Remaining RC limits
|
||||
|
||||
The RC33 limits carry forward unchanged; see [#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).
|
||||
@@ -0,0 +1,27 @@
|
||||
# Next UI 1.0.0-RC.35 — preparation
|
||||
|
||||
At this preparation checkpoint RC35 **is not yet published**. [RC34](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.34) remains the latest public candidate and is immutable. Planned artifacts are `com.kingsrook.qqq:qqq-frontend-next:1.0.0-RC.35` and `ghcr.io/qrun-io/qqq-frontend-next:1.0.0-RC.35`. 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 `ba38367` (merge of [PR164](https://github.com/QRun-IO/qqq-frontend-next/pull/164)). Merged since RC34: [PR164](https://github.com/QRun-IO/qqq-frontend-next/pull/164) (profile photo uploader and avatars everywhere, UI API 1.8.0).
|
||||
|
||||
## Backend requirements
|
||||
|
||||
Requires QQQ **4.1.0-RC.15** or later for the UI API 1.6.0 features (as RC34). Profiles and avatars need QQQ **4.1.0-RC.17** (UI API 1.8.0). RC35 does not publish QQQ 4.1.
|
||||
|
||||
## Features
|
||||
|
||||
- **Profiles and avatars** ([PR164](https://github.com/QRun-IO/qqq-frontend-next/pull/164)): a profile photo uploader on the Profile section and user avatars shown throughout the UI.
|
||||
|
||||
## Bundle budget
|
||||
|
||||
Report-only (owner decision 2026-10-13).
|
||||
|
||||
## Verification checkpoint
|
||||
|
||||
PR checks on the RC35 preparation source are recorded on the preparation PR; tag acceptance and public-artifact verification are recorded in the RC35 GitHub release after delivery.
|
||||
|
||||
## Remaining RC limits
|
||||
|
||||
The RC34 limits carry forward unchanged; see [#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).
|
||||
@@ -0,0 +1,27 @@
|
||||
# Next UI 1.0.0-RC.36 — preparation
|
||||
|
||||
At this preparation checkpoint RC36 **is not yet published**. [RC35](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.35) remains the latest public candidate and is immutable. Planned artifacts are `com.kingsrook.qqq:qqq-frontend-next:1.0.0-RC.36` and `ghcr.io/qrun-io/qqq-frontend-next:1.0.0-RC.36`. 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 `81a03ea` (merge of [PR175](https://github.com/QRun-IO/qqq-frontend-next/pull/175)). Merged since RC35: [PR175](https://github.com/QRun-IO/qqq-frontend-next/pull/175) (a11y: readable link text, 24px clear target, h2 widget titles, skip link focus).
|
||||
|
||||
## Backend requirements
|
||||
|
||||
Requires QQQ **4.1.0-RC.15** or later for the UI API 1.6.0 features (as RC35). Profiles and avatars need QQQ **4.1.0-RC.17** (UI API 1.8.0); against older servers avatars fall back to initials and the uploader stays hidden. RC36 does not publish QQQ 4.1.
|
||||
|
||||
## Product fixes
|
||||
|
||||
- **Accessibility** ([PR175](https://github.com/QRun-IO/qqq-frontend-next/pull/175)): link text meets 4.5:1 contrast through a new derived link color, clear buttons have 24px targets, widget titles are h2 headings, and the skip link takes focus correctly.
|
||||
|
||||
## Bundle budget
|
||||
|
||||
Report-only (owner decision 2026-10-13).
|
||||
|
||||
## Verification checkpoint
|
||||
|
||||
PR checks on the RC36 preparation source are recorded on the preparation PR; tag acceptance and public-artifact verification are recorded in the RC36 GitHub release after delivery.
|
||||
|
||||
## Remaining RC limits
|
||||
|
||||
The RC35 limits carry forward unchanged; see [#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).
|
||||
@@ -0,0 +1,11 @@
|
||||
# 1.0.0-RC.38 — prepared candidate
|
||||
|
||||
This candidate is not yet released. Merge and publication follow the compatible Core and Collaboration releases.
|
||||
|
||||
- Record file controls support previews, opening and downloading through the documented delivery APIs, including PDF previews.
|
||||
- Script authoring includes the isolated TypeScript editor, server lifecycle actions and record hook controls.
|
||||
- Process results support file delivery, PDF preview and ordered record lists through standard metadata.
|
||||
- Record queries respect authorization-filtered pagination; record layouts preserve section metadata.
|
||||
- The current permission editor and companion Tools workspace remain integrated.
|
||||
|
||||
The release uses the standard UI API and extension documents. Worker execution, storage configuration and printer/scanner qualification remain responsibilities of the respective QBits and host application. See [verification and release status](../QBIT-RELEASE-STATUS.md) and [known requirements](../QBIT-KNOWN-ISSUES.md).
|
||||
@@ -1,14 +1,30 @@
|
||||
# Next UI — unreleased
|
||||
|
||||
[RC21 preparation](1.0.0-RC.21.md) collects every change merged since [RC20](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.20). RC20 is published and immutable. Changes merged after the RC21 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).
|
||||
[RC36 preparation](1.0.0-RC.36.md) collects every change merged since [RC35](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.35). RC35 is published and immutable. Changes merged after the RC36 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).
|
||||
|
||||
## Features
|
||||
|
||||
None yet.
|
||||
- **Tools:** the `QQQ_TOOLS` widget provides a shared setup workspace for CLI/TUI, mobile scanners and Print Client, with platform-specific install commands, copyable scripts and instance connection details. An account-menu shortcut follows visible Tools metadata. Requires the Tools Qbit; existing device enrollment and permissions remain unchanged.
|
||||
|
||||
- **UI API 1.6.0 (QQQ 4.1.0-RC.15).** Next reads the typed `clientCapabilities` of table metadata for saved views, sharing, bulk processes and audits, applies `field.validation` (required, length, inclusive and exclusive bounds, patterns) before a request, reads client-neutral `uiSettings` setting by setting with the deprecated `materialDashboard` values as fallback, and shows the process error `traceId`. The full `materialDashboardTheme` still wins over the `uiSettings.theme` subset. Error messages keep their order (`userFacingError`, `error`, then the new `message`). Audits fall back to the permission-filtered instance metadata when a table's capability is unavailable, since RC.15 marks unaudited tables unavailable although explicit audits can exist. Off-spec calls stay at 0.
|
||||
- **What's new:** a paged release history with new badges, safe links and screenshots, notification deep links, and an unread count in the user menu. Uses UI API extension `release-notes` 1.0.0; requires qbit-release-notes 0.1.0-RC.1+.
|
||||
- **Profile photo and avatars everywhere** (QQQ 4.1.0-RC.17, UI API 1.8.0 `avatars`):
|
||||
- The core **Profile** preferences section renders generically. Its photo is a `FILE_UPLOAD` field with `valueIsUploadRef`, so you can drag and drop or choose a picture, crop it square (drag, arrow keys or the zoom slider), preview it and remove it. The cropped PNG goes to `POST /uploads`, and the ref saves with the form.
|
||||
- One `UserAvatar` (photo via one batched `POST /avatars/lookup` and `GET /avatars/{avatarId}`, otherwise initials) shows people in the user menu, presence, comments, mention candidates, the people picker, activity and your own assistant messages. A missing avatar is remembered for the session, so it isn't requested again.
|
||||
- **Privacy:** branding `gravatarDefault` no longer sends your email hash to Gravatar. A Gravatar appears only if you turn it on in your Profile, and the server serves it.
|
||||
- Image fields take a full form row at every width. e2e coverage is `tests/e2e/profile-avatars.spec.ts`, including the 6-width light/dark screenshot matrix.
|
||||
|
||||
## Product fixes
|
||||
|
||||
None yet.
|
||||
- **A preference action that connects another service takes you there.** A provider that starts an OAuth connection (Connect Slack, for example) answers with the address of the service's consent page in the action result's `message`, since the UI API has no redirect field. That address was shown as the text of a success toast: long, not clickable and gone in seconds. A message that is nothing but one absolute `https:` address is now where the browser goes, in the same tab, and is never shown. A message that only mentions an address is still a message. Plain `http:`, addresses with credentials and every other scheme are not followed.
|
||||
- **Preferences says how a connection ended.** A flow that left the dashboard returns to `/app/preferences?section=<name>&result=<code>` when it succeeded or `&error=<code>` when it did not. The section now shows the outcome once, above its form, as the section's label and the code as words (`Slack: connected.`, `Slack: authorization denied.`), as a status or an alert, and takes it off the address so a reload does not repeat it. Only a fixed lowercase code is read from the address, never a sentence.
|
||||
- **Links in process HTML look like links.** An `HTML` process component rendered links in the text color with no underline, so a link the backend put on a screen (a bulk load template, a "Continue to Slack" link) could not be told from the words around it. They now use the link color and an underline; a link's own inline style still wins.
|
||||
|
||||
- **The Ask button's activity dot sits on its icon.** While a reply was being written, the dot was placed in the bottom-right corner of the button's hit area, which has no visible edge and, from 1440 px, extends past the "Ask" label, so the dot floated under and to the right of the label. The dot and the unread badge are now positioned against the sparkles icon, at every width and with the panel open or closed. The bell and Jobs badges use the same anchor and keep their place.
|
||||
|
||||
- **Logout no longer races an in-flight request.** Logout ends the API client's session generation first: every request still in flight (axios and the realtime and assistant streams) is aborted, and any response of the old generation that still arrives is discarded, so none can refresh the `sessionId` cookie the logout expires or change client auth state (SEC-037, SEC-048).
|
||||
|
||||
- **The error banner says what failed, and no longer stacks.** The banner for a failed request used to read "Something went wrong" for every failure without an expected status, and a polling request that kept failing added one banner per attempt. It now reads `No response from the server`, `The server took too long to respond`, the server's own message, or `Request failed (HTTP 409)`, and names the request below it (`POST /qqq/v1/table/order/query`, without the query string, with ids shown as `{id}`). A repeat of the same failure updates the banner on screen; a lost connection shows one banner whichever requests met it. The 403, 404 and 5xx banners keep their wording and gain the request line; a 403 without a server message reads `Permission denied`. A cancelled request, an aborted `fetch` and a 401 show no banner. Each banner logs its status, axios code, error name and request to the console once (`[API] Request failed:`), never a message, body, header or query string.
|
||||
|
||||
## Browser testing and infrastructure
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# Jobs screenshots
|
||||
|
||||
The mocked browser spec is `tests/e2e/jobs-tray.spec.ts`. Capture the actual UI with:
|
||||
|
||||
```powershell
|
||||
$env:QQQ_E2E_SERVER='production'
|
||||
$env:QQQ_JOBS_SCREENSHOTS='docs/screenshots/jobs-tray'
|
||||
$env:QQQ_E2E_PORT='3117'
|
||||
pnpm exec playwright test tests/e2e/jobs-tray.spec.ts --workers=1 --reporter=list
|
||||
```
|
||||
|
||||
Captured 2026-10-14 from the production build (`QQQ_E2E_SERVER=production`), 1000 px tall:
|
||||
|
||||
| View | 390 light | 390 dark | 1440 light | 1440 dark |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Live job |  |  |  |  |
|
||||
| Open tray |  |  |  |  |
|
||||
| Jobs page |  |  |  |  |
|
||||
|
||||
The spec also checks horizontal overflow, popover bounds, keyboard focus restoration, axe at 1440
|
||||
light, and reduced motion; its lifecycle case runs an inline process, detaches it, watches it
|
||||
finish in the tray and reopens the finished view.
|
||||
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 32 KiB |
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"q-sequential-workflow.bundle.js","mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,CAAC;AACD,O;;;;;;;;;;;;;;ACVA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,6BAA6B;AAC7B;AACA;AACA;AACA;AACA;AACA;AACA,gBAAgB,SAAI,IAAI,SAAI;AAC5B;AACA,iDAAiD,OAAO;AACxD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,kDAAkD;AAClD;AACA;AACA;AACA,CAAC;AACyB;AAC1B,0C;;;;;;;;;;;;;;AClDA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,6BAA6B;AAC7B;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,gCAAgC,0CAA0C;AAC1E;AACA;AACA;AACA;AACA;AACA;AACA,4BAA4B,0BAA0B;AACtD;AACA;AACA;AACA;AACA;AACA,4BAA4B,4BAA4B;AACxD;AACA;AACA;AACA;AACA;AACA,CAAC;AACkB;AACnB,mC;;;;;;;;;;;;;;AC/DA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,6BAA6B;AAC7B;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aAAa;AACb;AACA,yCAAyC,iBAAiB;AAC1D;AACA;AACA;AACA;AACA,CAAC;AACyB;AAC1B,0C;;;;;;;;;;;;;;;;;;ACzDA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,6BAA6B;AAC7B;AACA;AACA;AACA;AACA;AACA;AACA,gBAAgB,SAAI,IAAI,SAAI;AAC5B;AACA,iDAAiD,OAAO;AACxD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,qBAAqB,SAAI,IAAI,SAAI;AACjC,6EAA6E,OAAO;AACpF;AACA;AACA;AACA;AACA;AACA;AACA;AACmD;AACE;AACH;AACJ;AAC9C;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,4BAA4B,8BAA8B;AAC1D,yCAAyC,2DAAc;AACvD;AACA;AACA;AACA;AACA,4BAA4B,yGAAyG;AACrI,wDAAwD,2DAAc;AACtE;AACA;AACA,4BAA4B,uDAAY;AACxC;AACA;AACA;AACA;AACA;AACA;AACA;AACA,6CAA6C,6DAAY;AACzD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,4BAA4B,4BAA4B;AACxD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,kEAAkE,gBAAgB;AAClF;AACA;AACA;AACA;AACA;AACA;AACA,mDAAmD,gBAAgB;AACnE;AACA;AACA;AACA;AACA;AACA;AACA,qDAAqD,gBAAgB;AACrE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAAS;AACT;AACA,2DAA2D,WAAW,iJAAiJ;AACvN;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,CAAC;AACyB;AAC1B,0C;;;;;;;;;;;;;;AC5JA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,6BAA6B;AAC7B;AACA;AACA;AACA;AACA;AACA;AACA,gBAAgB,SAAI,IAAI,SAAI;AAC5B;AACA,iDAAiD,OAAO;AACxD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,4BAA4B,yBAAyB;AACrD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,4BAA4B,yBAAyB;AACrD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,oCAAoC,wBAAwB;AAC5D;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAAS;AACT,gDAAgD;AAChD;AACA;AACA;AACA;AACA;AACA;AACA;AACA,CAAC;AACuB;AACxB,wC;;;;;;;;;;;;;;AC3GA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,6BAA6B;AAC7B;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO;AACP;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,CAAC,8CAA8C;AAC/C,6C;;;;;;;;;;;;;;AC9CA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,6BAA6B;AAC7B;AACA;AACA;AACA;AACA;AACA;AACA,qBAAqB,SAAI,IAAI,SAAI;AACjC,6EAA6E,OAAO;AACpF;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,CAAC;AAC0B;AAC3B,2C;;;;;;;;;;;;;;AClDA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,6BAA6B;AAC7B;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,CAAC;AACyB;AAC1B,0C;;;;;;;;;;;;;;;;ACvCA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,6BAA6B;AAC7B;AACA;AACA;AACA;AACA;AACA;AACoD;AACF;AAClD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,0CAA0C;AAC1C;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,wBAAwB,6EAA6E;AACrG;AACA,8BAA8B,6DAAe;AAC7C;AACA,wBAAwB,6EAA6E;AACrG;AACA,8BAA8B,2DAAc;AAC5C;AACA,wBAAwB,+EAA+E;AACvG;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,iDAAiD,gBAAgB;AACjE;AACA;AACA;AACA;AACA;AACA;AACA,iDAAiD,gBAAgB;AACjE;AACA;AACA;AACA;AACA;AACA;AACA,mDAAmD,gBAAgB;AACnE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,CAAC;AACuB;AACxB,wC;;;;;;;;;;;;;;;;;;ACjHA;AACgG;AACjB;AAC/E,8BAA8B,sEAA2B,CAAC,+EAAqC;AAC/F;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACALine truncated
|
||||
@@ -20,6 +20,8 @@ const BACKEND_PREFIXES: string[] = backendPrefixes
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
reactStrictMode: true,
|
||||
// Allow acceptance servers to run without sharing the developer server's lock/cache.
|
||||
distDir: process.env.QQQ_NEXT_DIST_DIR || '.next',
|
||||
// Reported to the backend as frontendVersion and shown on the developer page.
|
||||
env: { NEXT_PUBLIC_APP_VERSION: process.env.NEXT_PUBLIC_APP_VERSION || packageJson.version },
|
||||
...(exportBuild
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
{
|
||||
"name": "qqq-frontend-next",
|
||||
"version": "1.0.0-RC.21",
|
||||
"version": "1.0.0-RC.38",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@9.15.9",
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"build": "next build",
|
||||
"build:export": "QQQ_NEXT_OUTPUT=export NEXT_PUBLIC_MOCK_API=false next build",
|
||||
"scripting:worker": "node scripts/build-scripting-worker.mjs",
|
||||
"scripting:worker:check": "node scripts/build-scripting-worker.mjs && node scripts/check-scripting-worker.mjs",
|
||||
"dev": "node scripts/build-scripting-worker.mjs && node scripts/build-pdf-worker.mjs && node scripts/stage-dynamic-components.mjs && next dev --turbopack",
|
||||
"build": "node scripts/build-scripting-worker.mjs && node scripts/build-pdf-worker.mjs && node scripts/stage-dynamic-components.mjs && next build",
|
||||
"build:export": "node scripts/build-scripting-worker.mjs && node scripts/build-pdf-worker.mjs && node scripts/stage-dynamic-components.mjs && node scripts/build-export.mjs",
|
||||
"workflow:bundle": "node scripts/stage-dynamic-components.mjs",
|
||||
"build:jar": "pnpm build:export && mvn -B -q clean package",
|
||||
"start": "next start",
|
||||
"lint": "eslint src && node scripts/check-license-headers.mjs",
|
||||
@@ -22,20 +25,25 @@
|
||||
"test:acceptance": "node scripts/acceptance.mjs",
|
||||
"perf:budget": "node scripts/perf-budget.mjs",
|
||||
"ui-api:check": "node scripts/check-ui-api-spec.mjs",
|
||||
"tsc": "tsc",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"tsc": "node node_modules/typescript/bin/tsc",
|
||||
"typecheck": "node node_modules/typescript/bin/tsc --noEmit",
|
||||
"prepare": "git config core.hooksPath .githooks || true",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"build-storybook": "storybook build"
|
||||
"build-storybook": "storybook build",
|
||||
"pdf:worker": "node scripts/build-pdf-worker.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "6.18.6",
|
||||
"@codemirror/commands": "6.8.1",
|
||||
"@codemirror/lang-javascript": "6.2.5",
|
||||
"@codemirror/lang-sql": "6.9.0",
|
||||
"@codemirror/language": "6.11.2",
|
||||
"@codemirror/lint": "6.8.5",
|
||||
"@codemirror/state": "6.5.2",
|
||||
"@codemirror/view": "6.38.1",
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@jam.dev/recording-links": "0.4.0",
|
||||
"@jam.dev/sdk": "1.0.1",
|
||||
"@lezer/highlight": "1.2.1",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
@@ -52,11 +60,12 @@
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"dompurify": "^3.4.16",
|
||||
"dompurify": "^3.4.17",
|
||||
"lodash": "^4.18.1",
|
||||
"lucide-react": "^0.485.0",
|
||||
"next": "16.3.8",
|
||||
"rapidoc": "9.3.8",
|
||||
"next": "16.4.3",
|
||||
"pdfjs-dist": "6.4.299",
|
||||
"rapidoc": "10.1.0",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-hook-form": "^7.71.2",
|
||||
@@ -68,15 +77,16 @@
|
||||
"sonner": "^2.0.7",
|
||||
"sql-formatter": "15.6.6",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"zod": "^3.25.76"
|
||||
"typescript-editor": "npm:typescript@6.0.3",
|
||||
"zod": "^4.6.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@axe-core/playwright": "^4.11.1",
|
||||
"@eslint/eslintrc": "^3.3.4",
|
||||
"@playwright/test": "1.64.0-alpha-2026-10-01",
|
||||
"@storybook/addon-docs": "10.6.0",
|
||||
"@storybook/nextjs-vite": "10.6.0",
|
||||
"@storybook/react": "10.6.0",
|
||||
"@storybook/addon-docs": "10.6.2",
|
||||
"@storybook/nextjs-vite": "10.6.2",
|
||||
"@storybook/react": "10.6.2",
|
||||
"@tailwindcss/postcss": "^4.2.1",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
@@ -87,11 +97,11 @@
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^5.1.4",
|
||||
"@vitest/coverage-v8": "^4.1.11",
|
||||
"@vitest/ui": "^4.1.11",
|
||||
"@vitest/coverage-v8": "^4.1.12",
|
||||
"@vitest/ui": "^4.1.12",
|
||||
"autoprefixer": "^10.4.27",
|
||||
"eslint": "^9.39.3",
|
||||
"eslint-config-next": "16.3.6",
|
||||
"eslint-config-next": "16.4.1",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-jsdoc": "^62.7.1",
|
||||
"jest-axe": "^10.0.0",
|
||||
@@ -99,10 +109,11 @@
|
||||
"msw": "^2.12.10",
|
||||
"postcss": "^8.5.28",
|
||||
"prettier": "^3.8.1",
|
||||
"storybook": "10.6.0",
|
||||
"storybook": "10.6.2",
|
||||
"tailwindcss": "^4.2.1",
|
||||
"typescript": "^5.9.3",
|
||||
"vitest": "^4.1.11"
|
||||
"vite": "7.3.7",
|
||||
"vitest": "^4.1.12"
|
||||
},
|
||||
"msw": {
|
||||
"workerDirectory": [
|
||||
@@ -111,9 +122,22 @@
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"@codemirror/autocomplete": "6.18.6",
|
||||
"@codemirror/commands": "6.8.1",
|
||||
"@codemirror/language": "6.11.2",
|
||||
"@codemirror/lint": "6.8.5",
|
||||
"@codemirror/state": "6.5.2",
|
||||
"@codemirror/view": "6.38.1",
|
||||
"serialize-javascript": ">=7.0.3",
|
||||
"@humanfs/node": ">=0.16.8",
|
||||
"@babel/core": ">=7.29.6 <8"
|
||||
}
|
||||
"@babel/core": ">=7.29.6 <8",
|
||||
"source-map-js": ">=1.2.2",
|
||||
"follow-redirects": ">=1.16.2",
|
||||
"undici@>=7 <8": ">=7.30.1 <8",
|
||||
"fast-uri@>=3 <4": ">=3.1.9 <4"
|
||||
},
|
||||
"ignoredOptionalDependencies": [
|
||||
"electron"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -53,11 +53,6 @@
|
||||
"marker": "search-result-card",
|
||||
"gzipKB": 14.5
|
||||
},
|
||||
"quickSearchPanel": {
|
||||
"$comment": "The quick search results panel of the header box and the '/' dialog (groups, highlights, completions, failure and fallback notices), loaded when the panel first opens: import() in GlobalSearch and SearchDialog. Measured 9.4 KB gzip at introduction (#59); budget about 8% above.",
|
||||
"marker": "search-results-status",
|
||||
"gzipKB": 10.2
|
||||
},
|
||||
"commandRecordResults": {
|
||||
"$comment": "The command palette's record groups, loaded when the palette searches records: import() in CommandMenu. Measured 7.5 KB gzip at introduction (#59); budget about 8% above.",
|
||||
"marker": "command-menu-searching",
|
||||
|
||||
@@ -10,7 +10,7 @@ const port = Number(process.env.QQQ_E2E_PORT || 3107)
|
||||
const baseURL = `http://localhost:${port}`
|
||||
export default defineConfig({
|
||||
testDir: './tests/e2e',
|
||||
testMatch: ['collaboration-favorites.spec.ts', 'collaboration-record-panel.spec.ts'],
|
||||
testMatch: ['collaboration-favorites.spec.ts', 'collaboration-record-panel.spec.ts', 'collaboration-presence-share-tasks.spec.ts'],
|
||||
outputDir: './test-results/collaboration/artifacts',
|
||||
fullyParallel: false,
|
||||
workers: 1,
|
||||
@@ -34,7 +34,7 @@ export default defineConfig({
|
||||
webServer: {
|
||||
command: `pnpm dev --port ${port}`,
|
||||
url: baseURL,
|
||||
env: { NEXT_PUBLIC_MOCK_API: 'true' },
|
||||
env: { NEXT_PUBLIC_MOCK_API: 'true', QQQ_NEXT_DIST_DIR: '.next-collaboration' },
|
||||
reuseExistingServer: false,
|
||||
timeout: 120_000,
|
||||
},
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
</developers>
|
||||
|
||||
<properties>
|
||||
<revision>1.0.0-RC.21</revision>
|
||||
<revision>1.0.0-RC.38</revision>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<project.build.outputTimestamp>2026-09-24T00:00:00Z</project.build.outputTimestamp>
|
||||
</properties>
|
||||
@@ -89,8 +89,10 @@
|
||||
<files>
|
||||
<file>${project.basedir}/out/index.html</file>
|
||||
<file>${project.basedir}/out/app/_/_/index.html</file>
|
||||
<file>${project.basedir}/out/dynamic-qfmd-components/q-sequential-workflow.bundle.js</file>
|
||||
<file>${project.basedir}/out/dynamic-qfmd-components/q-sequential-workflow.bundle.js.map</file>
|
||||
</files>
|
||||
<message>Run "QQQ_NEXT_OUTPUT=export pnpm build" (or pnpm build:jar) before packaging.</message>
|
||||
<message>Run pnpm build:export (or pnpm build:jar) before packaging; the export must contain the QSequentialWorkflow editor bundle staged from dynamic-components/.</message>
|
||||
</requireFilesExist>
|
||||
</rules>
|
||||
</configuration>
|
||||
|
||||
@@ -9,7 +9,7 @@ import { execFileSync } from 'node:child_process'
|
||||
import { appendFileSync, readFileSync } from 'node:fs'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
|
||||
export const AREAS = ['esb', 'navigation', 'performance', 'processes', 'query', 'records', 'security', 'widgets']
|
||||
export const AREAS = ['esb', 'help', 'navigation', 'performance', 'processes', 'query', 'records', 'security', 'widgets']
|
||||
export const SMOKE = [
|
||||
'navigation/landing.spec.ts',
|
||||
'navigation/navigation.spec.ts',
|
||||
@@ -19,6 +19,7 @@ export const SMOKE = [
|
||||
].map((file) => `tests/acceptance/specs/${file}`)
|
||||
|
||||
const sourceAreas = [
|
||||
[/help|commandmenu/, 'help'],
|
||||
[/widget|dashboard|report|chart|pivot/, 'widgets'],
|
||||
[/query|grid|table|filter|search/, 'query'],
|
||||
[/record|form|field|input|validation|attachment|preview/, 'records'],
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* 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 https://www.apache.org/licenses/LICENSE-2.0
|
||||
*/
|
||||
|
||||
// Pass build settings through the child environment on Windows and POSIX alike.
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { createRequire } from 'node:module'
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
const result = spawnSync(process.execPath, [require.resolve('next/dist/bin/next'), 'build'], {
|
||||
stdio: 'inherit',
|
||||
env: { ...process.env, QQQ_NEXT_OUTPUT: 'export', NEXT_PUBLIC_MOCK_API: 'false' },
|
||||
})
|
||||
if (result.error) console.error(result.error.message)
|
||||
process.exit(result.status ?? 1)
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* 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 Copy the pinned trusted PDF worker; never copy uploaded files. */
|
||||
import { createRequire } from 'node:module'
|
||||
import { readFile, mkdir, copyFile } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
const require = createRequire(import.meta.url)
|
||||
const entry = require.resolve('pdfjs-dist/package.json')
|
||||
const root = path.dirname(entry)
|
||||
const pkg = JSON.parse(await readFile(entry, 'utf8'))
|
||||
if (pkg.version !== '6.4.299') throw new Error('Pinned PDF.js 6.4.299 is required')
|
||||
const output = new URL('../public/pdf-generated/', import.meta.url)
|
||||
await mkdir(output, { recursive: true })
|
||||
await copyFile(path.join(root, 'build/pdf.worker.min.mjs'), new URL('pdf-6.4.299.worker.js', output))
|
||||
await copyFile(path.join(root, 'LICENSE'), new URL('LICENSE', output))
|
||||
console.log('Prepared pinned PDF.js 6.4.299 worker and Apache-2.0 license')
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* 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 Build the offline editor worker from pinned trusted dependencies; never reads a script project. */
|
||||
|
||||
import { createRequire } from 'node:module'
|
||||
import { readFile, writeFile, mkdir } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
const ts = require('typescript-editor')
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
||||
if (ts.version !== '6.0.3') throw new Error('Pinned editor TypeScript 6.0.3 is required')
|
||||
const libraryDirectory = path.dirname(require.resolve('typescript-editor'))
|
||||
const libraries = Object.create(null)
|
||||
|
||||
/** Include only the standard ES2022 library closure, never DOM, Node or project packages. */
|
||||
async function includeLibrary(name) {
|
||||
if (!/^lib\.[a-z0-9.]+\.d\.ts$/.test(name)) throw new Error('Invalid shipped library name')
|
||||
if (libraries['/lib/' + name] !== undefined) return
|
||||
const source = await readFile(path.join(libraryDirectory, name), 'utf8')
|
||||
libraries['/lib/' + name] = source
|
||||
for (const reference of source.matchAll(/<reference lib="([a-z0-9.]+)"\s*\/>/g)) await includeLibrary('lib.' + reference[1] + '.d.ts')
|
||||
}
|
||||
await includeLibrary('lib.es2022.d.ts')
|
||||
const compiler = (await readFile(require.resolve('typescript-editor'), 'utf8')).replace(/\/\/# sourceMappingURL=.*$/gm, '')
|
||||
const source = await readFile(path.join(root, 'src/lib/scripting/typescript-session.ts'), 'utf8')
|
||||
const engine = ts.transpileModule(source, { compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS } }).outputText
|
||||
if (/require\s*\(/.test(engine)) throw new Error('Editor engine must have no runtime module imports')
|
||||
const worker = compiler + '\n;(() => {\nconst exports = {};\n' + engine + '\n' +
|
||||
'const session = new exports.TypeScriptSession(ts, ' + JSON.stringify(libraries) + ');\n' +
|
||||
`self.onmessage = (event) => {
|
||||
const id = event.data?.id;
|
||||
try {
|
||||
const result = session.run(event.data);
|
||||
if (new TextEncoder().encode(JSON.stringify(result)).length > 262144) throw new Error("Result limit");
|
||||
self.postMessage({ protocol: "qqq-typescript-ide/1", id, version: "6.0.3", result });
|
||||
} catch {
|
||||
self.postMessage({ protocol: "qqq-typescript-ide/1", id, version: "6.0.3", error: "Editor analysis could not be completed" });
|
||||
}
|
||||
};\n})();\n`
|
||||
const destination = path.join(root, 'public/scripting-generated')
|
||||
await mkdir(destination, { recursive: true })
|
||||
await writeFile(path.join(destination, 'typescript-6.0.3.worker.js'), worker)
|
||||
await writeFile(path.join(destination, 'LICENSE.txt'), await readFile(path.join(libraryDirectory, '../LICENSE.txt'), 'utf8'))
|
||||
await writeFile(path.join(destination, 'ThirdPartyNoticeText.txt'), await readFile(path.join(libraryDirectory, '../ThirdPartyNoticeText.txt'), 'utf8'))
|
||||
console.log('Generated offline TypeScript 6.0.3 worker (' + Buffer.byteLength(worker) + ' bytes, ' + Object.keys(libraries).length + ' standard libraries)')
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* 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 Real Chromium smoke test for the generated offline worker, served only on loopback. */
|
||||
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import http from 'node:http'
|
||||
import { chromium } from '@playwright/test'
|
||||
|
||||
const workerBytes = await readFile(new URL('../public/scripting-generated/typescript-6.0.3.worker.js', import.meta.url))
|
||||
const server = http.createServer((request, response) => {
|
||||
if (request.url === '/worker.js') {
|
||||
response.writeHead(200, { 'Content-Type': 'text/javascript', 'Cache-Control': 'no-store' }); response.end(workerBytes)
|
||||
} else if (request.url === '/') {
|
||||
response.writeHead(200, { 'Content-Type': 'text/html', 'Content-Security-Policy': "default-src 'none'; script-src 'self'; worker-src 'self'; connect-src 'none'" })
|
||||
response.end('<!doctype html><title>Offline TypeScript fixture</title>')
|
||||
} else { response.writeHead(404); response.end() }
|
||||
})
|
||||
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve))
|
||||
let browser
|
||||
try {
|
||||
browser = await chromium.launch({ headless: true })
|
||||
const page = await browser.newPage()
|
||||
const requested = []
|
||||
page.on('request', (request) => requested.push(new URL(request.url()).pathname))
|
||||
await page.goto('http://127.0.0.1:' + server.address().port)
|
||||
const result = await page.evaluate(async () => {
|
||||
const worker = new Worker('/worker.js')
|
||||
let id = 0
|
||||
const request = (operation, files, position = 0) => new Promise((resolve, reject) => {
|
||||
const requestId = ++id
|
||||
const timer = setTimeout(() => { worker.terminate(); reject(new Error('Worker deadline exceeded')) }, 10_000)
|
||||
worker.onmessage = (event) => { clearTimeout(timer); if (event.data.id !== requestId || event.data.error) reject(new Error('Worker protocol failed')); else resolve(event.data.result) }
|
||||
worker.onerror = () => { clearTimeout(timer); reject(new Error('Worker failed')) }
|
||||
worker.postMessage({ id: requestId, operation, files, file: 'src/main.ts', position, declarations: '' })
|
||||
})
|
||||
try {
|
||||
const diagnostics = await request('diagnostics', [{ path: 'src/main.ts', content: 'export const count: number = "wrong";' }])
|
||||
const source = 'import { order } from "./order.js";\norder.status;\norder.'
|
||||
const files = [{ path: 'src/main.ts', content: source }, { path: 'src/order.ts', content: 'export const order = { status: "READY" };' }]
|
||||
const completion = await request('completion', files, source.length)
|
||||
const hover = await request('hover', files, source.indexOf('status') + 1)
|
||||
const definition = await request('definition', files, source.indexOf('order.status') + 1)
|
||||
const formatting = await request('format', [{ path: 'src/main.ts', content: 'export const value={a:1,b:2};' }])
|
||||
await request('diagnostics', [{ path: 'src/main.ts', content: 'fetch("https://example.invalid/leak"); globalThis.__qqqNeverExecute = true;' }])
|
||||
return { diagnostics, completion, hover, definition, formatting, executed: globalThis.__qqqNeverExecute === true }
|
||||
} finally { worker.terminate() }
|
||||
})
|
||||
assert(result.diagnostics.diagnostics.some((item) => item.code === 2322))
|
||||
assert(result.completion.completions.some((item) => item.label === 'status'))
|
||||
assert.match(result.hover.hover.text, /string/)
|
||||
assert.equal(result.definition.definitions[0].file, 'src/order.ts')
|
||||
assert(result.formatting.edits.length > 0)
|
||||
assert.equal(result.executed, false)
|
||||
assert.deepEqual(requested.sort(), ['/', '/worker.js'])
|
||||
console.log('PASS real Chromium: strict diagnostics, multi-file completion/hover/definition, formatting, no source execution or external requests')
|
||||
} finally {
|
||||
await browser?.close()
|
||||
await new Promise((resolve) => server.close(resolve))
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import path from 'node:path'
|
||||
|
||||
const HTTP_METHODS = ['get', 'post', 'put', 'patch', 'delete']
|
||||
// operations the app works without (it checks for them), so an older server may lack them
|
||||
const OPTIONAL_OPERATIONS = new Set(['GET /qqq/v1/version'])
|
||||
const OPTIONAL_OPERATIONS = new Set(['GET /qqq/v1/version', 'GET /qqq/v1/help/search', 'GET /qqq/v1/help/coverage'])
|
||||
const bundled = JSON.parse(readFileSync(path.resolve('src/lib/api/openapi/qqq-v1.json'), 'utf-8'))
|
||||
|
||||
/** @param {string} version @returns {number[] | null} */
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"component": {
|
||||
"repository": "QRun-IO/qqq-frontend-component-sequential-workflow",
|
||||
"sourceCommit": "e6036d2a474a90d80c72bc4c5f242e666b3415f6",
|
||||
"sourceTree": "9efc09210a81dc4121aa53e156443b7f4fcb6dd7",
|
||||
"mainCommit": "ede4eba875ca17c5921ad4caf61a08bdff6f794d",
|
||||
"build": {
|
||||
"node": "v22.20.0",
|
||||
"npm": "10.9.3",
|
||||
"image": "node@sha256:b21fe589dfbe5cc39365d0544b9be3f1f33f55f3c86c87a76ff65a02f8f5848e",
|
||||
"commands": [
|
||||
"npm ci --ignore-scripts",
|
||||
"npm test (existing 3 suites, 62 tests)",
|
||||
"npx webpack"
|
||||
],
|
||||
"packageLockSha256": "737cc1a3e416d942c050076dfae36345e9ba3f62d679dec71f04c2a2382b1a21",
|
||||
"qualifiedUtc": "2026-11-05T12:31Z"
|
||||
},
|
||||
"mainTree": "9efc09210a81dc4121aa53e156443b7f4fcb6dd7",
|
||||
"mainMerge": "normal merge of PR 5 (parents a32a7b4d437f09c09b03eb8828fccfd7de4eae7a, e6036d2a474a90d80c72bc4c5f242e666b3415f6); its tree equals the qualified source tree, so the qualified assets are the canonical main build output"
|
||||
},
|
||||
"assetDirectory": "dynamic-components/q-sequential-workflow",
|
||||
"files": {
|
||||
"q-sequential-workflow.bundle.js": "9f8358b105d0c157cb32a476313a7750281549f8e9423dd7d125ceaaafe8f32d",
|
||||
"q-sequential-workflow.bundle.js.map": "776c716690a357213e00d109652ab7eb9c9025073b362c995564e11ca5c5ee92"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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 https://www.apache.org/licenses/LICENSE-2.0
|
||||
*/
|
||||
|
||||
// Vendor the qbit YAML as lossless JSON, like the other extension documents, then run
|
||||
// node scripts/generate-edi-types.mjs. No runtime schema or generator dependency ships.
|
||||
import { readFileSync, writeFileSync } from 'node:fs'
|
||||
|
||||
const document = JSON.parse(readFileSync(new URL('../src/lib/api/openapi/extensions/edi.json', import.meta.url), 'utf8'))
|
||||
const header = readFileSync(new URL('../src/lib/api/tables.ts', import.meta.url), 'utf8').split('/**')[0]
|
||||
function typeOf(schema) {
|
||||
let type = schema.$ref ? schema.$ref.split('/').at(-1)
|
||||
: schema.enum ? schema.enum.map(JSON.stringify).join(' | ')
|
||||
: schema.type === 'array' ? `Array<${typeOf(schema.items)}>`
|
||||
: schema.type === 'object' ? schema.properties
|
||||
? `{\n${Object.entries(schema.properties).map(([name, value]) => ` ${name}${schema.required?.includes(name) ? '' : '?'}: ${typeOf(value)}`).join('\n')}\n}`
|
||||
: `Record<string, ${typeof schema.additionalProperties === 'object' ? typeOf(schema.additionalProperties) : 'unknown'}>`
|
||||
: ['integer', 'number'].includes(schema.type) ? 'number'
|
||||
: ['string', 'boolean'].includes(schema.type) ? schema.type : 'unknown'
|
||||
return type + (schema.nullable && type !== 'unknown' ? ' | null' : '')
|
||||
}
|
||||
writeFileSync(new URL('../src/types/edi.ts', import.meta.url), header + '/** @file Generated from the vendored edi.json. Run scripts/generate-edi-types.mjs; do not edit. */\n\n' + Object.entries(document.components.schemas).map(([name, schema]) => `/** ${schema.description ?? name} */\nexport type ${name} = ${typeOf(schema)}\n`).join('\n'))
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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 https://www.apache.org/licenses/LICENSE-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file Stage the versioned, precompiled QSequentialWorkflow editor bundle (the qbit-workflows `QSequentialWorkflow` editor)
|
||||
* under `public/dynamic-qfmd-components/`, which is where qbit-workflows' widget metadata points
|
||||
* (`/dynamic-qfmd-components/q-sequential-workflow.bundle.js`) and where the single-origin host serves the UI from.
|
||||
* The bundle and its map are tracked library assets in `dynamic-components/q-sequential-workflow/`, compiled from the
|
||||
* canonical component repository (source commit and toolchain recorded in `dynamic-components.lock.json`). Nothing is
|
||||
* downloaded or built here, and a file is refused unless its sha256 matches the lock. A missing or altered bundle fails the
|
||||
* build, so a release can never silently omit the workflow editor.
|
||||
*/
|
||||
import { createHash } from 'node:crypto'
|
||||
import { copyFile, mkdir, readFile } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const here = path.dirname(fileURLToPath(import.meta.url))
|
||||
const sha256 = (bytes) => createHash('sha256').update(bytes).digest('hex')
|
||||
|
||||
/** Copies every locked file from `assetDir` to `outDir`; throws on a missing file or a hash mismatch. */
|
||||
export async function stageDynamicComponents({ assetDir, lock, outDir }) {
|
||||
await mkdir(outDir, { recursive: true })
|
||||
const staged = []
|
||||
for (const [name, expected] of Object.entries(lock.files)) {
|
||||
if (path.basename(name) !== name) throw new Error(`Locked file name must be a plain file name: ${name}`)
|
||||
if (!/^[0-9a-f]{64}$/.test(expected ?? '')) throw new Error(`${name} has no sha256 in the dynamic component lock`)
|
||||
const source = path.join(assetDir, name)
|
||||
const actual = sha256(await readFile(source).catch(() => { throw new Error(`Missing ${name} in ${assetDir}`) }))
|
||||
if (actual !== expected) throw new Error(`${name} sha256 ${actual} does not match the lock ${expected}`)
|
||||
await copyFile(source, path.join(outDir, name))
|
||||
staged.push(name)
|
||||
}
|
||||
return staged
|
||||
}
|
||||
|
||||
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||
const lock = JSON.parse(await readFile(path.join(here, 'dynamic-components.lock.json'), 'utf8'))
|
||||
const staged = await stageDynamicComponents({ assetDir: path.join(here, '..', lock.assetDirectory), lock, outDir: path.join(here, '../public/dynamic-qfmd-components') })
|
||||
console.log(`Staged dynamic components compiled from ${lock.component.repository}@${lock.component.sourceCommit.slice(0, 8)}: ${staged.join(', ')}`)
|
||||
}
|
||||
@@ -22,6 +22,7 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
|
||||
import { useRouteParams } from '@/lib/hooks/use-route-params'
|
||||
@@ -32,6 +33,10 @@ import { launchTableName } from '@/lib/utils/process-utils'
|
||||
import { NotFoundState } from '@/components/layout/NotFoundState'
|
||||
import { RouteRedirect } from '@/components/layout/RouteRedirect'
|
||||
|
||||
const EdiMapperPage = dynamic(() => import('@/components/edi-mapper/EdiMapperPage'), {
|
||||
loading: () => <p role="status">Loading mapper…</p>,
|
||||
})
|
||||
|
||||
/**
|
||||
* Opens the named process for the record, returning to the record afterwards, or shows the
|
||||
* in-shell not-found state when the segment is not one of the table's processes.
|
||||
@@ -42,6 +47,8 @@ export default function RecordProcessPage() {
|
||||
const { slug, recordId, action } = useRouteParams<{ slug: string; recordId: string; action: string }>()
|
||||
const { data: metaData, isError } = useQuery({ queryKey: queryKeys.metadataAll(), queryFn: loadMetaData, staleTime: 1000 * 60 * 30 })
|
||||
|
||||
if (slug === 'edi_map' && action === 'mapper') return <EdiMapperPage key={recordId} mapId={Number(recordId)} />
|
||||
|
||||
if (isError) {
|
||||
return <div role="alert" className="py-12 text-center text-destructive">Failed to load application metadata.</div>
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
import { render, screen, waitFor, within } from '@testing-library/react'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { http, HttpResponse } from 'msw'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type { QInstance } from '@/types'
|
||||
import { QContextProvider, useQContext } from '@/lib/context/q-context'
|
||||
@@ -55,7 +55,7 @@ vi.mock('@/lib/api/developer', () => ({
|
||||
getTableApis: vi.fn(async () => []),
|
||||
getApiVersions: vi.fn(),
|
||||
getApiTableSpec: vi.fn(),
|
||||
apiTableSpecUrl: () => '/api/v1/person/openapi.json',
|
||||
apiTableSpecUrl: () => '/qqq/v1/table/person/apis/siteApi/v1/openapi',
|
||||
apiMiddlewareBaseURL: () => '',
|
||||
}))
|
||||
|
||||
@@ -113,6 +113,9 @@ function renderPage() {
|
||||
}
|
||||
|
||||
describe('RecordDeveloperViewPage', () => {
|
||||
// The page lazy-loads this view; transforming its module graph cold must not count against the first test's waits
|
||||
beforeAll(async () => { await import('@/components/developer/RecordDeveloperView') }, 120_000)
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(getRecord).mockResolvedValue(record)
|
||||
vi.mocked(getRecordDeveloperData).mockResolvedValue({
|
||||
@@ -140,7 +143,7 @@ describe('RecordDeveloperViewPage', () => {
|
||||
expect(within(card).getAllByRole('tab').map((tab) => tab.textContent)).toEqual(['Code', 'Logs', 'Test', 'Docs'])
|
||||
expect(screen.getByTestId('page-header')).toHaveTextContent('Ada Lovelace Developer Mode')
|
||||
expect(getRecordDeveloperData).toHaveBeenCalledWith('person', '1')
|
||||
})
|
||||
}, 15_000) // three waits of up to 5 s each: the 5 s default could expire before they do
|
||||
|
||||
it('shows the raw values of the developer-mode record, not a separate record read (Material getRecordDeveloperMode)', async () => {
|
||||
serveInstance([])
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { http, HttpResponse } from 'msw'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { deserializeFilter } from '@/lib/utils/filter-utils'
|
||||
import apiClient from '@/lib/api/client'
|
||||
@@ -61,6 +61,12 @@ function renderPage() {
|
||||
}
|
||||
|
||||
describe('RecordViewPage independent association loading', () => {
|
||||
beforeAll(async () => {
|
||||
// Keep cold compilation of the lazy RecordView outside the first assertion's deadline,
|
||||
// so any case passes alone (-t) and under load.
|
||||
await import('@/components/records/RecordView')
|
||||
}, 30_000)
|
||||
|
||||
beforeEach(() => { localStorage.clear(); vi.restoreAllMocks(); navigation.search = 'tab=related' })
|
||||
|
||||
it('keeps the base and child Add, and requests no expansion, when the child table is listed without read permission (QRun-IO/qqq#671)', async () => {
|
||||
|
||||
@@ -59,7 +59,7 @@ const RecordView = lazy(() => import('@/components/records/RecordView').then((mo
|
||||
*
|
||||
* Loads the base record and optional whole-association expansion independently. Once
|
||||
* the record loads it is registered in the recently-viewed history (via
|
||||
* `addRecentRecord`) so it surfaces in the GlobalSearch and SearchDialog.
|
||||
* `addRecentRecord`) so it surfaces in the search box (recently viewed records).
|
||||
* The page header in QContext is updated to the record label.
|
||||
*
|
||||
* @returns A composed view that assembles:
|
||||
@@ -202,6 +202,7 @@ export default function RecordViewPage() {
|
||||
)}
|
||||
<AlertsRecordPanelSlot metaData={metaData} tableName={slug} recordId={recordId} />
|
||||
<Suspense fallback={<div role="status" aria-label="Loading record view" className="py-12 text-center">Loading record…</div>}><RecordView
|
||||
requestedPrimaryKey={recordId}
|
||||
instance={metaData}
|
||||
tableMetaData={tableMetaData}
|
||||
record={displayRecord}
|
||||
@@ -217,7 +218,7 @@ export default function RecordViewPage() {
|
||||
allTables={metaData?.tables}
|
||||
widgetMetaDataMap={metaData?.widgets}
|
||||
associationTables={associationTables}
|
||||
auditSource={auditSource(metaData)}
|
||||
auditSource={auditSource(metaData, tableMetaData.clientCapabilities)}
|
||||
/></Suspense>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -88,7 +88,7 @@ describe('TableDeveloperViewPage API Docs & Playground', () => {
|
||||
window.localStorage.clear()
|
||||
getTableApisMock.mockReset()
|
||||
getApiVersionsMock.mockReset()
|
||||
getApiVersionsMock.mockImplementation(async (path: string) => versions[path])
|
||||
getApiVersionsMock.mockImplementation((api) => versions[api.path])
|
||||
})
|
||||
|
||||
it('selects the first API and its current version and embeds RapiDoc for the table spec', async () => {
|
||||
@@ -108,7 +108,7 @@ describe('TableDeveloperViewPage API Docs & Playground', () => {
|
||||
|
||||
await waitFor(() => expect(container.querySelector('rapi-doc')).not.toBeNull())
|
||||
const rapiDoc = container.querySelector('[data-qqq-id="table-dev-api-reference"] rapi-doc')!
|
||||
expect(rapiDoc).toHaveAttribute('spec-url', '/person-api/2026.Q2/person/openapi.json')
|
||||
expect(rapiDoc).toHaveAttribute('spec-url', '/qqq/v1/table/person/apis/person-api/2026.Q2/openapi?format=json')
|
||||
expect(rapiDoc).toHaveAttribute('primary-color', '#6366f1')
|
||||
expect(rapiDoc).toHaveAttribute('show-header', 'false')
|
||||
expect(rapiDoc).toHaveAttribute('render-style', 'view')
|
||||
@@ -128,7 +128,7 @@ describe('TableDeveloperViewPage API Docs & Playground', () => {
|
||||
|
||||
expect(await screen.findByLabelText('API')).toHaveValue('admin-api')
|
||||
expect(await screen.findByLabelText('Version')).toHaveValue('v2')
|
||||
await waitFor(() => expect(container.querySelector('rapi-doc')).toHaveAttribute('spec-url', '/admin-api/v2/person/openapi.json'))
|
||||
await waitFor(() => expect(container.querySelector('rapi-doc')).toHaveAttribute('spec-url', '/qqq/v1/table/person/apis/admin-api/v2/openapi?format=json'))
|
||||
})
|
||||
|
||||
it('falls back to the first API and the current version when the remembered ones are gone', async () => {
|
||||
@@ -151,7 +151,7 @@ describe('TableDeveloperViewPage API Docs & Playground', () => {
|
||||
expect(await screen.findByLabelText('Version')).toHaveValue('v1')
|
||||
await user.selectOptions(screen.getByLabelText('Version'), 'v2')
|
||||
|
||||
await waitFor(() => expect(container.querySelector('rapi-doc')).toHaveAttribute('spec-url', '/admin-api/v2/person/openapi.json'))
|
||||
await waitFor(() => expect(container.querySelector('rapi-doc')).toHaveAttribute('spec-url', '/qqq/v1/table/person/apis/admin-api/v2/openapi?format=json'))
|
||||
expect(window.localStorage.getItem('qqq.tableDeveloperView.lastApiName')).toBe('admin-api')
|
||||
expect(window.localStorage.getItem('qqq.tableDeveloperView.lastApiVersion')).toBe('v2')
|
||||
})
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
*/
|
||||
|
||||
import type { ReactNode } from 'react'
|
||||
import { NestedAppRedirect } from '@/components/layout/NestedAppRedirect'
|
||||
import { EXPORT_PLACEHOLDER } from '@/lib/utils/export-placeholder'
|
||||
|
||||
/**
|
||||
@@ -23,12 +24,12 @@ export function generateStaticParams() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Pass-through layout that exists to carry generateStaticParams.
|
||||
* Resolves app-prefixed URLs before rendering the resource route.
|
||||
*
|
||||
* @param props - Layout props.
|
||||
* @param props.children - Route content.
|
||||
* @returns The children unchanged.
|
||||
* @returns The route content or its canonical redirect.
|
||||
*/
|
||||
export default function Layout({ children }: { children: ReactNode }) {
|
||||
return children
|
||||
return <NestedAppRedirect>{children}</NestedAppRedirect>
|
||||
}
|
||||
@@ -17,7 +17,7 @@
|
||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { http, HttpResponse } from 'msw'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import apiClient from '@/lib/api/client'
|
||||
import { QContextProvider } from '@/lib/context/q-context'
|
||||
@@ -54,6 +54,8 @@ function renderPage(page: React.ReactNode) {
|
||||
}
|
||||
|
||||
describe('Create and base edit use full metadata and actual write contracts', () => {
|
||||
// The pages lazy-load the form; transforming its module graph cold must not count against the first test's waits
|
||||
beforeAll(async () => { await import('@/components/forms/EntityForm') }, 120_000)
|
||||
beforeEach(() => { vi.restoreAllMocks(); vi.clearAllMocks(); localStorage.clear(); fixture() })
|
||||
|
||||
it('edits the base without requesting denied children or replacing associations', async () => {
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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 Shared access-request deep link; never fetches the referenced record. */
|
||||
'use client'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { Suspense } from 'react'
|
||||
const Request = dynamic(
|
||||
() => import('@/components/collaboration/RequestAccess').then((m) => m.RequestAccessPage),
|
||||
{ ssr: false }
|
||||
)
|
||||
/** Renders the opaque request form.
|
||||
* @returns Access page.
|
||||
*/
|
||||
export default function AccessRequestRoute() {
|
||||
return (
|
||||
<Suspense>
|
||||
<Request />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
@@ -43,6 +43,7 @@ import { loadMetaData } from '@/lib/api/metadata'
|
||||
import apiClient from '@/lib/api/client'
|
||||
import { queryKeys } from '@/lib/query-client'
|
||||
import { cn } from '@/lib/utils/cn'
|
||||
import { CreateIsolatedScript } from '@/components/scripts/CreateIsolatedScript'
|
||||
|
||||
/** Duration in milliseconds before the "copied!" feedback resets to the copy icon. */
|
||||
const COPY_RESET_MS = 2000
|
||||
@@ -252,6 +253,8 @@ export default function DeveloperPage() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<CreateIsolatedScript />
|
||||
|
||||
{/* Session Info */}
|
||||
<SectionCard
|
||||
title="Session Info"
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* 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 Static export placeholder for job detail routes. */
|
||||
import type { ReactNode } from 'react'
|
||||
import { EXPORT_PLACEHOLDER } from '@/lib/utils/export-placeholder'
|
||||
|
||||
/** Generates the export placeholder.
|
||||
* @returns One job path.
|
||||
*/
|
||||
export function generateStaticParams() { return [{ jobId: EXPORT_PLACEHOLDER }] }
|
||||
|
||||
/** Pass-through layout.
|
||||
* @param props - Children.
|
||||
* @returns Children unchanged.
|
||||
*/
|
||||
export default function Layout({ children }: { children: ReactNode }) { return children }
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* 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 Live job route, separate from metadata table records. */
|
||||
'use client'
|
||||
import { useRouteParams } from '@/lib/hooks/use-route-params'
|
||||
import { JobDetailView } from '@/components/jobs/JobDetailView'
|
||||
|
||||
/** Renders a job by id.
|
||||
* @returns Job view or invalid-id message.
|
||||
*/
|
||||
export default function JobRoute() {
|
||||
const { jobId } = useRouteParams<{ jobId: string }>()
|
||||
const id = Number(jobId)
|
||||
if (!Number.isSafeInteger(id) || id <= 0) return <p role="alert">Invalid job identifier.</p>
|
||||
return <div className="w-full min-w-0 rounded-xl border border-border bg-card"><JobDetailView key={id} jobId={id} /></div>
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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 Static Jobs page, taking precedence over the metadata slug route. */
|
||||
'use client'
|
||||
import dynamic from 'next/dynamic'
|
||||
const JobsPage = dynamic(() => import('@/components/jobs/JobsPage').then((module) => module.JobsPage))
|
||||
|
||||
/** Renders the Jobs page.
|
||||
* @returns Page content.
|
||||
*/
|
||||
export default function JobsRoute() { return <JobsPage /> }
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* 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 Static My work route, loaded like the notifications page. */
|
||||
'use client'
|
||||
import dynamic from 'next/dynamic'
|
||||
const MyWork = dynamic(() => import('@/components/collaboration/MyWork').then((m) => m.MyWork), {
|
||||
ssr: false,
|
||||
})
|
||||
/** Renders personal work.
|
||||
* @returns Lazy page.
|
||||
*/
|
||||
export default function MyWorkRoute() {
|
||||
return <MyWork />
|
||||
}
|
||||
@@ -240,6 +240,7 @@ export default function DashboardPage() {
|
||||
<Link
|
||||
key={target.key}
|
||||
href={`${target.path}/create`}
|
||||
prefetch={false}
|
||||
className="flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium text-foreground transition-colors hover:bg-accent"
|
||||
data-qqq-id={`dashboard-create-${target.key}`}
|
||||
>
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* 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 What's new route, split from the dashboard's first load. */
|
||||
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { Loader2 } from 'lucide-react'
|
||||
|
||||
/** The page loads on demand, so its code stays out of every route's first load. */
|
||||
const WhatsNewPage = dynamic(() => import('@/components/release-notes/WhatsNewPage').then((module) => module.WhatsNewPage), {
|
||||
ssr: false,
|
||||
loading: () => (
|
||||
<p className="flex items-center gap-2 p-6 text-sm text-muted-foreground" role="status" data-qqq-id="whats-new-page-loading">
|
||||
<Loader2 className="h-4 w-4 animate-spin" aria-hidden="true" />
|
||||
Loading release notes...
|
||||
</p>
|
||||
),
|
||||
})
|
||||
|
||||
/**
|
||||
* Renders the whats-new page at `/app/whats-new`.
|
||||
*
|
||||
* @returns The page.
|
||||
*/
|
||||
export default function WhatsNewRoute() {
|
||||
return <WhatsNewPage />
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* 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 React from 'react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
|
||||
import { resetClientErrorsForTesting, setClientErrorSink } from '@/lib/errors/client-errors'
|
||||
import PageError from './error'
|
||||
|
||||
describe('page error boundary', () => {
|
||||
afterEach(() => {
|
||||
resetClientErrorsForTesting()
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('reports the failure and offers Try again inside the shell', async () => {
|
||||
vi.spyOn(console, 'error').mockImplementation(() => undefined)
|
||||
const sink = vi.fn().mockResolvedValue(undefined)
|
||||
setClientErrorSink(sink)
|
||||
const reset = vi.fn()
|
||||
render(<PageError error={new Error("Cannot read properties of undefined (reading 'length')")} reset={reset} />)
|
||||
expect(screen.getByText('This page hit a problem')).toBeVisible()
|
||||
expect(sink).toHaveBeenCalledWith(expect.objectContaining({ scope: 'page' }))
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Try again' }))
|
||||
expect(reset).toHaveBeenCalledOnce()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* 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 Page error boundary — a page that throws is replaced inside the app shell, so the
|
||||
* navigation, header and assistant keep working.
|
||||
*/
|
||||
|
||||
'use client'
|
||||
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { AlertCircle, RefreshCw } from 'lucide-react'
|
||||
|
||||
import { clientErrorDetails, reportClientError, type ClientErrorReport } from '@/lib/errors/client-errors'
|
||||
|
||||
/**
|
||||
* Whether the error means the session ended (the root boundary sends the user to log in).
|
||||
*
|
||||
* @param error - The thrown error.
|
||||
* @returns True for 401 / unauthorized / session expired.
|
||||
*/
|
||||
function isSessionError(error: Error): boolean {
|
||||
const message = error.message?.toLowerCase() ?? ''
|
||||
return message.includes('401') || message.includes('unauthorized') || message.includes('session expired')
|
||||
}
|
||||
|
||||
/**
|
||||
* The page-level fallback, rendered by Next.js inside the dashboard layout.
|
||||
*
|
||||
* @param props - The Next.js error boundary props.
|
||||
* @param props.error - The thrown error.
|
||||
* @param props.reset - Renders the page again.
|
||||
* @returns A card with Try again, Go home and the error's details.
|
||||
*/
|
||||
export default function PageError({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) {
|
||||
const [report, setReport] = useState<ClientErrorReport | null>(null)
|
||||
const sessionExpired = isSessionError(error)
|
||||
useEffect(() => {
|
||||
if (!sessionExpired) setReport(reportClientError(error, { scope: 'page' }))
|
||||
}, [error, sessionExpired])
|
||||
// the root boundary owns the "Session Expired" screen
|
||||
if (sessionExpired) throw error
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-lg rounded-xl border border-border bg-card p-6 text-center shadow-sm" role="alert" data-qqq-id="page-error">
|
||||
<AlertCircle className="mx-auto mb-3 h-10 w-10 text-destructive" aria-hidden="true" />
|
||||
<h1 className="text-lg font-semibold text-foreground">This page hit a problem</h1>
|
||||
<p className="mt-1 text-sm text-muted-foreground">The rest of the app still works. Try again, or go to another page.</p>
|
||||
<div className="mt-4 flex flex-wrap justify-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={reset}
|
||||
className="inline-flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring pointer-coarse:min-h-11"
|
||||
data-qqq-id="button-page-try-again"
|
||||
>
|
||||
<RefreshCw className="h-4 w-4" aria-hidden="true" />
|
||||
Try again
|
||||
</button>
|
||||
<Link
|
||||
href="/"
|
||||
className="inline-flex items-center gap-2 rounded-md border border-border px-4 py-2 text-sm font-medium text-foreground hover:bg-accent focus:outline-none focus-visible:ring-2 focus-visible:ring-ring pointer-coarse:min-h-11"
|
||||
data-qqq-id="link-page-go-home"
|
||||
>
|
||||
Go home
|
||||
</Link>
|
||||
</div>
|
||||
{report && (
|
||||
<details className="mt-4 text-left text-xs text-muted-foreground">
|
||||
<summary className="cursor-pointer select-none">Details</summary>
|
||||
<pre className="mt-1 max-h-48 overflow-auto whitespace-pre-wrap break-words rounded bg-muted p-2 font-mono" data-qqq-id="page-error-details">{clientErrorDetails(report)}</pre>
|
||||
</details>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -22,8 +22,10 @@
|
||||
|
||||
/** Dashboard layout — authenticated route group shell providing sidebar, header, banners, command palette, and global keyboard shortcuts. */
|
||||
|
||||
import React, { useEffect, useState, useCallback, useMemo, useRef } from 'react'
|
||||
import { SecuredDataProvider, SecuredDataBannerSlot } from '@/components/secured-data/SecuredDataProvider'
|
||||
import React, { useEffect, useLayoutEffect, useState, useCallback, useMemo, useRef } from 'react'
|
||||
import { usePathname, useRouter } from 'next/navigation'
|
||||
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
|
||||
import { useAuth } from '@/lib/auth/use-auth'
|
||||
@@ -31,15 +33,17 @@ import { useQContext, QContextProvider } from '@/lib/context/q-context'
|
||||
import { useAppTreeRoutes } from '@/lib/hooks/use-routes'
|
||||
import { useDocumentTitle } from '@/lib/hooks/use-document-title'
|
||||
import { loadMetaData } from '@/lib/api/metadata'
|
||||
import { ApplicationLoading } from '@/components/layout/ApplicationLoading'
|
||||
import { applyBrandingTheme } from '@/lib/theme/apply-branding'
|
||||
import { skipToFirstZone } from '@/lib/utils/skip-link'
|
||||
import { readMaterialTheme } from '@/lib/theme/material-theme'
|
||||
import { useTheme } from '@/lib/theme/theme-provider'
|
||||
import { recordAnalytics } from '@/lib/analytics'
|
||||
import { useAnalytics } from '@/lib/analytics/use-analytics'
|
||||
import { queryKeys } from '@/lib/query-client'
|
||||
import { searchableTables } from '@/lib/utils/record-search'
|
||||
import { scrollPageToTop } from '@/lib/utils/scroll-to-top'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { Sparkles } from 'lucide-react'
|
||||
|
||||
import Sidebar from '@/components/layout/Sidebar'
|
||||
import Header from '@/components/layout/Header'
|
||||
import BannerComponent from '@/components/layout/Banner'
|
||||
@@ -47,18 +51,19 @@ import BrandedHeaderBar from '@/components/layout/BrandedHeaderBar'
|
||||
import CompanyFooter from '@/components/layout/CompanyFooter'
|
||||
import { buildBreadcrumbs, buildDocumentTitle } from '@/components/layout/Breadcrumbs'
|
||||
import { CommandMenu } from '@/components/feedback/CommandMenu'
|
||||
import { SearchDialog } from '@/components/feedback/SearchDialog'
|
||||
import { KeyboardShortcutsDialog } from '@/components/feedback/KeyboardShortcutsDialog'
|
||||
import { AccountMenu } from '@/components/layout/AccountMenu'
|
||||
import { HelpMenu, HelpMenuItems } from '@/components/layout/HelpMenu'
|
||||
import { HEADER_MENU_ITEM_CLASS } from '@/components/layout/HeaderMenu'
|
||||
import { singleKeyShortcutsEnabled } from '@/lib/keyboard/single-key-shortcuts'
|
||||
import { useSingleKeyShortcutsSync } from '@/lib/keyboard/use-single-key-shortcuts-sync'
|
||||
import { HelpPanelProvider } from '@/components/help/HelpPanelProvider'
|
||||
import { AssistantProvider, useAssistantController } from '@/components/assistant/AssistantProvider'
|
||||
import { AssistantHeaderButton } from '@/components/assistant/AssistantHeaderButton'
|
||||
import { AssistantMount } from '@/components/assistant/AssistantMount'
|
||||
import { AlertsBell, isAlertsInstalled } from '@/components/alerts/AlertsBell'
|
||||
import { AlertsBell, useAlertsInstalled } from '@/components/alerts/AlertsBell'
|
||||
import { NotificationBannerSlot } from '@/components/alerts/NotificationBannerSlot'
|
||||
|
||||
// The favorites menu checks for collaboration and loads after hydration, so neither it nor the
|
||||
// collaboration hooks are in the dashboard's first load (the slot stays empty until then).
|
||||
const LazyFavoritesMenu = dynamic(() => import('@/components/collaboration/FavoritesMenuSlot').then((module) => module.LazyFavoritesMenu), { ssr: false })
|
||||
|
||||
/**
|
||||
* Inner layout that renders the full dashboard chrome.
|
||||
*
|
||||
@@ -70,7 +75,8 @@ const LazyFavoritesMenu = dynamic(() => import('@/components/collaboration/Favor
|
||||
* - Applies the application theme (tokens, rules, customCss) and the branded header bar.
|
||||
* - Configures analytics and records a page view per screen.
|
||||
* - Redirects unauthenticated users to `/login`.
|
||||
* - Registers global keyboard shortcuts: Cmd+K (command palette), `/` (search), `?` (help).
|
||||
* - Registers global keyboard shortcuts: Cmd+K and `/` (search), `?` (keyboard shortcuts); the
|
||||
* single-key ones only while the user has not turned single-key shortcuts off (WCAG 2.1.4).
|
||||
* - Renders the desktop sidebar, mobile sidebar drawer, banners, header, and main content slot.
|
||||
*
|
||||
* @param children - The authenticated page content to render in the main slot.
|
||||
@@ -85,15 +91,17 @@ function DashboardLayoutContent({ children }: { children: React.ReactNode }) {
|
||||
// Mobile sidebar drawer state
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false)
|
||||
const menuButtonRef = useRef<HTMLButtonElement>(null)
|
||||
const helpButtonRef = useRef<HTMLButtonElement>(null)
|
||||
|
||||
// Command palette state
|
||||
// The one search box (pages, records, actions, help, Ask): Cmd/Ctrl+K, `/`, the header field
|
||||
const [commandOpen, setCommandOpen] = useState(false)
|
||||
// opened from Help > Search help: scoped to help articles
|
||||
const [commandHelpOnly, setCommandHelpOnly] = useState(false)
|
||||
const openSearch = useCallback((helpOnly = false) => {
|
||||
setCommandHelpOnly(helpOnly)
|
||||
setCommandOpen(true)
|
||||
}, [])
|
||||
|
||||
// Search dialog state (/ key)
|
||||
const [searchOpen, setSearchOpen] = useState(false)
|
||||
|
||||
// Keyboard shortcuts help dialog state
|
||||
// Keyboard shortcuts sheet (`?`, or Help > Keyboard shortcuts)
|
||||
const [helpOpen, setHelpOpen] = useState(false)
|
||||
|
||||
// Load full application metadata
|
||||
@@ -101,6 +109,7 @@ function DashboardLayoutContent({ children }: { children: React.ReactNode }) {
|
||||
data: metaData,
|
||||
isLoading: metaLoading,
|
||||
error: metaError,
|
||||
refetch: reloadMetaData,
|
||||
} = useQuery({
|
||||
queryKey: queryKeys.metadataAll(),
|
||||
queryFn: loadMetaData,
|
||||
@@ -109,19 +118,28 @@ function DashboardLayoutContent({ children }: { children: React.ReactNode }) {
|
||||
})
|
||||
|
||||
// QQQ Assistant (qbit-assistant): hidden and inert unless its /assistant/config probe enables it
|
||||
const alertsInstalled = useAlertsInstalled()
|
||||
const assistant = useAssistantController(metaData, isAuthenticated)
|
||||
const { enabled: assistantEnabled, toggle: toggleAssistant } = assistant
|
||||
|
||||
// The user's single-key shortcut setting (server Keyboard section, or this browser's choice)
|
||||
useSingleKeyShortcutsSync(isAuthenticated)
|
||||
|
||||
// Generate sidebar routes and path map from app tree
|
||||
const { sidebarRoutes, pathToLabelMap, ancestorAppMap, navTargets, homePath } = useAppTreeRoutes(metaData)
|
||||
const { sidebarRoutes, pathToLabelMap, ancestorAppMap, navTargets, homePath, sectionRedirects } = useAppTreeRoutes(metaData)
|
||||
|
||||
// A section heading is not a page: its name as a path (an old link to a group that used to be an
|
||||
// app) goes to the app that heads it, at the section
|
||||
useEffect(() => {
|
||||
const target = sectionRedirects[(pathname ?? '').replace(/\/+$/, '')]
|
||||
if (target) router.replace(target)
|
||||
}, [pathname, sectionRedirects, router])
|
||||
|
||||
// Every route change starts at the top of the page (Material App scrolls to 0 on each pathname)
|
||||
useEffect(() => {
|
||||
scrollPageToTop()
|
||||
}, [pathname])
|
||||
|
||||
// Tables the backend record search covers for this user (none: search stays local)
|
||||
const searchTables = useMemo(() => searchableTables(metaData), [metaData])
|
||||
|
||||
// Sync pathToLabelMap to QContext
|
||||
useEffect(() => {
|
||||
@@ -130,8 +148,8 @@ function DashboardLayoutContent({ children }: { children: React.ReactNode }) {
|
||||
}
|
||||
}, [pathToLabelMap, setPathToLabelMap])
|
||||
|
||||
// Sync branding to QContext and inject theme
|
||||
useEffect(() => {
|
||||
// Install branding before the first branded shell is painted.
|
||||
useLayoutEffect(() => {
|
||||
if (metaData?.branding) {
|
||||
setBranding(metaData.branding)
|
||||
// accent colors (validated, MED-3) and the favicon
|
||||
@@ -154,7 +172,7 @@ function DashboardLayoutContent({ children }: { children: React.ReactNode }) {
|
||||
// customCss; removed again when the dashboard unmounts (sign-out)
|
||||
const { theme, setTheme } = useTheme()
|
||||
const applicationTheme = useMemo(() => readMaterialTheme(metaData), [metaData])
|
||||
useEffect(() => { setTheme(applicationTheme) }, [applicationTheme, setTheme])
|
||||
useLayoutEffect(() => { setTheme(applicationTheme) }, [applicationTheme, setTheme])
|
||||
useEffect(() => () => setTheme(null), [setTheme])
|
||||
|
||||
// Analytics (QRun-IO/qqq#730): off unless configured; one page view per screen
|
||||
@@ -162,10 +180,8 @@ function DashboardLayoutContent({ children }: { children: React.ReactNode }) {
|
||||
|
||||
// Sync user ID to QContext
|
||||
useEffect(() => {
|
||||
if (user?.email) {
|
||||
setUserId(user.email)
|
||||
}
|
||||
}, [user?.email, setUserId])
|
||||
setUserId(user?.id ?? user?.email)
|
||||
}, [user?.id, user?.email, setUserId])
|
||||
|
||||
// Redirect to login if not authenticated
|
||||
useEffect(() => {
|
||||
@@ -193,18 +209,19 @@ function DashboardLayoutContent({ children }: { children: React.ReactNode }) {
|
||||
/**
|
||||
* Global `keydown` handler that drives the application-level keyboard shortcuts.
|
||||
*
|
||||
* - Cmd+K / Ctrl+K — toggles the command palette.
|
||||
* - Cmd+K / Ctrl+K — toggles the search box.
|
||||
* - Cmd+J / Ctrl+J — toggles the assistant panel (when available; works in text fields too).
|
||||
* - Escape — closes all overlays (command palette, search dialog, help dialog, mobile sidebar).
|
||||
* - `.` — opens the command palette (only when not in a text field).
|
||||
* - `/` — opens the search dialog (only when not in a text field).
|
||||
* - `?` — opens the keyboard shortcuts dialog (only when not in a text field).
|
||||
* - Escape — closes all overlays (search box, shortcuts sheet, mobile sidebar).
|
||||
* - `/` — opens the search box (not in a text field; off with single-key shortcuts).
|
||||
* - `.` — the same, kept quietly for Material habits (off with single-key shortcuts).
|
||||
* - `?` — opens the keyboard shortcuts sheet (not in a text field; off with single-key shortcuts).
|
||||
*
|
||||
* @param e - The native DOM keyboard event.
|
||||
*/
|
||||
const handleGlobalKeyDown = useCallback((e: KeyboardEvent) => {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
||||
e.preventDefault()
|
||||
setCommandHelpOnly(false)
|
||||
setCommandOpen((prev) => !prev)
|
||||
}
|
||||
if ((e.metaKey || e.ctrlKey) && !e.altKey && !e.shiftKey && e.key.toLowerCase() === 'j' && assistantEnabled) {
|
||||
@@ -214,46 +231,35 @@ function DashboardLayoutContent({ children }: { children: React.ReactNode }) {
|
||||
}
|
||||
if (e.key === 'Escape') {
|
||||
setCommandOpen(false)
|
||||
setSearchOpen(false)
|
||||
setHelpOpen(false)
|
||||
// The navigation drawer is a modal dialog and handles its own Escape
|
||||
}
|
||||
|
||||
// Single-key shortcuts — only when not focused in a text field
|
||||
if (!isInputFocused() && !e.metaKey && !e.ctrlKey && !e.altKey) {
|
||||
// Single-key shortcuts — only when not focused in a text field, and not turned off (WCAG 2.1.4)
|
||||
if (!isInputFocused() && !e.metaKey && !e.ctrlKey && !e.altKey && singleKeyShortcutsEnabled()) {
|
||||
if (e.key === '.') {
|
||||
e.preventDefault()
|
||||
recordAnalytics({ category: 'globalEvents', action: 'dotMenuKeyboardShortcut' })
|
||||
setCommandOpen(true)
|
||||
openSearch()
|
||||
}
|
||||
if (e.key === '/') {
|
||||
e.preventDefault()
|
||||
setSearchOpen(true)
|
||||
openSearch()
|
||||
}
|
||||
if (e.key === '?') {
|
||||
e.preventDefault()
|
||||
setHelpOpen(true)
|
||||
}
|
||||
}
|
||||
}, [isInputFocused, assistantEnabled, toggleAssistant])
|
||||
}, [isInputFocused, assistantEnabled, toggleAssistant, openSearch])
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('keydown', handleGlobalKeyDown)
|
||||
return () => document.removeEventListener('keydown', handleGlobalKeyDown)
|
||||
}, [handleGlobalKeyDown])
|
||||
|
||||
// Show loading state while auth initializes
|
||||
if (authLoading) {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center">
|
||||
<div
|
||||
className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent"
|
||||
role="status"
|
||||
aria-label="Loading"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
// Use the same neutral screen through authentication and metadata loading.
|
||||
if (authLoading) return <ApplicationLoading />
|
||||
|
||||
// Auth loaded but not authenticated — redirect handled above
|
||||
if (!isAuthenticated) {
|
||||
@@ -265,23 +271,28 @@ function DashboardLayoutContent({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center">
|
||||
<div className="text-center">
|
||||
<p className="text-destructive">Failed to load application metadata.</p>
|
||||
<p role="alert" className="text-destructive">Unable to load your workspace.</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
{metaError instanceof Error ? metaError.message : 'Unknown error'}
|
||||
</p>
|
||||
<button type="button" className="mt-4 rounded-md border px-4 py-2" onClick={() => { void reloadMetaData() }}>Try again</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!metaData) return <ApplicationLoading />
|
||||
|
||||
// One scroll container: the shell is exactly the viewport and only <main> scrolls. Both are
|
||||
// positioned (`relative`) so they contain absolutely positioned descendants (`sr-only` text,
|
||||
// tooltips): otherwise such an element's containing block is the viewport, `overflow-hidden`
|
||||
// cannot clip it, and one near the end of a long page (the company footer's "opens in a new
|
||||
// tab") makes the document taller than the window, a second scrollbar beside main's.
|
||||
return (
|
||||
<AssistantProvider value={assistant}>
|
||||
<HelpPanelProvider instance={metaData}>
|
||||
<SecuredDataProvider enabled={isAuthenticated}><AssistantProvider value={assistant}>
|
||||
<div className="relative flex h-screen flex-col overflow-hidden bg-background" data-qqq-id="app-shell">
|
||||
<SecuredDataBannerSlot />
|
||||
{/* Top-of-site banner spans the whole window, above the sidebar */}
|
||||
<BannerComponent banners={metaData?.branding?.banners} slot="QFMD_TOP_OF_SITE" className="border-x-0 border-t-0" />
|
||||
<BrandedHeaderBar theme={theme} />
|
||||
@@ -291,6 +302,7 @@ function DashboardLayoutContent({ children }: { children: React.ReactNode }) {
|
||||
href="#main-content"
|
||||
className="sr-only focus:not-sr-only focus:fixed focus:left-4 focus:top-4 focus:z-[9999] focus:inline-flex focus:items-center pointer-coarse:focus:min-h-11 focus:rounded-md focus:bg-primary focus:px-4 focus:py-2 focus:text-sm focus:font-medium focus:text-primary-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
|
||||
data-qqq-id="skip-to-content"
|
||||
onClick={(event) => { if (skipToFirstZone()) event.preventDefault() }}
|
||||
>
|
||||
Skip to main content
|
||||
</a>
|
||||
@@ -300,9 +312,7 @@ function DashboardLayoutContent({ children }: { children: React.ReactNode }) {
|
||||
routes={sidebarRoutes}
|
||||
branding={metaData?.branding}
|
||||
homePath={homePath}
|
||||
logout={logout}
|
||||
userName={user?.name}
|
||||
userEmail={user?.email}
|
||||
showFavorites={isAuthenticated}
|
||||
data-qqq-id="sidebar-container"
|
||||
/>
|
||||
|
||||
@@ -311,9 +321,8 @@ function DashboardLayoutContent({ children }: { children: React.ReactNode }) {
|
||||
routes={sidebarRoutes}
|
||||
branding={metaData?.branding}
|
||||
homePath={homePath}
|
||||
logout={logout}
|
||||
userName={user?.name}
|
||||
userEmail={user?.email}
|
||||
showFavorites={isAuthenticated}
|
||||
onSearchOpen={() => openSearch()}
|
||||
open={sidebarOpen}
|
||||
onClose={() => setSidebarOpen(false)}
|
||||
returnFocusRef={menuButtonRef}
|
||||
@@ -328,22 +337,37 @@ function DashboardLayoutContent({ children }: { children: React.ReactNode }) {
|
||||
onMenuOpen={() => setSidebarOpen(true)}
|
||||
menuOpen={sidebarOpen}
|
||||
menuButtonRef={menuButtonRef}
|
||||
onSearchOpen={() => setSearchOpen(true)}
|
||||
onCommandOpen={() => setCommandOpen(true)}
|
||||
onHelpOpen={() => setHelpOpen(true)}
|
||||
helpButtonRef={helpButtonRef}
|
||||
onSearchOpen={() => openSearch()}
|
||||
pathToLabelMap={pathToLabelMap}
|
||||
ancestorAppMap={ancestorAppMap}
|
||||
homePath={homePath}
|
||||
navTargets={navTargets}
|
||||
searchTables={searchTables}
|
||||
assistantButton={<AssistantHeaderButton />}
|
||||
favoritesButton={isAuthenticated ? <LazyFavoritesMenu /> : null}
|
||||
notificationsButton={isAuthenticated && isAlertsInstalled(metaData) ? <AlertsBell metaData={metaData} /> : null}
|
||||
notificationsButton={isAuthenticated && alertsInstalled ? <AlertsBell metaData={metaData} /> : null}
|
||||
helpMenu={<HelpMenu onSearchHelp={() => openSearch(true)} onShortcuts={() => setHelpOpen(true)} />}
|
||||
accountMenu={(
|
||||
<AccountMenu
|
||||
userName={user?.name}
|
||||
userEmail={user?.email}
|
||||
logout={logout}
|
||||
compactItems={(close) => (
|
||||
<>
|
||||
{/* the compact header has no room for Help and Ask: they lead this menu (help stays in one place) */}
|
||||
<HelpMenuItems close={close} onSearchHelp={() => openSearch(true)} onShortcuts={() => setHelpOpen(true)} />
|
||||
{assistantEnabled && (
|
||||
<button type="button" role="menuitem" className={HEADER_MENU_ITEM_CLASS} data-qqq-id="menu-item-ask"
|
||||
onClick={() => close(toggleAssistant)}>
|
||||
<Sparkles className="h-4 w-4 text-muted-foreground" aria-hidden="true" />
|
||||
Ask the assistant
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Page content */}
|
||||
<main className="relative flex-1 overflow-y-auto p-6" id="main-content" data-qqq-id="main-content">
|
||||
<main className="relative flex-1 overflow-y-auto p-6 focus:outline-none" id="main-content" tabIndex={-1} data-qqq-id="main-content">
|
||||
{metaLoading ? (
|
||||
<div
|
||||
className="flex items-center justify-center py-12"
|
||||
@@ -374,16 +398,15 @@ function DashboardLayoutContent({ children }: { children: React.ReactNode }) {
|
||||
onClose={() => setCommandOpen(false)}
|
||||
navTargets={navTargets}
|
||||
metaData={metaData}
|
||||
helpOnly={commandHelpOnly}
|
||||
onAskAssistant={assistantEnabled ? (text) => assistant.openWith(text) : undefined}
|
||||
/>
|
||||
|
||||
{/* Search Dialog */}
|
||||
<SearchDialog open={searchOpen} onClose={() => setSearchOpen(false)} navTargets={navTargets} searchTables={searchTables} />
|
||||
|
||||
{/* Keyboard Shortcuts Help Dialog */}
|
||||
<KeyboardShortcutsDialog open={helpOpen} onClose={() => setHelpOpen(false)} returnFocusRef={helpButtonRef} assistantEnabled={assistantEnabled} />
|
||||
{/* Keyboard Shortcuts Sheet */}
|
||||
<KeyboardShortcutsDialog open={helpOpen} onClose={() => setHelpOpen(false)} assistantEnabled={assistantEnabled} />
|
||||
</div>
|
||||
</AssistantProvider>
|
||||
</AssistantProvider></SecuredDataProvider>
|
||||
</HelpPanelProvider>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
import type { Metadata } from 'next'
|
||||
import { Inter } from 'next/font/google'
|
||||
import Providers from './providers'
|
||||
import { themeScript } from '@/lib/theme/theme-script'
|
||||
import '@/styles/globals.css'
|
||||
|
||||
/** Inter font configuration with the CSS variable `--font-inter`. */
|
||||
@@ -71,6 +72,7 @@ export default function RootLayout({
|
||||
return (
|
||||
<html lang="en" className={inter.variable} suppressHydrationWarning>
|
||||
<body className="font-sans antialiased" suppressHydrationWarning>
|
||||
<script dangerouslySetInnerHTML={{ __html: themeScript }} />
|
||||
<Providers>{children}</Providers>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
import Link from 'next/link'
|
||||
import { SearchX } from 'lucide-react'
|
||||
|
||||
import { NestedAppRedirect } from '@/components/layout/NestedAppRedirect'
|
||||
import { LegacyPathRedirect } from '@/components/layout/LegacyPathRedirect'
|
||||
|
||||
/**
|
||||
@@ -39,26 +40,28 @@ import { LegacyPathRedirect } from '@/components/layout/LegacyPathRedirect'
|
||||
export default function NotFound() {
|
||||
return (
|
||||
// A Material dashboard URL (nested under its apps) opens at its path here instead
|
||||
<LegacyPathRedirect>
|
||||
<div className="flex min-h-screen items-center justify-center bg-muted">
|
||||
<div className="flex flex-col items-center gap-6 text-center">
|
||||
<SearchX className="h-16 w-16 text-muted-foreground" aria-hidden="true" />
|
||||
<div>
|
||||
<h1 className="text-6xl font-bold text-foreground">404</h1>
|
||||
<p className="mt-2 text-xl text-muted-foreground">Page not found</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
The page you're looking for doesn't exist or has been moved.
|
||||
</p>
|
||||
<NestedAppRedirect>
|
||||
<LegacyPathRedirect>
|
||||
<div className="flex min-h-screen items-center justify-center bg-muted">
|
||||
<div className="flex flex-col items-center gap-6 text-center">
|
||||
<SearchX className="h-16 w-16 text-muted-foreground" aria-hidden="true" />
|
||||
<div>
|
||||
<h1 className="text-6xl font-bold text-foreground">404</h1>
|
||||
<p className="mt-2 text-xl text-muted-foreground">Page not found</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
The page you're looking for doesn't exist or has been moved.
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/"
|
||||
className="inline-flex items-center gap-2 rounded-md bg-primary px-4 py-2 pointer-coarse:min-h-11 text-sm font-medium text-primary-foreground hover:bg-primary/90 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||
data-qqq-id="link-go-home"
|
||||
>
|
||||
Go Home
|
||||
</Link>
|
||||
</div>
|
||||
<Link
|
||||
href="/"
|
||||
className="inline-flex items-center gap-2 rounded-md bg-primary px-4 py-2 pointer-coarse:min-h-11 text-sm font-medium text-primary-foreground hover:bg-primary/90 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||
data-qqq-id="link-go-home"
|
||||
>
|
||||
Go Home
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</LegacyPathRedirect>
|
||||
</LegacyPathRedirect>
|
||||
</NestedAppRedirect>
|
||||
)
|
||||
}
|
||||
@@ -31,6 +31,7 @@ import { AuthProvider } from '@/lib/auth/auth-provider'
|
||||
import { ThemeProvider } from '@/lib/theme/theme-provider'
|
||||
import { injectThemeTokens } from '@/lib/theme/tokens'
|
||||
import { AppToaster } from '@/components/feedback/Toast'
|
||||
import { QaCaptureLoader } from '@/components/qa-capture/QaCaptureLoader'
|
||||
|
||||
// Inject default theme tokens on first render
|
||||
if (typeof window !== 'undefined') {
|
||||
@@ -79,7 +80,11 @@ export default function Providers({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ThemeProvider>
|
||||
<AuthProvider>{children}</AuthProvider>
|
||||
<AuthProvider>
|
||||
{/* QA capture (qa-capture extension): nothing is loaded unless the server enables it for this user */}
|
||||
<QaCaptureLoader />
|
||||
{children}
|
||||
</AuthProvider>
|
||||
</ThemeProvider>
|
||||
<AppToaster />
|
||||
</QueryClientProvider>
|
||||
|
||||
@@ -14,22 +14,21 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// Tests for the Alerts record panel: metadata gating (no request without the qbit's app) and the
|
||||
// Tests for the Alerts record panel: extension gating (no request without the extension) and the
|
||||
// panel's hidden / open / watch states. The bell, popover and banners are in
|
||||
// NotificationCenter.test.tsx.
|
||||
|
||||
import React from 'react'
|
||||
import { render, screen, waitFor, within } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { render, screen, within } from '@testing-library/react'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { http, HttpResponse } from 'msw'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { axe } from 'jest-axe'
|
||||
|
||||
import { uiApiVersionHandler } from '@/mocks/ui-api-version'
|
||||
import { server } from '@/mocks/node'
|
||||
import type { AlertsRecordPanel } from '@/types/alerts'
|
||||
import type { QInstance } from '@/types/metadata'
|
||||
import { isAlertsInstalled } from './AlertsBell'
|
||||
import { AlertsRecordPanelSlot } from './AlertsRecordPanelSlot'
|
||||
|
||||
const router = vi.hoisted(() => ({ push: vi.fn(), replace: vi.fn(), back: vi.fn(), forward: vi.fn(), refresh: vi.fn(), prefetch: vi.fn() }))
|
||||
@@ -45,10 +44,10 @@ const SUMMARY = { unreadCount: 0, highestUnreadSeverity: null, notificationsHref
|
||||
const PANEL: AlertsRecordPanel = {
|
||||
visible: true,
|
||||
openCount: 1,
|
||||
open: [{ alertId: 9, title: 'Post has no excerpt', severity: 'WARNING', status: 'OPEN', group: 'Content', age: '1 d', href: '/app/alerts_alert/9' }],
|
||||
open: [{ alertId: 9, title: 'Post has no excerpt', severity: 'WARNING', status: 'OPEN', group: 'Content', age: '1 d', href: '/app/alerts_alert/9', actions: [{ name: 'ack', label: 'Acknowledge', href: '/app/customAck?record=9' }, { name: 'later', label: 'Snooze', href: '/app/customSnooze/9' }, { name: 'api', label: 'API only', operationId: 'customOperation' }] }],
|
||||
resolvedLast30Days: 2,
|
||||
viewAllHref: '/app/alerts_alert?filter=x',
|
||||
canManage: true,
|
||||
canManage: false,
|
||||
watching: false,
|
||||
}
|
||||
|
||||
@@ -69,16 +68,11 @@ function renderWithClient(ui: React.ReactElement) {
|
||||
return render(<QueryClientProvider client={client}>{ui}</QueryClientProvider>)
|
||||
}
|
||||
|
||||
describe('isAlertsInstalled', () => {
|
||||
it('is true only when the metadata lists the alerts app', () => {
|
||||
expect(isAlertsInstalled(WITH_ALERTS)).toBe(true)
|
||||
expect(isAlertsInstalled(WITHOUT_ALERTS)).toBe(false)
|
||||
expect(isAlertsInstalled(undefined)).toBe(false)
|
||||
})
|
||||
})
|
||||
beforeEach(() => server.use(uiApiVersionHandler('alerts')))
|
||||
|
||||
describe('AlertsRecordPanelSlot', () => {
|
||||
it('sends no request when the metadata does not announce Alerts', async () => {
|
||||
it('sends no request when the version does not announce Alerts', async () => {
|
||||
server.use(uiApiVersionHandler())
|
||||
let calls = 0
|
||||
server.use(
|
||||
http.get('/qqq/v1/alerts/inbox/summary', () => {
|
||||
@@ -123,8 +117,7 @@ describe('AlertsRecordPanelSlot', () => {
|
||||
expect(none.container).toBeEmptyDOMElement()
|
||||
})
|
||||
|
||||
it('shows open alerts with actions, recent resolutions, and watches the record', async () => {
|
||||
let watched: unknown = null
|
||||
it('shows open alerts with actions and recent resolutions; watching is read-only', async () => {
|
||||
let panel = PANEL
|
||||
server.use(
|
||||
http.get('/qqq/v1/alerts/inbox/summary', () => HttpResponse.json(SUMMARY)),
|
||||
@@ -133,24 +126,26 @@ describe('AlertsRecordPanelSlot', () => {
|
||||
expect(url.searchParams.get('tableName')).toBe('blog_post')
|
||||
expect(url.searchParams.get('recordId')).toBe('5')
|
||||
return HttpResponse.json(panel)
|
||||
}),
|
||||
http.post('/qqq/v1/alerts/record-panel/watch', async ({ request }) => {
|
||||
watched = await request.json()
|
||||
panel = { ...PANEL, watching: true }
|
||||
return HttpResponse.json({ watching: true, created: true })
|
||||
})
|
||||
)
|
||||
const user = userEvent.setup()
|
||||
renderWithClient(<AlertsRecordPanelSlot metaData={WITH_ALERTS} tableName="blog_post" recordId="5" />)
|
||||
const view = renderWithClient(<AlertsRecordPanelSlot metaData={WITH_ALERTS} tableName="blog_post" recordId="5" />)
|
||||
const section = await screen.findByRole('region', { name: 'Open alerts (1)' })
|
||||
expect(await axe(section)).toHaveNoViolations()
|
||||
expect(within(section).getByRole('link', { name: 'Post has no excerpt' })).toHaveAttribute('href', '/app/alerts_alert/9')
|
||||
expect(within(section).getByRole('link', { name: 'Acknowledge' })).toHaveAttribute('href', '/app/alertsAcknowledge?recordsParam=recordIds&recordIds=9&tableName=alerts_alert')
|
||||
expect(within(section).getByRole('link', { name: 'Snooze' })).toBeInTheDocument()
|
||||
expect(within(section).getByRole('link', { name: 'Acknowledge' })).toHaveAttribute('href', '/app/customAck?record=9')
|
||||
expect(within(section).getByRole('link', { name: 'Snooze' })).toHaveAttribute('href', '/app/customSnooze/9')
|
||||
expect(within(section).queryByRole('link', { name: 'API only' })).not.toBeInTheDocument()
|
||||
expect(within(section).getByText('2 resolved in the last 30 days')).toBeInTheDocument()
|
||||
|
||||
await user.click(within(section).getByRole('button', { name: 'Watch alerts for this record' }))
|
||||
await waitFor(() => expect(watched).toEqual({ tableName: 'blog_post', recordId: '5' }))
|
||||
expect(await within(section).findByText("You watch this record's alerts")).toBeInTheDocument()
|
||||
// the alerts extension (1.0.0) removed the watch operation: no button, only the legacy state
|
||||
expect(within(section).queryByRole('button')).not.toBeInTheDocument()
|
||||
expect(within(section).queryByText("You watch this record's alerts")).not.toBeInTheDocument()
|
||||
view.unmount()
|
||||
|
||||
panel = { ...PANEL, watching: true, open: PANEL.open.map(({ actions: _actions, ...alert }) => alert) }
|
||||
renderWithClient(<AlertsRecordPanelSlot metaData={WITH_ALERTS} tableName="blog_post" recordId="5" />)
|
||||
expect(await screen.findByText("You watch this record's alerts")).toBeInTheDocument()
|
||||
expect(screen.queryByRole('link', { name: 'Acknowledge' })).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('link', { name: 'Snooze' })).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -16,10 +16,9 @@
|
||||
|
||||
/**
|
||||
* @file AlertsBell — the header bell of the notification center (qbit-alerts). Like the QQQ
|
||||
* Assistant, it is gated on instance metadata: unless the metadata lists the qbit's `alerts`
|
||||
* app (which QQQ includes only when qbit-alerts is installed and the user holds `alerts.view`,
|
||||
* the same permission its `/alerts/*` routes require), it renders nothing and sends no request
|
||||
* at all. When enabled it polls the inbox summary every `pollSeconds` (60) while the tab is
|
||||
* Assistant, it is gated on the server's registered UI API extensions. Per-user access is
|
||||
* checked by the inbox summary (403/404 stops polling). It sends no inbox request
|
||||
* when the extension is absent. When enabled it polls the inbox summary every `pollSeconds` (60) while the tab is
|
||||
* visible (TanStack Query pauses interval refetches in a hidden tab and refetches when it shows
|
||||
* again), backing off exponentially to 5 minutes after errors. The popover's code loads on
|
||||
* first open, so the shell stays small.
|
||||
@@ -31,28 +30,20 @@ import React, { Suspense, lazy, useRef, useState } from 'react'
|
||||
import { Bell } from 'lucide-react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
|
||||
import { useUiApiExtensionInstalled, UI_API_EXTENSION } from '@/lib/api/ui-api-extensions'
|
||||
import { getAlertsInboxSummary } from '@/lib/api/alerts'
|
||||
import { queryKeys } from '@/lib/query-client'
|
||||
import type { AlertsInboxSummary } from '@/types/alerts'
|
||||
import type { QInstance } from '@/types/metadata'
|
||||
import { HEADER_BADGE_CLASS, HEADER_BUTTON_CLASS, HeaderIconAnchor } from '@/components/layout/HeaderMenu'
|
||||
|
||||
const AlertsBellPopover = lazy(() => import('./AlertsBellPopover'))
|
||||
|
||||
/** The app qbit-alerts publishes (`AlertsQBitConfig.APP_NAME`), gated on `alerts.view`. */
|
||||
export const ALERTS_APP_NAME = 'alerts'
|
||||
|
||||
/**
|
||||
* Whether the instance metadata announces qbit-alerts to this user: its `alerts` app is
|
||||
* present. QQQ leaves an app out of the metadata when the qbit is not installed or the user
|
||||
* lacks its permission (`alerts.view`, which the `/alerts/*` routes also require), so this is
|
||||
* the precondition for any Alerts request.
|
||||
*
|
||||
* @param metaData - Instance metadata.
|
||||
* @returns True when the Alerts app is present.
|
||||
/** Whether the server registers the Alerts extension.
|
||||
* @returns True when Alerts is installed.
|
||||
*/
|
||||
export function isAlertsInstalled(metaData: QInstance | undefined): boolean {
|
||||
const app = metaData?.apps?.[ALERTS_APP_NAME]
|
||||
return Boolean(app) && typeof app === 'object'
|
||||
export function useAlertsInstalled(): boolean {
|
||||
return useUiApiExtensionInstalled(UI_API_EXTENSION.alerts)
|
||||
}
|
||||
|
||||
/** The longest wait between polls after errors (5 minutes). */
|
||||
@@ -96,7 +87,7 @@ export function pollDelay(data: AlertsInboxSummary | null | undefined, failed: n
|
||||
* Shared with the record panel slot and the banner slot. It never fetches (nor polls) unless
|
||||
* `enabled`.
|
||||
*
|
||||
* @param enabled - Whether to load it: authenticated and {@link isAlertsInstalled}.
|
||||
* @param enabled - Whether to load it: authenticated and {@link useAlertsInstalled}.
|
||||
* @returns The TanStack Query result.
|
||||
*/
|
||||
export function useAlertsInbox(enabled: boolean) {
|
||||
@@ -115,40 +106,44 @@ export function useAlertsInbox(enabled: boolean) {
|
||||
/**
|
||||
* The header bell with its unread badge (red when a CRITICAL alert is unread).
|
||||
*
|
||||
* @param props - `metaData`: instance metadata, which must announce Alerts ({@link isAlertsInstalled}).
|
||||
* @param _props - `metaData`: retained for caller compatibility; registration comes from the version endpoint.
|
||||
* @returns The bell, or null when Alerts is not available.
|
||||
*/
|
||||
export function AlertsBell({ metaData }: { metaData: QInstance | undefined }) {
|
||||
const { data } = useAlertsInbox(isAlertsInstalled(metaData))
|
||||
export function AlertsBell(_props: { metaData: QInstance | undefined }) {
|
||||
const { data } = useAlertsInbox(useAlertsInstalled())
|
||||
const [open, setOpen] = useState(false)
|
||||
const buttonRef = useRef<HTMLButtonElement>(null)
|
||||
if (!data) return null
|
||||
|
||||
const unread = data.unreadCount
|
||||
const label = unread > 0 ? `Notifications, ${unread} unread` : 'Notifications, none unread'
|
||||
const critical = data.highestUnreadSeverity === 'CRITICAL'
|
||||
// severity is in the name, not only in the badge's color
|
||||
const label = unread > 0 ? `Notifications, ${unread} unread${critical ? ', critical' : ''}` : 'Notifications, none unread'
|
||||
return (
|
||||
<div className="relative">
|
||||
<button
|
||||
ref={buttonRef}
|
||||
type="button"
|
||||
onClick={() => setOpen((value) => !value)}
|
||||
className="relative flex items-center justify-center rounded-lg p-2 hover:bg-accent focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
className={HEADER_BUTTON_CLASS}
|
||||
aria-label={label}
|
||||
title="Notifications"
|
||||
aria-haspopup="dialog"
|
||||
aria-expanded={open}
|
||||
data-qqq-id="button-notifications"
|
||||
>
|
||||
<Bell className="h-5 w-5 text-foreground/60" aria-hidden="true" />
|
||||
{unread > 0 && (
|
||||
<span
|
||||
className={`absolute right-0.5 top-0.5 flex h-4 min-w-4 items-center justify-center rounded-full px-1 text-[10px] font-semibold leading-none ${critical ? 'bg-destructive text-white' : 'bg-primary text-primary-foreground'}`}
|
||||
aria-hidden="true"
|
||||
data-qqq-id="alerts-unread-badge"
|
||||
>
|
||||
{unread > 99 ? '99+' : unread}
|
||||
</span>
|
||||
)}
|
||||
<HeaderIconAnchor>
|
||||
<Bell className="h-5 w-5" aria-hidden="true" />
|
||||
{unread > 0 && (
|
||||
<span
|
||||
className={`${HEADER_BADGE_CLASS} ${critical ? 'bg-destructive text-white' : 'bg-primary text-primary-foreground'}`}
|
||||
aria-hidden="true"
|
||||
data-qqq-id="alerts-unread-badge"
|
||||
>
|
||||
{unread > 99 ? '99+' : unread}
|
||||
</span>
|
||||
)}
|
||||
</HeaderIconAnchor>
|
||||
</button>
|
||||
<span className="sr-only" aria-live="polite" data-qqq-id="alerts-unread-announcer">{label}</span>
|
||||
{open && (
|
||||
|
||||