Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e58db9d5e9
|
||
|
|
09d1c2b793
|
||
|
|
728dd2b559 | ||
|
|
bdc38f08e9 | ||
|
|
df9265e877 | ||
|
|
bb05d81cdb
|
||
|
|
5b27bb3989
|
||
|
|
89c35e9f2c
|
||
|
|
2e43dc23d8 | ||
|
|
5efb0782cd
|
||
|
|
bac9215e32 | ||
|
|
cc13b045f4 | ||
|
|
62d76af238
|
||
|
|
974407da47
|
||
|
|
bfe8a6e31c
|
||
|
|
a6c95b1ec7 | ||
|
|
5cafa460b0
|
||
|
|
4e00e7f644
|
||
|
|
db0b421678
|
||
|
|
01fb7f6965
|
||
|
|
1a6c126721
|
||
|
|
f833454c17
|
||
|
|
d01d470b6e
|
||
|
|
0997deb12c
|
||
|
|
196c899389
|
||
|
|
63dcf7e144
|
||
|
|
35cc9da17c
|
||
|
|
e2e0c984ce
|
||
|
|
8b9b14e2e5
|
||
|
|
60d0b0ffc7 | ||
|
|
805ce7d72c
|
||
|
|
690737fa46
|
||
|
|
1f4a38b9ff
|
||
|
|
1afd72ad32
|
||
|
|
7c130187b9
|
||
|
|
35de9ef8bb
|
||
|
|
1f624a2544
|
||
|
|
438eda36e3 | ||
|
|
d1c181aae9 | ||
|
|
c09dd23fc3 | ||
|
|
9b006df8b2
|
||
|
|
0ef9fa7273
|
||
|
|
3d30490859
|
||
|
|
4376c28896
|
||
|
|
5fd1015b80
|
||
|
|
4604312b18
|
||
|
|
295cf16bbf
|
||
|
|
fc5f5d946d
|
||
|
|
fce8aad1ad
|
||
|
|
14ec5cc390
|
||
|
|
35249098bb | ||
|
|
d4f5bf9b7b
|
||
|
|
8f7e741bac
|
||
|
|
77d61670c3
|
||
|
|
33d7d6e9c8
|
||
|
|
dc1afc146f
|
||
|
|
343e603874
|
||
|
|
df8e27708f | ||
|
|
e598437ef5
|
||
|
|
816dad4a37
|
||
|
|
277116e35e
|
||
|
|
8a0fbbd334
|
||
|
|
eb3ad6853c
|
||
|
|
b7f3c71b74
|
||
|
|
6316df0b4a
|
||
|
|
455d8a587c | ||
|
|
88ac531e50 | ||
|
|
16a3c0797d
|
||
|
|
a2f231862c | ||
|
|
11a8f275d4 | ||
|
|
248bca9f43
|
||
|
|
220f106d62
|
||
|
|
c2d0f61c15
|
||
|
|
509ab237cf | ||
|
|
641f4718e5
|
||
|
|
46c38f5c05
|
||
|
|
0282328bb0 | ||
|
|
7fcb6ac301
|
||
|
|
4b65167cb9
|
||
|
|
385aa693b1 | ||
|
|
347d314ab0 | ||
|
|
6989970011
|
||
|
|
d62a63eb2e
|
||
|
|
39acafc61d | ||
|
|
4792a5b66f | ||
|
|
be6217537d | ||
|
|
6d0558653f
|
||
|
|
ffde7af138
|
||
|
|
30b61917fc
|
||
|
|
705defe5f6 | ||
|
|
2b84663d7b
|
||
|
|
820f5e2e21
|
||
|
|
aab7acf7a6
|
||
|
|
1056674285
|
||
|
|
b6688d780b
|
||
|
|
c63510e200 | ||
|
|
01dd7e9586
|
||
|
|
c7fe584911 | ||
|
|
6abfb8bdaa
|
||
|
|
05479bfcdf
|
||
|
|
6ed257565a | ||
|
|
9169112f18
|
||
|
|
03d7bae9e4
|
||
|
|
ef7f1c5714 | ||
|
|
42ddc06864
|
||
|
|
6dea1c3a86 | ||
|
|
ebc734545e
|
||
|
|
67e817adfa | ||
|
|
47dcd6ff50
|
||
|
|
7720ce2fbe | ||
|
|
e7ab2f8f9b | ||
|
|
bc79580464
|
||
|
|
008aa8daf8
|
||
|
|
22648a5c79
|
||
|
|
78edbdf4c3
|
||
|
|
4d673edb5a | ||
|
|
a3ad88bc6f
|
||
|
|
12b2eee0a3
|
||
|
|
6bc5ea875f
|
||
|
|
9a5acd9e9e
|
||
|
|
363f68eb40
|
||
|
|
1187660904
|
||
|
|
92bcefddb8
|
||
|
|
be6ea9a723 | ||
|
|
6e7c908c06
|
||
|
|
ae47adc03b | ||
|
|
15a7e43e6f | ||
|
|
8d02617aad
|
||
|
|
54a67f69ef | ||
|
|
8ea0c89312 | ||
|
|
ed079d632b
|
||
|
|
ebdd4c2289
|
||
|
|
64cd1e6dc9
|
||
|
|
f11cc85685 | ||
|
|
7398f1aa01 | ||
|
|
061a7cd09b
|
||
|
|
596815c605
|
||
|
|
5de69be7e3 | ||
|
|
8880ec7974
|
||
|
|
df59430694
|
||
|
|
4644b4023d
|
||
|
|
9842bc6554
|
||
|
|
f23e03e417
|
||
|
|
3508c505f5
|
||
|
|
b32a29d25b | ||
|
|
fec62ec611
|
||
|
|
75c9c5d64d
|
||
|
|
98409c85af | ||
|
|
ddc66c6b63
|
||
|
|
2ed5239642
|
||
|
|
725ef87efd
|
||
|
|
a724fc191c | ||
|
|
5b4ef76b01
|
||
|
|
c1be9c61b3
|
||
|
|
c8b7e9b16a
|
||
|
|
72377ac30b | ||
|
|
4d8b93ed42 | ||
|
|
cf6a326bca
|
||
|
|
c2aa695044
|
||
|
|
6c1e3a67b1
|
||
|
|
9d87b6dfee
|
||
|
|
21ba08bcd5
|
||
|
|
89add474cf | ||
|
|
3f161f6fd5 | ||
|
|
c183249a4b
|
||
|
|
9ce3b81b04
|
||
|
|
a62b10ae12
|
||
|
|
71bcc37c9e
|
||
|
|
a9975487f6
|
||
|
|
95931a7c7e
|
||
|
|
38207b3ef5
|
||
|
|
1f4841bf12
|
||
|
|
5629bcb3c7 | ||
|
|
4fe5ee94f2 | ||
|
|
ed1dc64330 | ||
|
|
ef6f0a9219 | ||
|
|
3ae29246c9 | ||
|
|
6eac444a22 | ||
|
|
46158c42cc
|
||
|
|
03e7415aa2 | ||
|
|
b89d406419
|
||
|
|
b699204d87 | ||
|
|
51c2db74f8
|
||
|
|
f34269bd38
|
||
|
|
930804d36c
|
||
|
|
2f745d578b
|
||
|
|
b50513c3b4 | ||
|
|
60b110a0b1
|
||
|
|
148f9d653b
|
||
|
|
ace6579fc0
|
||
|
|
66edf51719
|
||
|
|
3175b47165
|
||
|
|
fabbad15a7
|
||
|
|
baf7e0a554
|
||
|
|
85da466255
|
||
|
|
a7acea381a
|
||
|
|
4d2b9f685c | ||
|
|
36382059bc | ||
|
|
51e00b4195
|
||
|
|
84c95e524a | ||
|
|
06d5aacd91 | ||
|
|
0449ff315a
|
||
|
|
577bd3dee8
|
||
|
|
4586f5b4f2 | ||
|
|
effee583b7
|
||
|
|
1f1c5f546a
|
||
|
|
a42887f6e5
|
||
|
|
a163dc53b1
|
||
|
|
77eef1c816
|
||
|
|
9a905890a8
|
||
|
|
fe0077e86c
|
||
|
|
61be012fc2
|
||
|
|
bc01d10a27 | ||
|
|
d3daa4d067
|
||
|
|
49788dd4e8
|
||
|
|
1412af5f69 | ||
|
|
9150fb732f
|
||
|
|
e540b6fcbd
|
||
|
|
7f03ba1f4c
|
||
|
|
68c2567f02
|
||
|
|
4e17d720f4
|
||
|
|
984c26b62b
|
||
|
|
d1eae03894
|
||
|
|
310c23f9cd
|
||
|
|
de387a61f7
|
||
|
|
e333223470 | ||
|
|
c064048c6f
|
||
|
|
2b4d82309c
|
||
|
|
4fc3dfd7aa
|
||
|
|
8c9619e7f7
|
||
|
|
d3298cca2b
|
||
|
|
edaa1ffb33
|
||
|
|
c564a29ce8
|
||
|
|
3ab0ed88fa
|
||
|
|
12ef8571f1
|
||
|
|
a4c8eaf8c9
|
||
|
|
26ab79f752
|
||
|
|
b0b0d469fe | ||
|
|
3ceed91214
|
||
|
|
4e12a5d524
|
||
|
|
18d1fd6e37 | ||
|
|
eee644a280 | ||
|
|
7682aa1ff0 | ||
|
|
8e334cd944 | ||
|
|
255554a184 | ||
|
|
e1d60b9634 | ||
|
|
d81b7ed5ae | ||
|
|
b8313b6819 | ||
|
|
22c9e02f20 | ||
|
|
138c3c6db5 | ||
|
|
70f26e9c9a | ||
|
|
2bc75d779e | ||
|
|
17e22edaee | ||
|
|
66d02c7b6a | ||
|
|
20fe7b7b9b | ||
|
|
4190ee01ba | ||
|
|
62d1e77847 | ||
|
|
a23fd997db | ||
|
|
f367756b8c | ||
|
|
4a055463bd | ||
|
|
c1a4efd7d7 | ||
|
|
0222f0dcad | ||
|
|
4fea636f24 | ||
|
|
5ca3e0b17a | ||
|
|
e0fb2598b0 | ||
|
|
61286717e5 | ||
|
|
a32ef74a9d | ||
|
|
d292c76530 | ||
|
|
e3fc4b2f52 | ||
|
|
b5f8be7645 | ||
|
|
8f552e2e29 | ||
|
|
1f18575abe | ||
|
|
6a5c1bb237 | ||
|
|
58a896345f | ||
|
|
bd904fe4ac | ||
|
|
09c271384c | ||
|
|
05763450ff | ||
|
|
aa0129f024 | ||
|
|
2bdc048440 | ||
|
|
7bd157dd81 | ||
|
|
2535e98c7e | ||
|
|
2853b9b148 | ||
|
|
29cb5584fc | ||
|
|
f36d8709c9 | ||
|
|
f7b2aa52a8 | ||
|
|
a1d3e48419 | ||
|
|
ea73118fca | ||
|
|
c629eb0b6c | ||
|
|
571ee7bc88 | ||
|
|
12cf7745f6 | ||
|
|
0f6581b6d7 | ||
|
|
46a1d5d7ec | ||
|
|
226efa841f | ||
|
|
dd130238be | ||
|
|
d8c803ef5f | ||
|
|
5e3738b73f | ||
|
|
289eca19d4 | ||
|
|
7ff90f1a63 | ||
|
|
50a935a4a5 | ||
|
|
40e987d4d0 | ||
|
|
71f5a39b09 | ||
|
|
45f99f27ce | ||
|
|
d17e880e79 | ||
|
|
f8fdb8e997 | ||
|
|
ad154b5b56 | ||
|
|
c6a58933a6 | ||
|
|
26e3a0d004 | ||
|
|
c9d7fcd393 | ||
|
|
b3e1ddda8b | ||
|
|
8d4668cdab | ||
|
|
254a0a2050 | ||
|
|
52cc038fa6 | ||
|
|
6090f2f9b4 | ||
|
|
32a9560970 | ||
|
|
96085c72e2 | ||
|
|
c029ad2a70 | ||
|
|
32a3b281ff | ||
|
|
a0a18cfa76 | ||
|
|
354ee2709d | ||
|
|
3c7c0e88b7 | ||
|
|
0679672aec | ||
|
|
6d0862284a | ||
|
|
cd3d4731d1 | ||
|
|
2eb075610e | ||
|
|
a317f0292d | ||
|
|
14d69667dc | ||
|
|
f0f0eae98e | ||
|
|
66ccd34642 | ||
|
|
c954005332 | ||
|
|
33cfb0b322 | ||
|
|
b9b1f3da84 | ||
|
|
f65cf6bfbd | ||
|
|
d5f275f85e | ||
|
|
d4795c80e6 | ||
|
|
b8b361d8da | ||
|
|
12c21a7560 | ||
|
|
d4e3289c61 | ||
|
|
7ddf45fa4d | ||
|
|
df33189ab4 | ||
|
|
bad28d9e21 | ||
|
|
8900d47e28 | ||
|
|
fcf08769d6 | ||
|
|
ac22946975 | ||
|
|
fbe9d148ec | ||
|
|
90a456ddcf | ||
|
|
263f92f7bd | ||
|
|
977c1dfda3 | ||
|
|
a8e4d3b210 | ||
|
|
9fb613d04c | ||
|
|
b1c0cedbe0 | ||
|
|
0316c7a980 | ||
|
|
3c409f9186 | ||
|
|
f2cdf03bf0 | ||
|
|
3676fe9980 | ||
|
|
1dbe1d91ef | ||
|
|
5be45cd0e1 | ||
|
|
b1495c4a44 | ||
|
|
1283bac07e | ||
|
|
b971340c39 | ||
|
|
5ae0c8c339 | ||
|
|
10094ee885 | ||
|
|
f7ae67f13d | ||
|
|
cb51aa614e | ||
|
|
f1a6a3a168 | ||
|
|
ca9b5374f4 | ||
|
|
1bd7bdcb94 | ||
|
|
dc640530f9 | ||
|
|
ef1f407993 | ||
|
|
e71ed784bd | ||
|
|
73adda133c | ||
|
|
9e98626d9d | ||
|
|
0905cfb0cd | ||
|
|
542ac47593 | ||
|
|
222199916e | ||
|
|
defb3abc82 | ||
|
|
e87fe90d91 | ||
|
|
d346d9bffd | ||
|
|
201a73901e | ||
|
|
d937507a2e | ||
|
|
bf8fa4e1c2 | ||
|
|
95847c25ab | ||
|
|
303ba61fe1 | ||
|
|
44c36fc87d | ||
|
|
eeab664e34 | ||
|
|
0fcd28344d | ||
|
|
b9a49ad29d | ||
|
|
69766f1902 | ||
|
|
e901df9101 | ||
|
|
49e51158aa | ||
|
|
381cce27e6 | ||
|
|
1395a8b7d7 | ||
|
|
093b8d758c | ||
|
|
654aa28a9f | ||
|
|
9732c56c99 | ||
|
|
113785600d | ||
|
|
a0d63ae981 | ||
|
|
a03a0ab7da | ||
|
|
30db3098ad | ||
|
|
e56ef0d7c4 | ||
|
|
5f47ffc630 | ||
|
|
8484779d51 | ||
|
|
121468b049 | ||
|
|
16dd6a7ba9 | ||
|
|
d969630e50 | ||
|
|
06cf1b0c91 | ||
|
|
d1db0cd002 | ||
|
|
096c55adbe | ||
|
|
4748b6c594 | ||
|
|
5077c78afd | ||
|
|
c44261376a | ||
|
|
6439944150 | ||
|
|
6027e65065 | ||
|
|
93b7b5a433 | ||
|
|
0f96c81dc8 | ||
|
|
9d29663fda | ||
|
|
0e71bb03c6 | ||
|
|
083db18e40 | ||
|
|
384c360c03 | ||
|
|
5cb2f5a922 | ||
|
|
75044c1781 | ||
|
|
1af7254bc4 | ||
|
|
214cb9c88e | ||
|
|
7524057454 | ||
|
|
bf97d71e22 | ||
|
|
97dd622fe1 | ||
|
|
c89da89ce5 | ||
|
|
38c75a49eb | ||
|
|
609d016c50 | ||
|
|
1694580521 | ||
|
|
658116dd83 | ||
|
|
c14e5aedb5 | ||
|
|
0aa65e2a26 | ||
|
|
f4faeeab56 | ||
|
|
5d7e76e6d2 | ||
|
|
ca400d5cee | ||
|
|
c7835b8968 | ||
|
|
7b2386dc75 | ||
|
|
6407176134 | ||
|
|
37bb68e659 | ||
|
|
ddda45d7de | ||
|
|
04df7cc7a4 | ||
|
|
3358f53db8 | ||
|
|
7736a6e815 | ||
|
|
642a350e2a | ||
|
|
6ff2272fa1 | ||
|
|
bac4c4e287 | ||
|
|
2b123fd1b3 | ||
|
|
ed95420e66 | ||
|
|
15a9918f8f | ||
|
|
84c4846a16 | ||
|
|
3e4d7c9520 | ||
|
|
3b20c412c5 | ||
|
|
3e5bed9c9c | ||
|
|
f1e92f3be8 | ||
|
|
b55bc57f6a | ||
|
|
560379501c | ||
|
|
d11741f2e0 | ||
|
|
c46dfc02dc | ||
|
|
7f299e2d03 | ||
|
|
a5322e553a | ||
|
|
1a31c75808 | ||
|
|
803a7ecd77 | ||
|
|
368d702581 | ||
|
|
e09ba6f79e | ||
|
|
3437e6d8b2 | ||
|
|
d61f12f28f | ||
|
|
10f0744b86 | ||
|
|
0dfc4d9ed2 | ||
|
|
dbf600e894 | ||
|
|
72f5a07ea6 | ||
|
|
9b4142c1b0 | ||
|
|
49b3e1720b | ||
|
|
4e51b5ddd8 | ||
|
|
2336e39573 | ||
|
|
962d48869c | ||
|
|
de94de58e4 | ||
|
|
f4b15f4616 | ||
|
|
20a5befbdb | ||
|
|
e9676e97af | ||
|
|
4c7918f785 | ||
|
|
19353e6562 | ||
|
|
510fc6e6dc | ||
|
|
74042c056e | ||
|
|
ec04caf2f7 | ||
|
|
3c4b24758c | ||
|
|
39bc6ae4f0 | ||
|
|
89b9708bb2 | ||
|
|
6d53c14bff | ||
|
|
9b19cd184a | ||
|
|
f543bfe1f0 | ||
|
|
037d0a86f8 | ||
|
|
cf3c3ad6e9 | ||
|
|
e3bea6e6a7 | ||
|
|
e42ad2b2bc | ||
|
|
622e273dc1 | ||
|
|
3e9960d3a4 | ||
|
|
c421e37e18 | ||
|
|
a7601087c7 | ||
|
|
fe4297eeff | ||
|
|
a16f4cd519 | ||
|
|
2774be8c26 | ||
|
|
cbfa7a9299 | ||
|
|
dbff3b49f7 | ||
|
|
c7ab4c6699 | ||
|
|
b79446c65f | ||
|
|
f1b58f50e5 | ||
|
|
144a340204 | ||
|
|
1276df8a4b | ||
|
|
710ec015c5 | ||
|
|
e54650291e | ||
|
|
6bb90cf76b | ||
|
|
0f4ae0a48d | ||
|
|
ce670a1499 | ||
|
|
4b202c4d8f | ||
|
|
336ec87be0 | ||
|
|
8450428445 | ||
|
|
301c5f6a51 | ||
|
|
35eaaa59e8 | ||
|
|
a186853397 | ||
|
|
6087831f26 | ||
|
|
4ba3254468 | ||
|
|
05a1076979 | ||
|
|
e5dba6c896 | ||
|
|
e05f3ece06 | ||
|
|
9593bbf2d8 | ||
|
|
e9fc79b5cb | ||
|
|
f46ae2aa09 | ||
|
|
3b84e3782d | ||
|
|
33227986ea | ||
|
|
7d34d0b6c9 | ||
|
|
df02feff26 | ||
|
|
df0712ee05 | ||
|
|
7af15dea7f | ||
|
|
289bc4126a | ||
|
|
a4e51003b7 | ||
|
|
555a90444f | ||
|
|
3d3bb6acf5 | ||
|
|
01e35477ce | ||
|
|
979bb06049 | ||
|
|
3498f7046f | ||
|
|
1127e2f659 | ||
|
|
b7f90cb27a | ||
|
|
2a29872394 | ||
|
|
33f7884035 | ||
|
|
4f833aac39 | ||
|
|
a2b3d93856 | ||
|
|
0caa294929 | ||
|
|
993d79d88c | ||
|
|
9691312b13 | ||
|
|
167e2a3a35 | ||
|
|
8fa7d6a046 | ||
|
|
fcf240d800 | ||
|
|
f108367db6 | ||
|
|
455c937845 | ||
|
|
b5aac3c9ff | ||
|
|
792f635941 | ||
|
|
f1bcddb8f2 | ||
|
|
3cafb4d09a | ||
|
|
2d051dc2ea | ||
|
|
f1589c3421 | ||
|
|
375901fc34 | ||
|
|
e8675bd7e3 | ||
|
|
9fedda9ac8 | ||
|
|
9ca9aa8e3b | ||
|
|
abe9bd7664 | ||
|
|
262f5a5bc8 | ||
|
|
010bb4e020 | ||
|
|
423cb3e48e | ||
|
|
465fa77855 | ||
|
|
4b54e026ac | ||
|
|
ddec18d8ad | ||
|
|
4e100a34f3 | ||
|
|
16fa3cf0c6 | ||
|
|
58a4b8cbe3 | ||
|
|
50c6c97565 | ||
|
|
09b2ebad71 | ||
|
|
5b0480dac2 | ||
|
|
90615a34f9 | ||
|
|
4627ca7198 | ||
|
|
ca0e2cb7e7 | ||
|
|
aeba4f7060 | ||
|
|
23c3073831 | ||
|
|
a178f66272 | ||
|
|
d19d522b6d | ||
|
|
0d272edd4f | ||
|
|
16fe7ed518 | ||
|
|
835acb5c79 | ||
|
|
3b2536b91b | ||
|
|
be49ac3299 | ||
|
|
e5e5b8ea72 | ||
|
|
ed98aec576 | ||
|
|
b2c7e71712 | ||
|
|
a1c68fdbc7 | ||
|
|
e48ad3173d | ||
|
|
f3b036b375 | ||
|
|
92851bf8b0 | ||
|
|
db0c6d86aa | ||
|
|
e57201ef55 | ||
|
|
0ee169b798 | ||
|
|
5e9f013376 | ||
|
|
40efee5f8b | ||
|
|
d3fad8cbda | ||
|
|
709edfbf15 | ||
|
|
d7b2ee8cd7 | ||
|
|
58b952445a | ||
|
|
1a71b4988d | ||
|
|
a8860f6d0d | ||
|
|
4bb147ea4e | ||
|
|
8147a29e3b | ||
|
|
d0818d1a61 | ||
|
|
53bc5d377c | ||
|
|
1f57dbf657 | ||
|
|
06c47ee3be | ||
|
|
604c44c7b9 | ||
|
|
caeadfb078 | ||
|
|
6c7eef0f9f | ||
|
|
2b6569f33b | ||
|
|
657510542c | ||
|
|
e33cca670e | ||
|
|
44fff10964 | ||
|
|
24d573cb10 | ||
|
|
2c5eb2d0bf | ||
|
|
8712543266 | ||
|
|
9d57547042 | ||
|
|
ff15d7eee4 | ||
|
|
cdb9549efd | ||
|
|
983e8d8230 | ||
|
|
c799b98bdf | ||
|
|
d9ddd1609d | ||
|
|
e9ce3f1931 | ||
|
|
947300b734 | ||
|
|
8484d7e4c6 | ||
|
|
a7962d1ba6 | ||
|
|
850f180e5e | ||
|
|
f856d926ba | ||
|
|
523da4ca49 | ||
|
|
50f4e3fe4d | ||
|
|
76105121cb | ||
|
|
0ba5fe447e | ||
|
|
82df289ba7 | ||
|
|
1714c6a2c3 | ||
|
|
3ddb1311f8 | ||
|
|
1a649a5458 | ||
|
|
57b80eb1fd | ||
|
|
eca667af73 | ||
|
|
e9eb14f4f0 | ||
|
|
301bf3b31b | ||
|
|
a98aa8f089 | ||
|
|
eff2d11254 | ||
|
|
730b38930a | ||
|
|
ce7a5a690a | ||
|
|
23a855297d | ||
|
|
818935ec23 | ||
|
|
cc58e1dcbb | ||
|
|
1d28e4608a | ||
|
|
8cc626d61f | ||
|
|
439349ddeb | ||
|
|
b10ce42f64 | ||
|
|
350be92d6d | ||
|
|
04e738671a | ||
|
|
d72143e7d1 | ||
|
|
93afb572f8 | ||
|
|
0d477151f8 | ||
|
|
cc0f91c3e1 | ||
|
|
6b0e305a9c | ||
|
|
f180a469c5 | ||
|
|
a78af68929 | ||
|
|
4ead47f053 | ||
|
|
7142274d76 | ||
|
|
6c78038ced | ||
|
|
5647e3c8e3 | ||
|
|
33c2335c94 | ||
|
|
14d5b9a831 | ||
|
|
6359846bba | ||
|
|
f276ea841a | ||
|
|
5b5af2ce18 | ||
|
|
d4c2ecc084 | ||
|
|
fec12b630f | ||
|
|
68f850aa1c | ||
|
|
a9c547f706 | ||
|
|
3040d20b45 | ||
|
|
2d84445645 | ||
|
|
a8b4b65373 | ||
|
|
41cd6d2034 | ||
|
|
8b8e1c7c03 | ||
|
|
12edc4cf1e | ||
|
|
511c005cad | ||
|
|
bca0cc7d6d | ||
|
|
e4b6742912 | ||
|
|
04118ae948 | ||
|
|
ab6f1ef3b8 | ||
|
|
da909ec1f4 | ||
|
|
81cd5346cb | ||
|
|
da72f89eb9 | ||
|
|
1bb154dee9 | ||
|
|
f075784933 | ||
|
|
01bd6254f9 | ||
|
|
c60a13facd | ||
|
|
b413535cb5 | ||
|
|
62b557345c | ||
|
|
806678e7f9 | ||
|
|
021a08cec5 | ||
|
|
8dbfddf631 | ||
|
|
66c8e1fe48 | ||
|
|
b1a65216f6 | ||
|
|
a0f1165459 | ||
|
|
c8bcf70c05 | ||
|
|
65f77c8f46 | ||
|
|
768e6ca7f8 | ||
|
|
55de69a6f5 | ||
|
|
13070e3b48 | ||
|
|
2f820518c7 | ||
|
|
76a76ac0c7 | ||
|
|
47f7707482 | ||
|
|
3affdbaa12 | ||
|
|
9c312c6b0a | ||
|
|
80abe29fe5 | ||
|
|
0d53307396 | ||
|
|
9eee90a8df | ||
|
|
a468612e34 | ||
|
|
15d8d1835d | ||
|
|
0bdde715e4 | ||
|
|
75e111a5f0 | ||
|
|
1ebc2cd437 | ||
|
|
7182332294 | ||
|
|
bc2a06c917 | ||
|
|
df8e2e61cf | ||
|
|
6073f1c4b3 | ||
|
|
d28c569b3a | ||
|
|
1137802f8c | ||
|
|
66db82b3ce | ||
|
|
26052f99db | ||
|
|
641961a335 |
No files matched your search
@@ -0,0 +1,80 @@
|
||||
version: 2.1
|
||||
|
||||
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
|
||||
|
||||
workflows:
|
||||
|
||||
test_only:
|
||||
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]+)?$/
|
||||
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(which pnpm:*)",
|
||||
"Bash(node:*)",
|
||||
"Bash(npm install:*)",
|
||||
"Bash(pnpm create:*)",
|
||||
"Bash(pnpm install:*)",
|
||||
"Bash(pnpm approve-builds:*)",
|
||||
"Bash(pnpm add:*)",
|
||||
"Bash(pnpm tsc:*)",
|
||||
"Bash(pnpm test:*)",
|
||||
"Bash(pnpm build:*)",
|
||||
"Bash(ls:*)",
|
||||
"Bash(git add:*)",
|
||||
"Bash(git commit:*)",
|
||||
"Bash(cat:*)",
|
||||
"Bash(git:*)",
|
||||
"Bash(pnpm dlx:*)",
|
||||
"Bash(echo killed:*)",
|
||||
"Bash(cp:*)",
|
||||
"Bash(grep:*)",
|
||||
"Bash(sleep 1:*)",
|
||||
"Bash(pnpm list:*)",
|
||||
"Bash(lsof:*)",
|
||||
"Bash(rm:*)",
|
||||
"Bash(pnpm lint:*)",
|
||||
"Bash(pnpm test:e2e:*)",
|
||||
"Bash(head:*)",
|
||||
"Bash(NEXT_PUBLIC_MOCK_API=true pnpm test:e2e:*)",
|
||||
"Bash(pnpm test:coverage:*)",
|
||||
"Bash(pnpm audit:*)",
|
||||
"Bash(pnpm up:*)",
|
||||
"Bash(pnpm remove:*)",
|
||||
"Bash(./node_modules/.bin/next lint:*)",
|
||||
"Bash(NEXT_PUBLIC_MOCK_API=true pnpm dev:*)",
|
||||
"mcp__circleci-mcp-server__list_followed_projects",
|
||||
"Bash(echo circleci CLI not found:*)",
|
||||
"Bash(circleci config:*)",
|
||||
"Bash(tail:*)",
|
||||
"mcp__circleci-mcp-server__get_latest_pipeline_status",
|
||||
"Bash(circleci orb:*)",
|
||||
"Bash(circleci diagnostic:*)",
|
||||
"mcp__circleci-mcp-server__run_pipeline",
|
||||
"mcp__circleci-mcp-server__get_build_failure_logs",
|
||||
"Bash(cd /Users/james.maes/Git.Local/QRun-IO/qqq-orb && make validate 2>&1)",
|
||||
"mcp__circleci-mcp-server__get_job_test_results",
|
||||
"Bash(pnpm typecheck:*)",
|
||||
"Bash(pnpm dev:*)",
|
||||
"Bash(python3:*)",
|
||||
"Bash(echo Exit: $?:*)",
|
||||
"Bash(gh api:*)",
|
||||
"Bash(pnpm why:*)",
|
||||
"Bash(npm view:*)",
|
||||
"Bash(chmod +x:*)",
|
||||
"Bash(done)",
|
||||
"Bash(pnpm exec:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
.git
|
||||
.next
|
||||
node_modules
|
||||
test-results
|
||||
playwright-report
|
||||
coverage
|
||||
.env*
|
||||
*.log
|
||||
@@ -0,0 +1,8 @@
|
||||
# Mock API environment — copy to .env.local to run without a real backend
|
||||
#
|
||||
# cp .env.mock .env.local && pnpm dev
|
||||
# OR pass inline:
|
||||
# NEXT_PUBLIC_MOCK_API=true pnpm dev
|
||||
|
||||
NEXT_PUBLIC_MOCK_API=true
|
||||
NEXT_PUBLIC_API_BASE_URL=http://localhost:3000/qqq/v1
|
||||
@@ -0,0 +1,10 @@
|
||||
#!/bin/sh
|
||||
# .githooks/pre-commit
|
||||
#
|
||||
# Enforces the Apache 2.0 license header on all staged TypeScript/TSX files.
|
||||
# Registered automatically via "prepare" in package.json.
|
||||
#
|
||||
# To install hooks manually:
|
||||
# git config core.hooksPath .githooks
|
||||
|
||||
pnpm lint:license
|
||||
@@ -0,0 +1,5 @@
|
||||
# Default reviewers for every change.
|
||||
* @KofTwentyTwo
|
||||
|
||||
# The UI API contract (QRun-IO/qqq-frontend-next#107): the vendored documents and the off-spec list
|
||||
/src/lib/api/openapi/ @KofTwentyTwo
|
||||
@@ -0,0 +1,32 @@
|
||||
# Weekly, grouped dependency updates. Minor and patch updates arrive as one PR per ecosystem;
|
||||
# major updates stay separate so each gets its own review. At most three version-update PRs per
|
||||
# ecosystem are open at once, so their real-backend acceptance runs do not starve release PRs of runners. Security updates are enabled in the
|
||||
# repository settings and are not limited by this schedule.
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: npm # pnpm-lock.yaml
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
open-pull-requests-limit: 3
|
||||
groups:
|
||||
npm-minor-and-patch:
|
||||
update-types: [minor, patch]
|
||||
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
open-pull-requests-limit: 3
|
||||
groups:
|
||||
github-actions:
|
||||
patterns: ['*']
|
||||
|
||||
- package-ecosystem: maven # pom.xml (the dashboard jar)
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
open-pull-requests-limit: 3
|
||||
groups:
|
||||
maven-minor-and-patch:
|
||||
update-types: [minor, patch]
|
||||
@@ -0,0 +1,281 @@
|
||||
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
|
||||
@@ -0,0 +1,158 @@
|
||||
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:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
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
|
||||
@@ -0,0 +1,75 @@
|
||||
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"
|
||||
@@ -0,0 +1,43 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
.env
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
.claude/
|
||||
target/
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/log/
|
||||
/.playwright-mcp/
|
||||
/storybook-static/
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 2,
|
||||
"printWidth": 100,
|
||||
"bracketSpacing": true,
|
||||
"arrowParens": "always"
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { StorybookConfig } from '@storybook/nextjs-vite'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
// The Vite builder: the webpack-based @storybook/nextjs framework bundled Node core
|
||||
// polyfills (crypto-browserify -> elliptic, GHSA-848j-6mx2-7j84) that no story needs.
|
||||
const config: StorybookConfig = {
|
||||
stories: ['../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
|
||||
staticDirs: ['../public'],
|
||||
addons: [
|
||||
'@storybook/addon-docs',
|
||||
],
|
||||
framework: {
|
||||
name: '@storybook/nextjs-vite',
|
||||
options: {},
|
||||
},
|
||||
viteFinal: async (config) => {
|
||||
const src = fileURLToPath(new URL('../src', import.meta.url))
|
||||
const alias = config.resolve?.alias
|
||||
config.resolve = {
|
||||
...config.resolve,
|
||||
alias: Array.isArray(alias)
|
||||
? [...alias, { find: '@', replacement: src }]
|
||||
: { ...alias, '@': src },
|
||||
}
|
||||
// Rollup drops the Next.js "use client" directives (meaningless in Storybook) and warns
|
||||
// once per client module; keep the build log to the warnings that matter.
|
||||
const onwarn = config.build?.rollupOptions?.onwarn
|
||||
config.build = {
|
||||
...config.build,
|
||||
rollupOptions: {
|
||||
...config.build?.rollupOptions,
|
||||
onwarn(warning, warn) {
|
||||
if (warning.code === 'MODULE_LEVEL_DIRECTIVE' || warning.code === 'SOURCEMAP_ERROR') return
|
||||
if (onwarn) onwarn(warning, warn)
|
||||
else warn(warning)
|
||||
},
|
||||
},
|
||||
}
|
||||
return config
|
||||
},
|
||||
}
|
||||
|
||||
export default config
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { Preview } from '@storybook/nextjs-vite'
|
||||
import '../src/styles/globals.css'
|
||||
|
||||
const preview: Preview = {
|
||||
tags: ['autodocs'],
|
||||
initialGlobals: { backgrounds: { value: 'light' } },
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
backgrounds: {
|
||||
options: {
|
||||
light: { name: 'light', value: '#ffffff' },
|
||||
dark: { name: 'dark', value: '#0f172a' },
|
||||
},
|
||||
},
|
||||
nextjs: {
|
||||
appDirectory: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default preview
|
||||
@@ -0,0 +1,201 @@
|
||||
# CLAUDE.md — QQQ Frontend Next
|
||||
|
||||
## Project Overview
|
||||
|
||||
This is the **QQQ Admin UI Modernization** project — a complete rewrite of the QQQ admin frontend from React + Material UI to Next.js + Tailwind CSS + shadcn/ui.
|
||||
|
||||
QQQ is a metadata-driven, low-code application framework. The admin UI renders entirely from backend metadata — no table names, field lists, or navigation items are hardcoded. The backend declares metadata (tables, processes, apps, widgets), and the frontend renders it dynamically.
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Framework:** Next.js 16 (App Router) with React 19
|
||||
- **Language:** TypeScript 5.x (`strict: true` — no `any` in component props)
|
||||
- **Styling:** Tailwind CSS 4.x with CSS custom properties for QQQ theme tokens
|
||||
- **Components:** shadcn/ui (Radix primitives + Tailwind)
|
||||
- **Data Grid:** TanStack Table v8 (replaces MUI X DataGrid Pro)
|
||||
- **Forms:** React Hook Form v7 + Zod validation (replaces Formik + Yup)
|
||||
- **Data Fetching:** TanStack Query v5
|
||||
- **Charts:** Recharts
|
||||
- **Testing:** Vitest (unit), Playwright (E2E), axe-core (a11y)
|
||||
- **Package Manager:** pnpm
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── app/ # Next.js App Router pages
|
||||
│ ├── (auth)/ # Auth routes (login, callback)
|
||||
│ ├── (dashboard)/ # Authenticated routes
|
||||
│ │ ├── layout.tsx # Dashboard layout (sidebar + nav + banners)
|
||||
│ │ └── app/ # All app routes
|
||||
│ │ ├── [appName]/page.tsx # App home (dashboard)
|
||||
│ │ ├── [tableName]/page.tsx # Record query
|
||||
│ │ ├── [tableName]/create/page.tsx # Create record
|
||||
│ │ ├── [tableName]/[recordId]/page.tsx # Record view
|
||||
│ │ ├── [tableName]/[recordId]/edit/page.tsx # Edit record
|
||||
│ │ └── [processName]/page.tsx # Process execution
|
||||
│ ├── layout.tsx # Root layout (providers)
|
||||
│ ├── error.tsx # Global error boundary
|
||||
│ └── not-found.tsx # 404
|
||||
├── components/
|
||||
│ ├── ui/ # shadcn/ui primitives
|
||||
│ ├── layout/ # Sidebar, Header, Breadcrumbs, Banner
|
||||
│ ├── records/ # RecordView, FieldValue
|
||||
│ ├── forms/ # DynamicForm, EntityForm, field components
|
||||
│ ├── query/ # DataGrid, FilterBuilder, ColumnConfig, Pagination
|
||||
│ ├── process/ # StepWizard, ValidationReview, BulkLoad
|
||||
│ ├── widgets/ # Widget containers, charts, blocks, grids
|
||||
│ └── feedback/ # Alerts, Toasts, Modals, CommandMenu
|
||||
├── lib/
|
||||
│ ├── api/ # Typed API client modules
|
||||
│ │ ├── client.ts # Base axios client with 401 interceptor
|
||||
│ │ ├── auth.ts # Auth endpoints
|
||||
│ │ ├── metadata.ts # Metadata endpoints
|
||||
│ │ ├── tables.ts # CRUD + query/count
|
||||
│ │ ├── processes.ts # Process lifecycle
|
||||
│ │ ├── widgets.ts # Widget data
|
||||
│ │ └── possible-values.ts
|
||||
│ ├── auth/ # Auth providers (OAuth2, Auth0, Anonymous)
|
||||
│ ├── hooks/ # Shared React hooks
|
||||
│ ├── utils/ # Pure utility functions
|
||||
│ ├── context/ # React contexts (QContext)
|
||||
│ └── theme/ # Theme provider, CSS tokens
|
||||
├── types/ # TypeScript type definitions
|
||||
└── styles/ # Global styles, theme tokens
|
||||
```
|
||||
|
||||
## Key Conventions
|
||||
|
||||
### Metadata-Driven Rendering
|
||||
- ALL components that render from metadata accept it as a prop — never fetch internally
|
||||
- No hardcoded table names, field names, or process names anywhere
|
||||
- User-facing text uses `label` from metadata, never `name`
|
||||
- UI structure derives entirely from backend metadata
|
||||
|
||||
### API Integration
|
||||
- All API calls go through typed functions in `src/lib/api/` — never raw `fetch`
|
||||
- TanStack Query manages server state (caching, deduplication, revalidation)
|
||||
- Query keys follow the factory pattern in `src/lib/query-client.ts`
|
||||
- 401 responses intercepted globally → redirect to login
|
||||
|
||||
### Component Patterns
|
||||
- Server Components: root layouts, metadata-fetching pages
|
||||
- Client Components: interactive UI (forms, grids, filters) — mark with `"use client"`
|
||||
- Every interactive component has a `data-qqq-id` attribute for CSS customization
|
||||
- Naming: `data-qqq-id="{component-type}-{identifier}"` (e.g., `data-qqq-id="button-save"`)
|
||||
|
||||
### State Management
|
||||
- Server state: TanStack Query
|
||||
- UI state: React context (QContext)
|
||||
- Form state: React Hook Form
|
||||
- URL state: Next.js searchParams (filters, pagination — shareable URLs)
|
||||
- Local persistence: localStorage (density, column widths, saved views)
|
||||
|
||||
### File Naming
|
||||
- Components: PascalCase (`RecordQuery.tsx`)
|
||||
- Hooks: camelCase with `use-` prefix (`use-metadata.ts`)
|
||||
- Utilities: kebab-case (`filter-utils.ts`)
|
||||
- Types: kebab-case (`metadata.ts`)
|
||||
- Tests: `*.test.ts(x)` co-located with source
|
||||
|
||||
### Import Order
|
||||
```typescript
|
||||
// 1. React/Next.js
|
||||
import { useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
// 2. External libraries
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
|
||||
// 3. Types
|
||||
import type { QTableMetaData } from '@/types'
|
||||
|
||||
// 4. Lib (API, hooks, utils)
|
||||
import { queryRecords } from '@/lib/api/tables'
|
||||
|
||||
// 5. Components
|
||||
import { Button } from '@/components/ui/button'
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
- Error boundaries at route level (`error.tsx`) and widget level
|
||||
- API errors via TanStack Query `onError` → toast notification
|
||||
- Form validation: Zod schemas from field metadata
|
||||
- Loading: skeleton components matching final layout
|
||||
- Empty: descriptive messages with action buttons
|
||||
|
||||
### Accessibility (WCAG 2.1 AA)
|
||||
- All form inputs: `<label>` with `htmlFor`, `aria-required`, `aria-invalid`
|
||||
- All buttons: descriptive text or `aria-label`
|
||||
- Focus trapped in modals, restored on close
|
||||
- Keyboard navigation for all interactive elements
|
||||
- Color contrast: 4.5:1 normal, 3:1 large text
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
pnpm dev # Start dev server
|
||||
pnpm build # Production build
|
||||
pnpm test # Run Vitest unit tests
|
||||
pnpm test:e2e # Run Playwright E2E tests
|
||||
pnpm tsc --noEmit # Type check without emitting
|
||||
pnpm lint # ESLint
|
||||
pnpm format # Prettier
|
||||
```
|
||||
|
||||
## Branching and Releases
|
||||
|
||||
Trunk-based: `main` is the only long-lived branch (no `develop`, `release/*` or `hotfix/*`).
|
||||
Branch from `main`, keep the branch current by merging `main` in (no rebase or force-push under
|
||||
review), sign every commit, and open the PR into `main`. Releases are signed `v*` tags on `main`.
|
||||
Required checks and the release flow are in [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
|
||||
## Implementation Plans
|
||||
|
||||
Full implementation plans are in `docs/implementation-plans/`. The project is built in 6 work packages:
|
||||
|
||||
1. **Package 1** — Scaffold, Auth, Layout Shell (foundation — must be first)
|
||||
2. **Package 2** — Record Query (data grid, filters, pagination, saved views)
|
||||
3. **Package 3** — Record View + CRUD (forms, field types, validation)
|
||||
4. **Package 4** — Process Execution (step wizard, async polling, bulk load)
|
||||
5. **Package 5** — Dashboard + Widgets (charts, statistics, blocks, grids)
|
||||
6. **Package 6** — Polish + Parity (responsive, a11y, perf, Storybook, E2E)
|
||||
|
||||
Shared context for all agents: `docs/implementation-plans/shared-context/`
|
||||
|
||||
## Reference Codebase
|
||||
|
||||
The current frontend being replaced lives in the sibling `qqq-frontend-material-dashboard` repo. Key files to reference for behavior parity:
|
||||
- `App.tsx` — route generation, auth flow, layout
|
||||
- `src/qqq/pages/records/query/RecordQuery.tsx` — most complex page (260+ state vars)
|
||||
- `src/qqq/pages/records/view/RecordView.tsx` — record detail
|
||||
- `src/qqq/pages/processes/ProcessRun.tsx` — process execution
|
||||
- `src/qqq/components/widgets/` — 60+ widget components
|
||||
|
||||
## API Base URL
|
||||
|
||||
Set via `NEXT_PUBLIC_API_BASE_URL` environment variable. Defaults to `/qqq/v1`.
|
||||
The API uses cookie-based session auth (`sessionUUID` cookie).
|
||||
|
||||
## Knowledge base
|
||||
|
||||
Durable, cross-session knowledge about this repo and the wider QQQ platform lives in
|
||||
the second-brain vault (`R:/Git.Local/KofTwentyTwo/second-brain`):
|
||||
|
||||
- Platform hub: `knowledge/qqq/qqq-hub.md`
|
||||
- This repo's dossier: `knowledge/qqq/repos/qqq-frontend-next.md`
|
||||
(reviewed at commit `41cd6d2034a8` on branch `main`, 2026-07-04)
|
||||
|
||||
Consult the dossier before deep-diving the code — it covers the API contract this
|
||||
frontend targets (including versioned-middleware endpoint gaps), maturity assessment,
|
||||
known parity gaps, and its relationship to `qqq-frontend-material-dashboard`.
|
||||
|
||||
<!-- 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 -->
|
||||
@@ -0,0 +1,51 @@
|
||||
# Contributing
|
||||
|
||||
## Branching: trunk-based on `main`
|
||||
|
||||
`main` is the only long-lived branch. There is no `develop`, `release/*` or `hotfix/*` branch.
|
||||
|
||||
1. Branch from the current `main` (`feature/...`, `fix/...`, `docs/...`, `chore/...`).
|
||||
2. Keep the branch current by merging `main` into it; do not rebase or force-push a branch
|
||||
that is under review.
|
||||
3. Open a pull request into `main`. Every commit must be signed.
|
||||
4. A pull request merges when the required checks pass and the code owner approves.
|
||||
Head branches are deleted automatically after the merge.
|
||||
|
||||
### Required checks
|
||||
|
||||
| Check | Source |
|
||||
|-------|--------|
|
||||
| `Typecheck, lint, unit tests and coverage` | `.github/workflows/ci.yml` |
|
||||
| `Mocked e2e (Playwright, chromium)` | `.github/workflows/ci.yml` |
|
||||
| `Bundle budget (static export)` | `.github/workflows/ci.yml` |
|
||||
| `Storybook build` | `.github/workflows/ci.yml` |
|
||||
| `ci/circleci: qqq-orb/node_app_test_only` | `.circleci/config.yml` |
|
||||
|
||||
The real-backend acceptance matrix (`.github/workflows/acceptance.yml`) runs on every pull request
|
||||
and on `main`. Treat a red row as a release blocker and investigate it; never re-run it until it passes.
|
||||
|
||||
## Releases
|
||||
|
||||
A release is a signed `v<version>` tag on `main` (for example `v1.0.0-RC.14`), where `package.json`
|
||||
and `pom.xml` carry that version. The tag publishes the dashboard jar to Maven Central (CircleCI
|
||||
`publish_dashboard_jar`) and the dashboard container image (`.github/workflows/dashboard-image.yml`).
|
||||
Release notes live in `docs/releases/`.
|
||||
|
||||
## Dependencies
|
||||
|
||||
Dependabot opens grouped weekly updates for npm, GitHub Actions and Maven (`.github/dependabot.yml`),
|
||||
plus security updates as they are published.
|
||||
|
||||
## UI API calls (merge order)
|
||||
Next calls the backend only through operations described in the vendored UI API documents:
|
||||
`src/lib/api/openapi/qqq-v1.json`, plus one JSON file per extension in `extensions/`.
|
||||
`ui-api-contract.test.ts` fails on any other call.
|
||||
|
||||
A feature that needs new backend calls lands in this order:
|
||||
1. The qbit or module ships the operations as a UI API extension, with spec classes and its
|
||||
committed `openapi/qqq-v1/<name>.yaml`.
|
||||
2. That document is vendored here, as `extensions/<name>.json` or a re-vendored `qqq-v1.json`.
|
||||
3. The UI PR that calls them.
|
||||
|
||||
`OFF_SPEC_CALLS` in `ui-api-calls.ts` is a shrinking list of calls that predate the extension point.
|
||||
`OFF_SPEC_CEILING` caps it: lower the ceiling as entries move onto specs, and never raise it.
|
||||
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
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.
|
||||
@@ -1,3 +1,180 @@
|
||||
# PR assets: SQL Workbench theme
|
||||
# QQQ Frontend Next
|
||||
|
||||
Before (main) and after screenshots of the SQL Workbench, light and dark, desktop and phone (mocked API, production build).
|
||||
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.
|
||||
|
||||
**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).
|
||||
|
||||
## Use it in a QQQ application
|
||||
|
||||
The dashboard is the default admin UI for QQQ 4.1+. Add the jar (managed by `qqq-bom-pom`) and `QApplicationJavalinServer` serves it at `/`, on the same port and origin as the API:
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>com.kingsrook.qqq</groupId>
|
||||
<artifactId>qqq-frontend-next</artifactId>
|
||||
<version>1.0.0-RC.21</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 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>`.
|
||||
|
||||
Feature coverage is recorded in the real-backend acceptance matrix in [`docs/acceptance/feature-matrix.md`](docs/acceptance/feature-matrix.md) ([QRun-IO/qqq#649](https://github.com/QRun-IO/qqq/issues/649)). Run it with `pnpm test:acceptance` (see [`tests/acceptance/README.md`](tests/acceptance/README.md)).
|
||||
|
||||
## Adopting in another app
|
||||
|
||||
How an existing QQQ backend application (for example one still on QQQ 0.x with the Material Dashboard) switches to, or upgrades, the Next dashboard. The frontend is distributed only as a Maven jar; there is no npm package (`package.json` is `private`).
|
||||
|
||||
### Coordinates and compatible versions
|
||||
|
||||
| Piece | Coordinates | Version |
|
||||
|---|---|---|
|
||||
| Next dashboard | `com.kingsrook.qqq:qqq-frontend-next` | `1.0.0-RC.13` (latest; [all versions](https://repo1.maven.org/maven2/com/kingsrook/qqq/qqq-frontend-next/)) |
|
||||
| QQQ BOM | `com.kingsrook.qqq:qqq-bom-pom` (import scope) | `4.1.0-RC.3` (manages `qqq-frontend-next` at `1.0.0-RC.11`, so pin `1.0.0-RC.13` explicitly) |
|
||||
| SQL Workbench QBit (optional) | `com.kingsrook.qbits:qbit-sql-workbench` | `1.0.0-RC.1` (built against QQQ `4.1.0-RC.2`) |
|
||||
| Container image (optional, separate Node server) | `ghcr.io/qrun-io/qqq-frontend-next` | `1.0.0-RC.13` |
|
||||
|
||||
| Next feature | Minimum QQQ | Backend work |
|
||||
|---|---|---|
|
||||
| Dashboard at `/` | 4.1.0-RC.1 | Add the jar; `QApplicationJavalinServer` (`qqq-middleware-javalin`) serves it. |
|
||||
| Standalone branding images | 4.1.0-RC.2 | Branding metadata ([qqq#1009](https://github.com/QRun-IO/qqq/issues/1009)). |
|
||||
| FILE_PREVIEW thumbnails and lightbox | 4.1.0-RC.3 | Adorn each field with `AdornmentType.FilePreviewValues.newFieldAdornment()` ([qqq#1056](https://github.com/QRun-IO/qqq/issues/1056)). The field value, or `thumbnailUrlField` / `fullSizeUrlField`, must be an http(s) URL or a same-origin path; a BLOB field needs a `FILE_DOWNLOAD` adornment as well. Plain URL fields are never auto-detected. |
|
||||
| Instance home app | 4.1.0-RC.3 | `qInstance.setHomeAppName("<app>")` ([qqq#1059](https://github.com/QRun-IO/qqq/issues/1059)). Users who may not open that app get Next's `/app` overview. |
|
||||
| SQL Workbench | 4.1.0-RC.2 + QBit | Produce the QBit (`new SqlWorkbenchQBitProducer().withQBitConfig(...).produce(qInstance)`); it adds the `sqlWorkbench` app with the `SQL_WORKBENCH` widget, five tables and `/qqq/v1/sql-workbench/*` routes. Grant `sqlWorkbench.hasAccess` (and `sqlWorkbench.write` only for write mode) to super-users; configure `readOnlyCredentials` in production. See the [QBit README](https://github.com/QRun-IO/qbit-sql-workbench#readme). |
|
||||
| Responsive layouts, readable child lists, compact dates | 4.1.0-RC.1 | None; frontend only. Child-record widgets honour `onlyIncludeFieldNames` order. |
|
||||
|
||||
### Switching from the Material Dashboard
|
||||
|
||||
1. Move the application to QQQ 4.1 (`qqq-bom-pom` `4.1.0-RC.3`). This is a major upgrade from QQQ 0.x; follow the QQQ 4.1 upgrade notes for the backend itself.
|
||||
2. Add the `qqq-frontend-next` dependency above. When the jar is on the classpath, QQQ serves Next at `/` unless the Material Dashboard is requested explicitly.
|
||||
3. Choose the dashboard in code with `withServeFrontendNext(true)` / `withServeFrontendMaterialDashboard(true)`, or, when neither is set in code, with `-Dqqq.javalin.frontend=next|material|none`. An app that sets `withServeFrontendMaterialDashboard(false)` and serves Material through its own route provider gets Next at `/` as soon as the jar is added; remove that custom route, or set `withServeFrontendNext(false)` to keep Material.
|
||||
4. Authentication: keep the existing identity-provider client. OAUTH2 redirects to `{origin}/token` and AUTH_0 to `{origin}/`, as Material did; both use PKCE.
|
||||
5. URLs change from `/<app>/<table>/<id>` (Material) to `/app/<table>/<id>` (Next). Map old bookmarks and links with `QInstance.redirects` (exact or wildcard paths).
|
||||
6. Optional: set `homeAppName`, add `FILE_PREVIEW` adornments, add the SQL Workbench QBit (table above). No database migrations are needed for the dashboard itself; the SQL Workbench QBit needs its five tables (DDL in its README).
|
||||
|
||||
For a full per-feature checklist (backend upgrade, QBits, migrations, permissions) see the QQQ adoption checklist: https://github.com/QRun-IO/qqq/blob/develop/docs/migration/adoption-checklist.adoc
|
||||
|
||||
To upgrade between RCs, change only the `qqq-frontend-next` version and check the minimum QQQ version in the table above and in the [release notes](docs/releases/).
|
||||
|
||||
### Configuration
|
||||
|
||||
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.
|
||||
|
||||
### 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)).
|
||||
- Served at `/` only: sub-path hosting is not supported ([#732](https://github.com/QRun-IO/qqq/issues/732)).
|
||||
- Material parity is not complete ([#714](https://github.com/QRun-IO/qqq/issues/714)); check the parity ledger for the screens your app relies on.
|
||||
- Real Auth0, Google Drive and QuickSight integrations are not yet verified end to end ([#713](https://github.com/QRun-IO/qqq/issues/713)).
|
||||
- The container image fixes the backend origin at build time (`host.docker.internal:8000`); use the jar, or rebuild the image, for another origin.
|
||||
- WebKit and tablet acceptance runs show intermittent navigation diagnostics ([#985](https://github.com/QRun-IO/qqq/issues/985), [#904](https://github.com/QRun-IO/qqq/issues/904)).
|
||||
|
||||
## Run the local QQQ sample
|
||||
|
||||
Use the [QQQ quickstart](https://github.com/QRun-IO/qqq/blob/main/qqq-sample-project/README.md#quickstart-with-next): it clones editable Java sample source, compiles it against published QQQ, and serves this dashboard from the sample on port 8000. It needs JDK 21+, Git, curl and unzip; no Node.js, Maven or Docker installation.
|
||||
|
||||
A container image (`ghcr.io/qrun-io/qqq-frontend-next`, built from `docker/quickstart/Dockerfile`) remains available for running the dashboard as a separate Node server in front of a backend on host port 8000. The backend origin is fixed at image build time (`QQQ_BACKEND_URL`).
|
||||
|
||||
## Frontend development prerequisites
|
||||
|
||||
- **Node.js** 20.19+ or 22.12+ (required by the Storybook developer tooling)
|
||||
- **pnpm** (install with `npm install -g pnpm` if not already present)
|
||||
|
||||
## Frontend development
|
||||
|
||||
```bash
|
||||
git clone https://github.com/QRun-IO/qqq-frontend-next.git
|
||||
cd qqq-frontend-next
|
||||
pnpm install
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
The dev server starts at [http://localhost:3000](http://localhost:3000).
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
|---|---|---|
|
||||
| `NEXT_PUBLIC_API_BASE_URL` | `/qqq/v1` | Base URL for all QQQ backend API calls |
|
||||
| `QQQ_BACKEND_URL` | _(unset)_ | Standalone build only: backend origin for same-origin API rewrites; the container image uses `http://host.docker.internal:8000` |
|
||||
| `QQQ_NEXT_OUTPUT` | _(unset)_ | `export` produces the static export for QQQ hosting (`pnpm build:export`) |
|
||||
| `NEXT_PUBLIC_MOCK_API` | _(unset)_ | Set to `true` to activate MSW browser mocks for dev/demo without a live backend |
|
||||
|
||||
Copy `.env.mock` to `.env.local` to run against MSW mocks, or set `NEXT_PUBLIC_MOCK_API=false` and point `NEXT_PUBLIC_API_BASE_URL` at a real backend.
|
||||
|
||||
## Available Commands
|
||||
|
||||
| Command | Description |
|
||||
|---|---|
|
||||
| `pnpm dev` | Start the Next.js development server (Turbopack) |
|
||||
| `pnpm build` | Compile the standalone (Node server) production build |
|
||||
| `pnpm build:export` | Compile the static export served by QQQ (`out/`) |
|
||||
| `pnpm build:jar` | Static export packaged as the `qqq-frontend-next` Maven jar |
|
||||
| `pnpm test:acceptance` | Real-backend acceptance suite and matrix gate (needs `QQQ_SAMPLE_JAR`) |
|
||||
| `pnpm start` | Serve the production build |
|
||||
| `pnpm test` | Run Vitest unit tests (single pass) |
|
||||
| `pnpm test:watch` | Run Vitest in watch mode |
|
||||
| `pnpm test:coverage` | Run Vitest with V8 coverage; fails below the `src/lib` thresholds in `vitest.config.ts` |
|
||||
| `pnpm test:e2e` | Run the mocked Playwright e2e suite (`tests/e2e`, no backend); `QQQ_E2E_SERVER=production` serves the `pnpm build` output, `QQQ_E2E_PORT` picks the port |
|
||||
| `pnpm perf:budget` | Check the static export (`out/`) against the bundle budget in `perf-budget.json` ([`docs/acceptance/performance.md`](docs/acceptance/performance.md)) |
|
||||
| `pnpm typecheck` | Run `tsc --noEmit` (type check without emitting files) |
|
||||
| `pnpm lint` | Run ESLint + Apache license header check |
|
||||
| `pnpm format` | Run Prettier over all source files |
|
||||
| `pnpm storybook` | Start Storybook component explorer on port 6006 |
|
||||
| `pnpm build-storybook` | Build the static Storybook (`storybook-static/`) |
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── app/ # Next.js App Router pages
|
||||
│ ├── (auth)/ # Login and OAuth callback routes
|
||||
│ ├── (dashboard)/ # Authenticated shell (sidebar + nav)
|
||||
│ │ └── app/ # All app routes (tables, records, processes)
|
||||
│ ├── layout.tsx # Root layout — providers
|
||||
│ ├── error.tsx # Global error boundary
|
||||
│ └── not-found.tsx # 404 page
|
||||
├── components/
|
||||
│ ├── ui/ # shadcn/ui primitives (Button, Input, Dialog, …)
|
||||
│ ├── layout/ # Sidebar, Header, Breadcrumbs, Banner
|
||||
│ ├── records/ # RecordView, FieldValue
|
||||
│ ├── forms/ # DynamicForm, EntityForm, field components
|
||||
│ ├── query/ # DataGrid, FilterBuilder, ColumnConfig, Pagination
|
||||
│ ├── process/ # StepWizard, ValidationReview, BulkLoad
|
||||
│ ├── widgets/ # Widget containers, charts, stat blocks, grids
|
||||
│ └── feedback/ # Alerts, Toasts, Modals, CommandMenu
|
||||
├── lib/
|
||||
│ ├── api/ # Typed API client modules (client.ts, tables.ts, …)
|
||||
│ ├── auth/ # Auth providers (OAuth2, Auth0, Anonymous)
|
||||
│ ├── hooks/ # Shared React hooks (useLocalStorage, useMetadata, …)
|
||||
│ ├── utils/ # Pure utility functions
|
||||
│ ├── context/ # React contexts (QContext)
|
||||
│ └── theme/ # Theme provider and CSS custom property tokens
|
||||
├── types/ # TypeScript type definitions
|
||||
└── styles/ # Global styles and theme tokens
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
| Document | Purpose |
|
||||
|---|---|
|
||||
| [`CLAUDE.md`](./CLAUDE.md) | Full architecture reference — conventions, patterns, key decisions |
|
||||
| [`docs/STATE-MANAGEMENT.md`](./docs/STATE-MANAGEMENT.md) | When to use each state container (TanStack Query, RHF, URL params, localStorage, QContext) |
|
||||
| [`docs/API-ERROR-HANDLING.md`](./docs/API-ERROR-HANDLING.md) | Global error handler, 401 interceptor, per-status guidance, code examples |
|
||||
| [`docs/implementation-plans/`](./docs/implementation-plans/) | Work-package implementation plans (6 packages) |
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Framework:** Next.js 16 (App Router) with React 19
|
||||
- **Language:** TypeScript 5.x (`strict: true`)
|
||||
- **Styling:** Tailwind CSS 4.x with CSS custom properties
|
||||
- **Components:** shadcn/ui (Radix primitives + Tailwind)
|
||||
- **Data Grid:** TanStack Table v8
|
||||
- **Forms:** React Hook Form v7 + Zod
|
||||
- **Data Fetching:** TanStack Query v5
|
||||
- **Charts:** Recharts
|
||||
- **Testing:** Vitest (unit), Playwright (E2E), axe-core (a11y)
|
||||
- **Package Manager:** pnpm
|
||||
|
Before Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 121 KiB |
|
Before Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 139 KiB |
|
Before Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 95 KiB |
@@ -0,0 +1,25 @@
|
||||
FROM registry-1.docker.io/library/node:22-bookworm-slim AS frontend
|
||||
WORKDIR /app
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
RUN corepack enable && corepack prepare pnpm@9.15.9 --activate
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
RUN pnpm install --frozen-lockfile
|
||||
COPY . .
|
||||
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
|
||||
LABEL org.opencontainers.image.source="https://github.com/QRun-IO/qqq-frontend-next"
|
||||
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 ./
|
||||
COPY --from=frontend --chown=node:node /app/.next/static ./.next/static
|
||||
COPY --from=frontend --chown=node:node /app/public ./public
|
||||
# server.js wrapped with the dashboard security headers (QRun-IO/qqq#734); add origins the
|
||||
# dashboard must reach (QuickSight, custom component bundles) with QQQ_DASHBOARD_CSP_SOURCES
|
||||
COPY --from=frontend --chown=node:node /app/standalone/qqq-server.mjs /app/standalone/security-headers.mjs /app/standalone/branding-assets.mjs /app/standalone/backend-prefixes.json ./
|
||||
COPY LICENSE ./LICENSE
|
||||
USER node
|
||||
EXPOSE 3000
|
||||
CMD ["node", "qqq-server.mjs"]
|
||||
@@ -0,0 +1,293 @@
|
||||
# Master Agent Execution Plan — QQQ Frontend Next
|
||||
|
||||
## Your Role
|
||||
|
||||
You are the **Master Orchestrator Agent**. Your job is to build the entire QQQ admin UI replacement by spawning and controlling implementation sub-agents, one per work package, in the correct dependency order. You delegate all coding work to sub-agents. You verify their output, handle failures, and ensure the full project compiles and integrates at the end.
|
||||
|
||||
## Repository
|
||||
|
||||
You are working in `qqq-frontend-next` — a new repo for the Next.js replacement of the QQQ admin UI.
|
||||
|
||||
### Critical Files in This Repo
|
||||
|
||||
```
|
||||
docs/
|
||||
├── qqq-admin-modernization-requirements.md # Full requirements spec
|
||||
├── master-architect-agent-prompt.md # How the plans were designed
|
||||
├── 00-master-execution-plan.md # THIS FILE — your instructions
|
||||
└── implementation-plans/
|
||||
├── 00-project-overview.md # Tech stack, directory structure, conventions
|
||||
├── 01-scaffold-and-auth.md # Work Package 1 plan
|
||||
├── 02-record-query.md # Work Package 2 plan
|
||||
├── 03-record-view-and-crud.md # Work Package 3 plan
|
||||
├── 04-process-execution.md # Work Package 4 plan
|
||||
├── 05-dashboard-and-widgets.md # Work Package 5 plan
|
||||
├── 06-polish-and-parity.md # Work Package 6 plan
|
||||
├── cross-check-report.md # Requirements coverage verification
|
||||
├── shared-context/
|
||||
│ ├── api-contract.md # API spec — give to every agent
|
||||
│ ├── type-definitions.md # TypeScript types — give to every agent
|
||||
│ └── coding-conventions.md # Coding standards — give to every agent
|
||||
└── review-checklists/
|
||||
├── review-01-scaffold.md through review-06-polish.md
|
||||
```
|
||||
|
||||
### Reference Repos (Read-Only)
|
||||
|
||||
If accessible, the agents should also reference:
|
||||
- `qqq-frontend-material-dashboard` — The current UI being replaced (behavior parity)
|
||||
- `qqq-frontend-core` — TypeScript model types and API client (types to port)
|
||||
- `qqq` — Backend middleware (API endpoint source of truth)
|
||||
|
||||
---
|
||||
|
||||
## Execution Phases
|
||||
|
||||
### Phase 1: Foundation (BLOCKING — must complete before any other phase)
|
||||
|
||||
**Spawn Agent 1: Package 1 — Scaffold + Auth + Layout Shell**
|
||||
|
||||
Instruct the agent:
|
||||
> Read these files before writing any code:
|
||||
> - `docs/implementation-plans/01-scaffold-and-auth.md` (your implementation plan)
|
||||
> - `docs/implementation-plans/shared-context/api-contract.md`
|
||||
> - `docs/implementation-plans/shared-context/type-definitions.md`
|
||||
> - `docs/implementation-plans/shared-context/coding-conventions.md`
|
||||
> - `docs/implementation-plans/00-project-overview.md`
|
||||
> - `CLAUDE.md`
|
||||
>
|
||||
> Also read the reference implementations in `qqq-frontend-material-dashboard`:
|
||||
> - `src/App.tsx` — Route generation, auth dispatch, layout
|
||||
> - `src/qqq/components/horseshoe/` — SideNav, NavBar, Breadcrumbs
|
||||
>
|
||||
> And from `qqq-frontend-core`:
|
||||
> - `src/model/**/*.ts` — All TypeScript types to port
|
||||
> - `src/controllers/QController.ts` — Auth methods
|
||||
>
|
||||
> Execute every step in the implementation plan (Steps 1-11).
|
||||
> After each step, verify: `pnpm tsc --noEmit` passes.
|
||||
> When done, verify ALL acceptance criteria in Section 9.
|
||||
> Commit your work.
|
||||
|
||||
**Verification gate before proceeding:**
|
||||
```bash
|
||||
pnpm dev # Must start without errors
|
||||
pnpm build # Must succeed
|
||||
pnpm tsc --noEmit # Zero errors
|
||||
pnpm test # All tests pass
|
||||
```
|
||||
|
||||
If the agent fails, fix the issues before moving to Phase 2.
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: Core Pages (CAN BE PARALLEL — no file conflicts)
|
||||
|
||||
**Spawn Agent 2: Package 2 — Record Query**
|
||||
|
||||
Instruct the agent:
|
||||
> Read these files before writing any code:
|
||||
> - `docs/implementation-plans/02-record-query.md` (your implementation plan)
|
||||
> - `docs/implementation-plans/shared-context/api-contract.md`
|
||||
> - `docs/implementation-plans/shared-context/type-definitions.md`
|
||||
> - `docs/implementation-plans/shared-context/coding-conventions.md`
|
||||
> - `docs/implementation-plans/00-project-overview.md`
|
||||
> - `CLAUDE.md`
|
||||
>
|
||||
> Also read the reference implementation:
|
||||
> - `qqq-frontend-material-dashboard/src/qqq/pages/records/query/RecordQuery.tsx`
|
||||
> - `qqq-frontend-material-dashboard/src/qqq/components/query/` (all files)
|
||||
> - `qqq-frontend-core/src/controllers/QControllerV1.ts` (query/count methods)
|
||||
>
|
||||
> Execute every step in the implementation plan.
|
||||
> The foundation from Package 1 is already in place — import from it.
|
||||
> After each step: `pnpm tsc --noEmit`
|
||||
> When done: verify ALL acceptance criteria in Section 9.
|
||||
> Commit your work.
|
||||
|
||||
**Spawn Agent 3: Package 3 — Record View + CRUD**
|
||||
|
||||
Instruct the agent:
|
||||
> Read these files before writing any code:
|
||||
> - `docs/implementation-plans/03-record-view-and-crud.md` (your implementation plan)
|
||||
> - `docs/implementation-plans/shared-context/api-contract.md`
|
||||
> - `docs/implementation-plans/shared-context/type-definitions.md`
|
||||
> - `docs/implementation-plans/shared-context/coding-conventions.md`
|
||||
> - `docs/implementation-plans/00-project-overview.md`
|
||||
> - `CLAUDE.md`
|
||||
>
|
||||
> Also read the reference implementation:
|
||||
> - `qqq-frontend-material-dashboard/src/qqq/pages/records/view/RecordView.tsx`
|
||||
> - `qqq-frontend-material-dashboard/src/qqq/components/forms/` (all files)
|
||||
> - `qqq-frontend-material-dashboard/src/qqq/components/forms/EntityForm.tsx`
|
||||
> - `qqq-frontend-material-dashboard/src/qqq/components/forms/DynamicForm.tsx`
|
||||
> - `qqq-frontend-core/src/controllers/QController.ts` (CRUD methods: get, create, update, delete)
|
||||
>
|
||||
> Execute every step in the implementation plan.
|
||||
> The foundation from Package 1 is already in place — import from it.
|
||||
> After each step: `pnpm tsc --noEmit`
|
||||
> When done: verify ALL acceptance criteria in Section 9.
|
||||
> Commit your work.
|
||||
|
||||
**Verification gate before proceeding:**
|
||||
Both agents must complete. Then:
|
||||
```bash
|
||||
pnpm tsc --noEmit # Zero errors (both packages together)
|
||||
pnpm test # All tests pass
|
||||
pnpm build # Must succeed
|
||||
```
|
||||
|
||||
If running in parallel on branches, merge both to main and resolve conflicts before Phase 3.
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: Process Execution (SEQUENTIAL — needs forms from Package 3)
|
||||
|
||||
**Spawn Agent 4: Package 4 — Process Execution**
|
||||
|
||||
Instruct the agent:
|
||||
> Read these files before writing any code:
|
||||
> - `docs/implementation-plans/04-process-execution.md` (your implementation plan)
|
||||
> - `docs/implementation-plans/shared-context/api-contract.md`
|
||||
> - `docs/implementation-plans/shared-context/type-definitions.md`
|
||||
> - `docs/implementation-plans/shared-context/coding-conventions.md`
|
||||
> - `CLAUDE.md`
|
||||
>
|
||||
> Also read the reference implementation:
|
||||
> - `qqq-frontend-material-dashboard/src/qqq/pages/processes/ProcessRun.tsx`
|
||||
> - `qqq-frontend-material-dashboard/src/qqq/components/processes/` (all files)
|
||||
> - `qqq-frontend-core/src/controllers/QController.ts` (process methods)
|
||||
>
|
||||
> IMPORTANT: You must reuse form components from Package 3:
|
||||
> - Import `EntityForm` from `@/components/forms/EntityForm`
|
||||
> - Import `DynamicFormField` from `@/components/forms/DynamicFormField`
|
||||
> - Import form hooks and possible-values utilities
|
||||
>
|
||||
> Execute every step. After each: `pnpm tsc --noEmit`.
|
||||
> When done: verify ALL acceptance criteria.
|
||||
> Commit your work.
|
||||
|
||||
**Verification gate:**
|
||||
```bash
|
||||
pnpm tsc --noEmit && pnpm test && pnpm build
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Phase 4: Dashboard + Widgets (needs DataGrid from P2, forms from P3)
|
||||
|
||||
**Spawn Agent 5: Package 5 — Dashboard and Widgets**
|
||||
|
||||
Instruct the agent:
|
||||
> Read these files before writing any code:
|
||||
> - `docs/implementation-plans/05-dashboard-and-widgets.md` (your implementation plan)
|
||||
> - `docs/implementation-plans/shared-context/api-contract.md`
|
||||
> - `docs/implementation-plans/shared-context/type-definitions.md`
|
||||
> - `docs/implementation-plans/shared-context/coding-conventions.md`
|
||||
> - `CLAUDE.md`
|
||||
>
|
||||
> Also read the reference implementation:
|
||||
> - `qqq-frontend-material-dashboard/src/qqq/components/widgets/` (all files — 60+)
|
||||
> - `qqq-frontend-material-dashboard/src/qqq/pages/apps/AppHome.tsx`
|
||||
>
|
||||
> IMPORTANT: Reuse components from previous packages:
|
||||
> - DataGrid from Package 2 (`@/components/query/DataGrid`) for RecordGridWidget
|
||||
> - EntityForm from Package 3 (`@/components/forms/EntityForm`) for DynamicFormWidget
|
||||
> - Cell renderers from Package 2 for field value display in widgets
|
||||
>
|
||||
> Execute every step. After each: `pnpm tsc --noEmit`.
|
||||
> When done: verify ALL acceptance criteria.
|
||||
> Commit your work.
|
||||
|
||||
**Verification gate:**
|
||||
```bash
|
||||
pnpm tsc --noEmit && pnpm test && pnpm build
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Phase 5: Polish + Parity (needs EVERYTHING)
|
||||
|
||||
**Spawn Agent 6: Package 6 — Polish, Parity, and Production Readiness**
|
||||
|
||||
Instruct the agent:
|
||||
> Read these files before writing any code:
|
||||
> - `docs/implementation-plans/06-polish-and-parity.md` (your implementation plan)
|
||||
> - `docs/implementation-plans/shared-context/coding-conventions.md`
|
||||
> - `CLAUDE.md`
|
||||
>
|
||||
> Also read the reference implementation for features unique to this package:
|
||||
> - `qqq-frontend-material-dashboard/src/qqq/components/misc/CommandMenu.tsx`
|
||||
> - `qqq-frontend-material-dashboard/src/qqq/components/audits/AuditBody.tsx`
|
||||
> - `qqq-frontend-material-dashboard/src/qqq/pages/records/developer/`
|
||||
> - `qqq-frontend-material-dashboard/src/qqq/components/scripts/ScriptViewer.tsx`
|
||||
>
|
||||
> This is the final package. ALL previous packages are complete.
|
||||
> Your job is: responsive audit, accessibility fixes, command palette, keyboard shortcuts,
|
||||
> audit trail, developer tools, Storybook, Playwright E2E, performance optimization,
|
||||
> theme tokens, and customCss injection.
|
||||
>
|
||||
> Execute every step. After each: `pnpm tsc --noEmit`.
|
||||
> When done: verify ALL acceptance criteria.
|
||||
> Run: `pnpm build && pnpm test && pnpm lint`
|
||||
> Commit your work.
|
||||
|
||||
---
|
||||
|
||||
## Final Verification (Master Agent Does This)
|
||||
|
||||
After all 6 agents complete:
|
||||
|
||||
1. **Full build check:**
|
||||
```bash
|
||||
pnpm tsc --noEmit # Zero errors
|
||||
pnpm build # Production build succeeds
|
||||
pnpm test # All unit tests pass
|
||||
pnpm lint # Zero lint errors
|
||||
```
|
||||
|
||||
2. **Review checklists:** Read and verify each file in `docs/implementation-plans/review-checklists/`:
|
||||
- `review-01-scaffold.md` through `review-06-polish.md`
|
||||
- Every checkbox should be satisfiable
|
||||
|
||||
3. **Cross-check report:** Read `docs/implementation-plans/cross-check-report.md` — all requirements should show ✅
|
||||
|
||||
4. **Integration smoke test:** If a backend is available, verify the app end-to-end:
|
||||
- Login → sidebar loads → click table → query page loads → filter → paginate → click row → record view → edit → save → delete → navigate to process → run process → dashboard loads widgets
|
||||
|
||||
5. **Commit everything and push:**
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "Complete QQQ Admin UI Modernization — all 6 work packages implemented"
|
||||
git push origin main
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Recovery
|
||||
|
||||
If an agent fails mid-execution:
|
||||
1. Read its error output
|
||||
2. Fix the immediate issue (missing import, type error, etc.)
|
||||
3. Resume the agent from where it left off (it should be able to continue from the last successful step)
|
||||
4. If the failure is architectural (wrong approach), read the relevant section of the implementation plan and correct course
|
||||
|
||||
If agents produce conflicting code (e.g., both define the same component):
|
||||
1. The implementation plans are designed to avoid this — each package owns specific files
|
||||
2. If it happens, the later package's version wins (it has more context)
|
||||
3. Verify the merged result compiles: `pnpm tsc --noEmit`
|
||||
|
||||
---
|
||||
|
||||
## Key Constraints for ALL Agents
|
||||
|
||||
- **pnpm only** (not npm, not yarn)
|
||||
- **TypeScript strict** — no `any` in component props
|
||||
- **Every interactive element** gets `data-qqq-id` attribute
|
||||
- **All API calls** through `src/lib/api/` — never raw fetch/axios
|
||||
- **Metadata-driven** — no hardcoded table/field/process names
|
||||
- **Labels not names** — user-facing text uses `label` from metadata
|
||||
- **Import order** — React/Next → external libs → types → lib → components
|
||||
- **Error boundaries** at route level and widget level
|
||||
- **Loading skeletons** for all async content
|
||||
- **Empty states** with action buttons for all list/grid views
|
||||
@@ -0,0 +1,338 @@
|
||||
# Accessibility Reference — QQQ Frontend Next
|
||||
|
||||
**Standard:** WCAG 2.1 Level AA
|
||||
**Testing tools:** axe-core (automated), Playwright E2E assertions, manual keyboard testing
|
||||
|
||||
---
|
||||
|
||||
## WCAG 2.1 AA Compliance Status
|
||||
|
||||
The sections below summarise what has been implemented and what remains outstanding,
|
||||
based on the design review conducted 2026-03-01 (`docs/DESIGN-REVIEW-2026-03-01.md`).
|
||||
|
||||
---
|
||||
|
||||
## Key Implementations (Resolved Items)
|
||||
|
||||
### Skip-to-Content Link (D-L-1)
|
||||
|
||||
A visually hidden skip link is rendered as the first element in the dashboard layout
|
||||
(`src/app/(dashboard)/layout.tsx`). It becomes visible on keyboard focus and links to
|
||||
`#main-content`, allowing keyboard users to bypass the sidebar on every page navigation.
|
||||
|
||||
```tsx
|
||||
<a
|
||||
href="#main-content"
|
||||
className="sr-only focus:not-sr-only focus:fixed focus:left-4 focus:top-4 focus:z-50 ..."
|
||||
data-qqq-id="skip-to-content"
|
||||
>
|
||||
Skip to main content
|
||||
</a>
|
||||
```
|
||||
|
||||
The target `<main id="main-content" data-qqq-id="main-content">` is present in the same layout.
|
||||
|
||||
---
|
||||
|
||||
### `aria-sort` on DataGrid Column Headers (D-Q-1)
|
||||
|
||||
`src/components/query/DataGrid.tsx` computes an `aria-sort` value for every `<th>` based on
|
||||
the current TanStack Table sort state:
|
||||
|
||||
- Sorted ascending → `aria-sort="ascending"`
|
||||
- Sorted descending → `aria-sort="descending"`
|
||||
- Not sorted → `aria-sort="none"` (only on sortable columns; unsortable columns omit the attribute)
|
||||
|
||||
Screen readers announce the sort direction when the user focuses or activates a column header.
|
||||
|
||||
---
|
||||
|
||||
### Row Selection `aria-label` (D-Q-2)
|
||||
|
||||
Each row's selection checkbox carries a descriptive label:
|
||||
|
||||
```tsx
|
||||
aria-label={`Select ${row.original.recordLabel ?? 'record'}`}
|
||||
```
|
||||
|
||||
The "select all" header checkbox uses `aria-label="Select all rows on this page"`.
|
||||
|
||||
---
|
||||
|
||||
### Keyboard-Accessible Column Resize Handles (D-Q-3)
|
||||
|
||||
The resize handle on each column header responds to `ArrowLeft` and `ArrowRight` keyboard
|
||||
events, adjusting the column width by ±10 px per keypress. The handler is implemented in
|
||||
`handleResizeKeyDown` inside `DataGrid.tsx`:
|
||||
|
||||
```typescript
|
||||
if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') return
|
||||
const delta = e.key === 'ArrowRight' ? 10 : -10
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `aria-live` Region for Fetch-in-Progress (D-Q-6)
|
||||
|
||||
The DataGrid wraps the loading progress bar and result count in an `aria-live="polite"` region
|
||||
so that background re-fetches triggered by sort or filter changes are announced to screen readers
|
||||
without interrupting the current reading flow.
|
||||
|
||||
---
|
||||
|
||||
### Focus Management in ProcessRun (D-P-1)
|
||||
|
||||
`src/components/process/ProcessRun.tsx` uses a `stepHeadingRef` and a `useEffect` keyed on the
|
||||
active step name to move focus to the step heading whenever the wizard advances or retreats.
|
||||
This satisfies WCAG 2.4.3 (Focus Order) by ensuring screen reader users are positioned at the
|
||||
start of each new step:
|
||||
|
||||
```typescript
|
||||
const stepHeadingRef = useRef<HTMLHeadingElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (hasRenderedFirstStep.current) {
|
||||
stepHeadingRef.current?.focus()
|
||||
}
|
||||
hasRenderedFirstStep.current = true
|
||||
}, [activeStepName])
|
||||
```
|
||||
|
||||
The target `<h2>` carries `tabIndex={-1}` and is referenced by `ref={stepHeadingRef}`.
|
||||
|
||||
---
|
||||
|
||||
### Screen-Reader Text for Trend Indicators (D-W-1)
|
||||
|
||||
`src/components/widgets/StatisticsWidget.tsx` renders a visually hidden `<span>` next to each
|
||||
trend arrow icon so the direction is announced by screen readers:
|
||||
|
||||
```tsx
|
||||
<span className="sr-only">{config.srLabel}</span>
|
||||
// srLabel values: 'trending up,' | 'trending down,' | 'no change,'
|
||||
```
|
||||
|
||||
The icon itself is `aria-hidden="true"` so it is not double-announced.
|
||||
|
||||
---
|
||||
|
||||
### Submit Button Disabled While Submitting (D-F-1)
|
||||
|
||||
`src/components/forms/EntityForm.tsx` reads `isSubmitting` from React Hook Form's `formState`
|
||||
and applies it to all action buttons, preventing duplicate submissions:
|
||||
|
||||
```tsx
|
||||
disabled={disabled || isSubmitting}
|
||||
// Label also changes: isSubmitting ? 'Saving...' : saveButtonLabel
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Baseline Form Accessibility
|
||||
|
||||
All form inputs across `DynamicForm`, `EntityForm`, and individual field components have:
|
||||
|
||||
- `<label>` with `htmlFor` wired to the input `id`
|
||||
- `aria-required={field.isRequired}`
|
||||
- `aria-invalid={!!errors[field.name]}`
|
||||
- `aria-describedby` pointing to the error message element
|
||||
- Required asterisk marked `aria-hidden="true"` to avoid screen-reader repetition
|
||||
- Help tooltips use Radix Tooltip, which is keyboard-accessible via `Tab` / `Escape`
|
||||
|
||||
---
|
||||
|
||||
### CSS Media Queries
|
||||
|
||||
#### `prefers-reduced-motion` (D-L-2 / D-T-1)
|
||||
|
||||
`src/styles/globals.css` collapses all animation and transition durations to near-zero
|
||||
when the user's OS has reduced motion enabled. This resolves skeleton pulse, sidebar
|
||||
slide-in, toast fade, and all other CSS transitions:
|
||||
|
||||
```css
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### `prefers-contrast: more` (D-T-2)
|
||||
|
||||
A high-contrast media block increases border weights and forces text colors to fully opaque
|
||||
black, targeting users who have enabled "Increase Contrast" in their OS settings:
|
||||
|
||||
```css
|
||||
@media (prefers-contrast: more) {
|
||||
:root {
|
||||
--color-border: #000000;
|
||||
--color-input: #000000;
|
||||
--color-muted-foreground: #000000;
|
||||
}
|
||||
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
border-color: var(--color-border) !important;
|
||||
}
|
||||
|
||||
:focus-visible {
|
||||
outline: 3px solid #000000 !important;
|
||||
outline-offset: 2px !important;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Mobile 16px Font-Size Fix (D-R-1)
|
||||
|
||||
iOS Safari auto-zooms any focused input whose `font-size` is below 16px. A mobile-scoped rule
|
||||
prevents this without affecting desktop appearance:
|
||||
|
||||
```css
|
||||
@media screen and (max-width: 767px) {
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Dark-Mode Focus Ring Fix (D-T-5)
|
||||
|
||||
The default `color-mix`-based focus ring can disappear on dark backgrounds. A dark-mode
|
||||
override forces the ring to a solid, fully opaque color:
|
||||
|
||||
```css
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:focus-visible {
|
||||
outline-color: var(--color-ring);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Additional Resolved Items (from `What's Working Well`)
|
||||
|
||||
| Item | Implementation |
|
||||
|------|----------------|
|
||||
| `aria-required`, `aria-invalid`, `aria-describedby` on all form inputs | `DynamicFormField.tsx`, `EntityForm.tsx` |
|
||||
| `role="status"` + `aria-busy="true"` on skeleton components | All skeleton components |
|
||||
| `role="status"` + `aria-live="polite"` on `EmptyState` | `EmptyState.tsx` |
|
||||
| Full keyboard nav in SearchDialog (ArrowUp/Down/Enter/Escape) | `SearchDialog.tsx` |
|
||||
| Breadcrumbs: `aria-label="Breadcrumb"` + `aria-current="page"` | `Breadcrumbs.tsx` |
|
||||
| `aria-expanded` on mobile drawer | `Sidebar.tsx` |
|
||||
| DOMPurify sanitization on all user-controlled HTML output | `FieldValue.tsx`, `BlockWidget.tsx`, `ProcessHtmlStep.tsx` |
|
||||
| Unsaved-changes guard: `beforeunload` + confirm modal | `EntityForm.tsx` |
|
||||
| Focus rings on all interactive elements via `:focus-visible` | `globals.css` |
|
||||
| Muted text darkened to `#5a6270` for WCAG AA contrast margin | `globals.css` |
|
||||
|
||||
---
|
||||
|
||||
## Automated Testing
|
||||
|
||||
### Running axe-core Scans
|
||||
|
||||
The helper in `tests/e2e/a11y-helpers.ts` wraps `@axe-core/playwright` and asserts zero
|
||||
violations against the `wcag2a`, `wcag2aa`, and `wcag21aa` rule sets:
|
||||
|
||||
```typescript
|
||||
import { checkA11y } from './a11y-helpers'
|
||||
|
||||
test('page is accessible', async ({ page }) => {
|
||||
await page.goto('/app/person')
|
||||
await waitForAppReady(page)
|
||||
await checkA11y(page)
|
||||
})
|
||||
```
|
||||
|
||||
`checkA11y` runs on every navigation E2E test in `navigation.spec.ts`. To run all E2E tests
|
||||
including accessibility assertions:
|
||||
|
||||
```bash
|
||||
pnpm test:e2e
|
||||
```
|
||||
|
||||
To run a single spec with axe output in the terminal:
|
||||
|
||||
```bash
|
||||
pnpm test:e2e -- tests/e2e/navigation.spec.ts
|
||||
```
|
||||
|
||||
### Known Suppressed Violations (with TODOs)
|
||||
|
||||
Three rule violations are currently disabled in `checkA11y` because the underlying issues
|
||||
are not yet fixed. Each has a TODO comment in `a11y-helpers.ts`:
|
||||
|
||||
| Rule | Reason suppressed | TODO |
|
||||
|------|-------------------|------|
|
||||
| `color-contrast` | Amber banner background (#f59e0b) and indigo sidebar/button states yield contrast ratios just below 4.5:1 | Audit all Tailwind color pairs in Banner, Sidebar, and Button and replace with WCAG AA-compliant pairs |
|
||||
| `aria-input-field-name` | Some form inputs rendered by `DynamicForm` may have broken `<label htmlFor>` associations when IDs are generated dynamically | Ensure every `<input>` has either a matching `htmlFor` or an `aria-label` |
|
||||
| `scrollable-region-focusable` | The TanStack Table scrollable wrapper `<div>` has no `tabIndex`, blocking keyboard scrolling | Add `tabIndex={0}` to the scrollable DataGrid wrapper |
|
||||
|
||||
---
|
||||
|
||||
## Outstanding Work
|
||||
|
||||
The following items from the design review are **not yet resolved** and require future work.
|
||||
|
||||
### High Priority (WCAG 2.1 AA gaps)
|
||||
|
||||
| ID | Area | Description |
|
||||
|----|------|-------------|
|
||||
| D-V-1 | Record View | Edit/Delete buttons render regardless of user permissions; they fail with 403 on action instead of being hidden/disabled based on `editPermission`/`deletePermission`. |
|
||||
| D-L-3 | Layout | Global search is hidden on mobile (`hidden md:block`); mobile users have no access to search. |
|
||||
|
||||
### Medium Priority (UX gaps)
|
||||
|
||||
| ID | Area | Description |
|
||||
|----|------|-------------|
|
||||
| D-L-5 | Layout | Mobile sidebar `aria-expanded` state may not be consistently set on the nav landmark. |
|
||||
| D-Q-4 | FilterBuilder | Async combobox shows no loading indicator while possible-value options are fetching. |
|
||||
| D-Q-5 | Pagination | "Go to page" input silently ignores out-of-range values with no user feedback. |
|
||||
| D-Q-7 | DataGrid | Column visibility toggle changes are not announced via a `role="status"` region. |
|
||||
| D-P-2 | Process | Completed step indicators in the wizard are not keyboard-navigable for back-navigation. |
|
||||
| D-P-3 | Process | No timeout message if backend polling runs beyond ~60 seconds. |
|
||||
| D-P-4 | Process | Validation review result list has no severity filter or field-name search. |
|
||||
| D-P-5 | Process | File upload step shows no byte-level progress during multipart upload. |
|
||||
| D-W-2 | Widgets | Widget API call failures show blank space with no error boundary or fallback UI. |
|
||||
| D-FB-1 | Feedback | Keyboard Shortcuts dialog is not discoverable (no visible `?` hint in the header). |
|
||||
| D-FB-2 | Feedback | Toasts have no dismiss button; users must wait for auto-timeout. |
|
||||
| D-FB-3 | Feedback | CommandMenu icon buttons may be below the 44px minimum touch target size. |
|
||||
| D-R-2 | Responsive | Multiple interactive elements (pagination controls, density buttons) below 44px touch target. |
|
||||
|
||||
### Items Deferred Pending Backend Support
|
||||
|
||||
| ID | Area | Description |
|
||||
|----|------|-------------|
|
||||
| D-V-6 | Record View | "Last modified by / at" audit footer requires backend to expose this data on the record endpoint. |
|
||||
| D-T-3 | Theme | Dual theming systems (`--qqq-primary-color` and `--color-primary`) need consolidation; depends on coordinating with backend-supplied theme values. |
|
||||
|
||||
---
|
||||
|
||||
## Checklist for New Component Authors
|
||||
|
||||
When adding a new interactive component, verify the following before opening a pull request:
|
||||
|
||||
- [ ] Every `<input>`, `<select>`, and `<textarea>` has a `<label htmlFor>` that matches its `id`, or an `aria-label` / `aria-labelledby` attribute.
|
||||
- [ ] Required inputs have `aria-required={true}`.
|
||||
- [ ] Invalid inputs have `aria-invalid={true}` and an `aria-describedby` pointing to the error message element.
|
||||
- [ ] All icon-only buttons have a descriptive `aria-label` (e.g. `aria-label="Close dialog"`).
|
||||
- [ ] Icon elements that are purely decorative are marked `aria-hidden="true"`.
|
||||
- [ ] Modals/dialogs trap focus while open and restore it to the triggering element on close (use Radix Dialog for this automatically).
|
||||
- [ ] All interactive elements are reachable and operable via `Tab` / `Shift+Tab` / `Enter` / `Space` / `Escape`.
|
||||
- [ ] Interactive elements have a visible `:focus-visible` ring (do not add `outline: none` without a replacement).
|
||||
- [ ] Any element that announces dynamic state changes uses `aria-live="polite"` (or `role="status"`).
|
||||
- [ ] Touch targets are at least 44x44px (`min-h-[44px] min-w-[44px]` or equivalent padding).
|
||||
- [ ] Color is not the only means of conveying information (pair color with text or icon).
|
||||
- [ ] Text color meets a minimum 4.5:1 contrast ratio against its background for normal-weight text at normal size.
|
||||
- [ ] The component does not break at 320px viewport width.
|
||||
- [ ] The component respects `prefers-reduced-motion` (animations driven by CSS transitions inherit the global rule in `globals.css`; JS-driven animations need a manual check).
|
||||
- [ ] Any rendered HTML that originates from user input or the backend is sanitized with DOMPurify before being set via `dangerouslySetInnerHTML`.
|
||||
- [ ] Run `pnpm test:e2e` and confirm `checkA11y` passes for any page that includes the new component.
|
||||
@@ -0,0 +1,310 @@
|
||||
# API Error Handling Guide
|
||||
|
||||
This document describes how API errors flow through the QQQ Frontend Next application and what, if anything, individual components need to do about them.
|
||||
|
||||
The short answer is: **most errors require no per-call handling**. The global error handler in the query client toasts an appropriate message automatically, and the 401 interceptor in the Axios client redirects to login. Individual components only need to add their own handling when the generic behavior is insufficient for a specific UX requirement.
|
||||
|
||||
---
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
There are two independent error interception layers:
|
||||
|
||||
1. **Axios response interceptor** (`src/lib/api/client.ts`) — fires synchronously on every HTTP response before the promise settles. Handles 401 (unauthorized) only.
|
||||
2. **TanStack Query global handlers** (`src/lib/query-client.ts`) — `QueryCache.onError` for reads, `MutationCache.onError` for writes. Fire after the interceptor. Handle all other errors by toasting a message.
|
||||
|
||||
```
|
||||
API response
|
||||
│
|
||||
▼
|
||||
Axios interceptor
|
||||
│ 401 → call unauthorizedCallback() → redirect to login
|
||||
│ all others → pass through as rejected promise
|
||||
▼
|
||||
TanStack Query cache (onError)
|
||||
│ 401 → silently ignored (already handled)
|
||||
│ 403 → toast "Permission denied" / "You do not have permission…"
|
||||
│ 404 → toast "Resource not found"
|
||||
│ 5xx → toast "Server error — retrying…" / "Server error — please try again"
|
||||
│ network → toast "Something went wrong"
|
||||
▼
|
||||
Component (optional per-query onError)
|
||||
│ Only needed when generic toast is wrong for the specific UX
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## The 401 Interceptor
|
||||
|
||||
**Location:** `src/lib/api/client.ts`
|
||||
|
||||
The singleton `APIClient` registers a response interceptor that fires on every request:
|
||||
|
||||
```typescript
|
||||
this.client.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error: AxiosError) => {
|
||||
if (error.response?.status === 401 && this.unauthorizedCallback) {
|
||||
this.unauthorizedCallback()
|
||||
}
|
||||
return Promise.reject(error) // always re-reject so TanStack Query also sees it
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
The `unauthorizedCallback` is wired to the auth provider's logout/redirect function during initialization. When a 401 arrives:
|
||||
|
||||
1. The callback fires → clears auth state → redirects to the login page.
|
||||
2. The error is still re-rejected, so TanStack Query's `onError` also fires.
|
||||
3. The global handler in `query-client.ts` explicitly silences 401s to avoid a redundant toast on top of the redirect.
|
||||
|
||||
**Implication for components:** Never handle 401 in a component. The interceptor + auth provider handle it globally. Writing per-component 401 handling will race with the redirect and confuse users.
|
||||
|
||||
---
|
||||
|
||||
## The Global Query Error Handler
|
||||
|
||||
**Location:** `src/lib/query-client.ts` — `handleQueryError` function
|
||||
|
||||
```typescript
|
||||
function handleQueryError(error: unknown, context: 'query' | 'mutation'): void {
|
||||
const status = getErrorStatusCode(error)
|
||||
|
||||
if (status === 401) return // handled by axios interceptor
|
||||
|
||||
if (status === 403) {
|
||||
toast.error(
|
||||
context === 'mutation'
|
||||
? 'You do not have permission to perform this action'
|
||||
: 'Permission denied'
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (status === 404) {
|
||||
toast.error('Resource not found')
|
||||
return
|
||||
}
|
||||
|
||||
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'),
|
||||
}),
|
||||
})
|
||||
```
|
||||
|
||||
**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.
|
||||
|
||||
---
|
||||
|
||||
## Extracting the Status Code
|
||||
|
||||
**Location:** `src/lib/utils/error-utils.ts`
|
||||
|
||||
```typescript
|
||||
import { getErrorStatusCode } from '@/lib/utils/error-utils'
|
||||
|
||||
const status = getErrorStatusCode(error)
|
||||
```
|
||||
|
||||
`getErrorStatusCode` handles two shapes:
|
||||
- `AxiosError` — reads `error.response?.status`
|
||||
- Plain objects with a numeric `status` property (e.g. fetch-based wrappers)
|
||||
|
||||
Returns `undefined` for anything else (non-object errors, null, network failures with no response).
|
||||
|
||||
---
|
||||
|
||||
## Status-by-Status Guide
|
||||
|
||||
### 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.
|
||||
|
||||
```typescript
|
||||
import { getErrorStatusCode } from '@/lib/utils/error-utils'
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: (data: FieldValues) => saveRecord('order', data),
|
||||
onError: (error) => {
|
||||
if (getErrorStatusCode(error) === 400) {
|
||||
// Map backend validation messages to React Hook Form fields
|
||||
const fieldErrors = extractFieldErrors(error) // your helper
|
||||
for (const [field, message] of Object.entries(fieldErrors)) {
|
||||
form.setError(field, { type: 'server', message })
|
||||
}
|
||||
}
|
||||
// Non-400 errors are already toasted by the global handler
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
If a 400 arrives outside of a form context, add a toast yourself:
|
||||
|
||||
```typescript
|
||||
onError: (error) => {
|
||||
if (getErrorStatusCode(error) === 400) {
|
||||
toast.error('Invalid request — please check your input')
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 403 — Permission Denied
|
||||
|
||||
The global handler toasts a generic permission-denied message. If the 403 should instead silently hide an action (e.g. a button that should not appear for users without permission), suppress the default behavior by checking permissions from metadata before rendering rather than relying on the API to reject:
|
||||
|
||||
```typescript
|
||||
// Preferred: hide the action before the user tries it
|
||||
{tableMetaData.capabilities.includes('insert') && (
|
||||
<Button onClick={openCreateModal}>New Record</Button>
|
||||
)}
|
||||
```
|
||||
|
||||
If you need custom handling on a 403, use `onError` and call `toast` yourself — or do nothing if the generic toast is acceptable.
|
||||
|
||||
### 404 — Not Found
|
||||
|
||||
The global handler toasts "Resource not found". For record-view pages where 404 means the record was deleted, render an empty state instead:
|
||||
|
||||
```typescript
|
||||
const { data: record, error } = useQuery({
|
||||
queryKey: queryKeys.tableRecord(tableName, recordId),
|
||||
queryFn: () => getRecord(tableName, recordId),
|
||||
})
|
||||
|
||||
if (getErrorStatusCode(error) === 404) {
|
||||
return <EmptyState message="This record no longer exists" />
|
||||
}
|
||||
```
|
||||
|
||||
The global handler will also toast, which is acceptable — the empty state just provides additional UI context.
|
||||
|
||||
### 5xx — Server Error
|
||||
|
||||
The global handler toasts "Server error — retrying…" for queries and "Server error — please try again" for mutations. Queries are automatically retried up to 3 times with exponential back-off (1 s → 2 s → 4 s, max 30 s) before the error surfaces.
|
||||
|
||||
No additional per-component handling is needed for 5xx unless you want a custom retry UI or a structured error state for a specific page.
|
||||
|
||||
### 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.
|
||||
|
||||
---
|
||||
|
||||
## When to Add Per-Query Error Handling
|
||||
|
||||
Add `onError` to a specific `useQuery` or `useMutation` call when:
|
||||
|
||||
| Scenario | What to do |
|
||||
|---|---|
|
||||
| 400 with field-level validation messages | Map errors to form fields with `form.setError()` |
|
||||
| 403 should render empty state instead of toast | Catch 403, render empty UI, call `toast` yourself if the global one also fires |
|
||||
| 404 should render a structured empty state page | Check `getErrorStatusCode(error) === 404` and render `<EmptyState>` |
|
||||
| Error should navigate away (e.g. deleted parent record) | `useRouter().push('/...')` in `onError` |
|
||||
| The generic toast message is wrong for this specific action | Supply a custom `onError` that calls `toast.error(customMessage)` |
|
||||
|
||||
For all other cases, rely on the global handler and write no error-handling code in the component.
|
||||
|
||||
---
|
||||
|
||||
## Retry Behavior
|
||||
|
||||
The `smartRetry` function in `src/lib/query-client.ts` controls when TanStack Query retries:
|
||||
|
||||
```typescript
|
||||
function smartRetry(failureCount: number, error: unknown): boolean {
|
||||
const status = getErrorStatusCode(error)
|
||||
// Never retry client errors — they are deterministic
|
||||
if (status !== undefined && status >= 400 && status < 500) return false
|
||||
// Retry server errors and network failures up to 3 times
|
||||
return failureCount < 3
|
||||
}
|
||||
```
|
||||
|
||||
Back-off schedule: 1 s → 2 s → 4 s, capped at 30 s.
|
||||
|
||||
4xx errors are never retried. There is no per-component way to opt into retrying 4xx errors — that would be intentional since a 400 with bad field data will not resolve itself on retry.
|
||||
|
||||
---
|
||||
|
||||
## Complete Example — Form with 400 Handling
|
||||
|
||||
```typescript
|
||||
'use client'
|
||||
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { toast } from 'sonner'
|
||||
import { saveRecord } from '@/lib/api/tables'
|
||||
import { queryKeys } from '@/lib/query-client'
|
||||
import { getErrorStatusCode } from '@/lib/utils/error-utils'
|
||||
|
||||
export function EditOrderForm({ orderId, schema, defaultValues }: Props) {
|
||||
const queryClient = useQueryClient()
|
||||
const form = useForm({ resolver: zodResolver(schema), defaultValues })
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: (data: FieldValues) => saveRecord('order', orderId, data),
|
||||
onSuccess: () => {
|
||||
// Invalidate so the record view refreshes
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.tableRecord('order', orderId) })
|
||||
toast.success('Order saved')
|
||||
},
|
||||
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.
|
||||
const body = (error as AxiosError<{ errors: Record<string, string> }>).response?.data
|
||||
if (body?.errors) {
|
||||
for (const [field, message] of Object.entries(body.errors)) {
|
||||
form.setError(field, { type: 'server', message })
|
||||
}
|
||||
} else {
|
||||
toast.error('Please correct the highlighted fields')
|
||||
}
|
||||
return
|
||||
}
|
||||
// 401, 403, 404, 5xx, network — already toasted by the global handler
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<form onSubmit={form.handleSubmit((data) => mutation.mutate(data))}>
|
||||
{/* field components */}
|
||||
</form>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| 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 |
|
||||
| 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 |
|
||||
@@ -0,0 +1,316 @@
|
||||
# Code Quality Review — QQQ Frontend Next
|
||||
|
||||
**Date:** 2026-02-27
|
||||
**Scope:** DRY, KISS, component design, hooks, TypeScript quality, naming, structure
|
||||
**Note:** Security/runtime bugs are documented separately in `CODE-REVIEW-2026-02-27.md`
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
The codebase demonstrates **strong architectural foundations** with excellent separation of concerns and metadata-driven rendering patterns. However, there are **significant code quality issues** across multiple dimensions: substantial DRY violations in filter/combobox logic, an overengineered god-hook, inconsistent error handling approaches, and TypeScript practices that miss opportunities for stronger typing. The `use-record-query` hook is an anti-pattern — a 540-line hook mixing data fetching, URL synchronization, localStorage persistence, and UI state. FilterBuilder has two combobox implementations that are ~95% identical. Several utilities and constants are defined inline or scattered across files. While the component structure is clean and the metadata-driven approach is correct, the implementation needs targeted refactoring to reduce coupling, eliminate duplication, and simplify abstractions.
|
||||
|
||||
---
|
||||
|
||||
## CRITICAL Quality Issues (blocks maintainability)
|
||||
|
||||
### CQ-CRIT-1: `use-record-query` — God Hook Anti-Pattern (540 lines)
|
||||
**File:** `src/lib/hooks/use-record-query.ts`
|
||||
|
||||
This hook violates SRP catastrophically. It manages:
|
||||
- Pagination state (pageNum, pageSize)
|
||||
- Filter state (userFilter, quickSearchTerm, filterMode)
|
||||
- Column configuration (columnVisibility, columnOrder, columnWidths)
|
||||
- Row selection state
|
||||
- UI panel toggles (filterPanelOpen, columnConfigOpen)
|
||||
- localStorage persistence (5 separate useEffect blocks)
|
||||
- URL params synchronization (1 useEffect block)
|
||||
- TanStack Query integration (2 queries)
|
||||
- Saved views (saveView, loadView, deleteView callbacks)
|
||||
- 18+ action dispatchers
|
||||
|
||||
The hook returns 40+ properties. Components must destructure dozens of items they don't need. The reducer pattern for 24+ action types is overkill for this state complexity.
|
||||
|
||||
**Why it's Critical:**
|
||||
- Testing requires mocking localStorage, URL manipulation, TanStack Query, and multiple state branches simultaneously
|
||||
- Any change to one concern risks breaking another
|
||||
- Components using this hook are tightly coupled to its entire API shape
|
||||
- The localStorage sync fires 5 separate effects independently
|
||||
|
||||
**Recommended Fix:**
|
||||
Split into 3-4 focused hooks:
|
||||
- `useRecordPagination()` — pageNum, pageSize, totalPages, setPage, setPageSize
|
||||
- `useRecordFilter()` — userFilter, quickSearchTerm, setFilter, resetFilter
|
||||
- `useRecordSelection()` — rowSelection, setRowSelection, clearSelection
|
||||
- `useRecordUIState()` — columnConfigOpen, filterPanelOpen, toggles
|
||||
|
||||
Let RecordQuery coordinate these together.
|
||||
|
||||
---
|
||||
|
||||
### CQ-CRIT-2: FilterBuilder — Two Comboboxes That Are 95% Identical
|
||||
**File:** `src/components/query/FilterBuilder.tsx`, lines 508-927
|
||||
|
||||
`PossibleValueSingleSelect` (lines 508-698) and `PossibleValueMultiSelect` (lines 713-927) are nearly identical. Both:
|
||||
- Fetch options via `fetchTablePossibleValues()`
|
||||
- Debounce search input (300ms) — copied at lines 557-563 and 771-777
|
||||
- Manage AbortController for stale request cancellation — copied at lines 585-590 and 799-804
|
||||
- Implement click-outside-to-close
|
||||
- Render search input, loading state, option list
|
||||
|
||||
Differences: only selection model (single vs. multi) and tag display.
|
||||
|
||||
**Why it's Critical:**
|
||||
- Bug fix in one requires manual sync to the other
|
||||
- 400+ lines of duplicated code
|
||||
- Missing memoization: callbacks re-created on every render
|
||||
|
||||
**Recommended Fix:**
|
||||
Extract a `useAsyncCombobox` hook:
|
||||
```typescript
|
||||
function useAsyncCombobox(tableName: string, fieldName: string) {
|
||||
// Shared debounce, abort, cleanup logic
|
||||
return { options, isLoading, debouncedFetch, abortRef }
|
||||
}
|
||||
```
|
||||
Then SingleSelect and MultiSelect become thin wrappers managing only the selection model.
|
||||
|
||||
---
|
||||
|
||||
### CQ-CRIT-3: `zod-from-metadata` — Repeated Required/Optional Pattern x10
|
||||
**File:** `src/lib/utils/zod-from-metadata.ts`, lines 11-114
|
||||
|
||||
STRING, TEXT, HTML, and PASSWORD cases (lines 19-60, 99-110) all repeat:
|
||||
```typescript
|
||||
let schema = z.string()
|
||||
if (isRequired) schema = schema.min(1, `${label} is required`)
|
||||
if (maxLength) schema = schema.max(maxLength, ...)
|
||||
return isRequired ? schema : schema.optional()
|
||||
```
|
||||
|
||||
The required/optional wrapping pattern is written identically across all 10+ field types.
|
||||
|
||||
**Recommended Fix:**
|
||||
```typescript
|
||||
function buildStringSchema(isRequired: boolean, maxLength?: number, label?: string) {
|
||||
let schema = z.string()
|
||||
if (isRequired) schema = schema.min(1, `${label} is required`)
|
||||
if (maxLength) schema = schema.max(maxLength, `Must be ${maxLength} characters or fewer`)
|
||||
return isRequired ? schema : schema.optional()
|
||||
}
|
||||
|
||||
case 'STRING':
|
||||
case 'TEXT':
|
||||
case 'HTML':
|
||||
case 'PASSWORD':
|
||||
return buildStringSchema(isRequired, field.maxLength, field.label)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### CQ-CRIT-4: Inconsistent Error Status Detection Across Components
|
||||
**Files:**
|
||||
- `src/components/records/RecordView.tsx` lines 68-83 — custom `getErrorStatusCode()`, handles 403/404/500
|
||||
- `src/components/query/RecordQuery.tsx` lines 493-512 — generic "Failed to load records" for all errors
|
||||
|
||||
Error handling is implemented differently in the two main page components. If the API changes, fixes are needed in multiple places.
|
||||
|
||||
**Recommended Fix:**
|
||||
```typescript
|
||||
// src/lib/utils/error-utils.ts
|
||||
export function getErrorStatusCode(error: unknown): number | undefined { ... }
|
||||
export function getErrorMessage(error: unknown, context: string): string { ... }
|
||||
```
|
||||
Use this in both RecordView and RecordQuery.
|
||||
|
||||
---
|
||||
|
||||
## HIGH Quality Issues (significant tech debt)
|
||||
|
||||
### CQ-HIGH-1: RecordQuery — Too Many Responsibilities (586 lines)
|
||||
**File:** `src/components/query/RecordQuery.tsx`
|
||||
|
||||
This component manages: quick search debounce, view mode toggle, mobile filter sheet state, density selector, and renders: toolbar (150+ lines inline), filter panel (desktop + mobile variants), bulk action bar, error/empty states, DataGrid, RecordCardView, and process launcher coordination.
|
||||
|
||||
**Extractable subcomponents:**
|
||||
- `<RecordQueryToolbar />` — all toolbar state and rendering
|
||||
- `<DensitySelector />` — the dropdown (lines 291-339)
|
||||
- `<ViewModeToggle />` — button group (lines 258-288)
|
||||
- `<QuickSearchInput />` — search field with debounce
|
||||
|
||||
---
|
||||
|
||||
### CQ-HIGH-2: FilterBuilder — No Memoization on Recursive Components
|
||||
**File:** `src/components/query/FilterBuilder.tsx`
|
||||
|
||||
The recursive chain FilterBuilder → FilterGroup → CriteriaRow → FilterValueInput doesn't memoize callbacks or components. Every filter state change re-renders all sibling rows, and PossibleValueSingleSelect's useEffect (line 566) re-fires unnecessarily — potentially resetting combobox search input during user input.
|
||||
|
||||
**Fix:**
|
||||
```typescript
|
||||
export const CriteriaRow = React.memo(function CriteriaRow({ ... }) { ... })
|
||||
export const FilterValueInput = React.memo(function FilterValueInput({ ... }) { ... })
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### CQ-HIGH-3: Field Adornments Typed as `Record<string, unknown>`
|
||||
**File:** `src/components/records/FieldValue.tsx`, lines 65-66, 119-120
|
||||
|
||||
```typescript
|
||||
const href = (linkAdornment?.values?.['linkURL'] ?? String(value)) as string
|
||||
const colorMap = (chipAdornment?.values?.['colorMap'] ?? {}) as Record<string, string>
|
||||
```
|
||||
|
||||
These `as` casts are necessary because adornment `values` is `Record<string, unknown>`. The type information exists in the backend but isn't modeled.
|
||||
|
||||
**Recommended Fix:** Discriminated union types for adornments:
|
||||
```typescript
|
||||
type LinkAdornment = { type: 'LINK'; values: { linkURL: string } }
|
||||
type ChipAdornment = { type: 'CHIP'; values: { colorMap: Record<string, string> } }
|
||||
export type FieldAdornment = LinkAdornment | ChipAdornment | IconAdornment | ...
|
||||
```
|
||||
Eliminates all `as` casts in FieldValue.
|
||||
|
||||
---
|
||||
|
||||
### CQ-HIGH-4: Magic Numbers and Strings Scattered Throughout
|
||||
- `use-record-query.ts:246` — `[10, 25, 50, 100].includes(n)` — hardcoded page sizes
|
||||
- `RecordQuery.tsx:75` — `400` ms debounce
|
||||
- `FilterBuilder.tsx:560` — `300` ms debounce
|
||||
- `DataGrid.tsx:41-51` — density class maps inline
|
||||
- `FieldValue.tsx:1024` — bytes-per-KB hardcoded
|
||||
|
||||
**Fix:**
|
||||
```typescript
|
||||
// src/lib/constants.ts
|
||||
export const PAGE_SIZE_OPTIONS = [10, 25, 50, 100] as const
|
||||
export const SEARCH_DEBOUNCE_MS = 400
|
||||
export const COMBOBOX_DEBOUNCE_MS = 300
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### CQ-HIGH-5: `use-process` — Stale Closure via Manual `stateRef`
|
||||
**File:** `src/lib/hooks/use-process.ts`, lines 105-128
|
||||
|
||||
```typescript
|
||||
const stateRef = useRef(state)
|
||||
stateRef.current = state
|
||||
// later: stateRef.current.stepValues
|
||||
```
|
||||
|
||||
Manually syncing a ref to shadow state is a sign that the polling subscription (lines 249-268) is too coupled to mutable state. This pattern is fragile and indicates the state management design needs simplification.
|
||||
|
||||
---
|
||||
|
||||
### CQ-HIGH-6: Dead Toolbar Button
|
||||
**File:** `src/components/query/RecordQuery.tsx`, lines 394-401
|
||||
|
||||
A "Settings / more" button has an aria-label but no onClick handler — it does nothing. Either wire it up or remove it.
|
||||
|
||||
---
|
||||
|
||||
## MEDIUM Quality Issues
|
||||
|
||||
### CQ-MED-1: `use-record-query` — `effectiveFilter` Depends on Unrelated State
|
||||
**File:** `src/lib/hooks/use-record-query.ts`, lines 317-332
|
||||
|
||||
`effectiveFilter` is recomputed when `state.columnVisibility` changes, but column visibility has nothing to do with filters. The dependency array is over-inclusive.
|
||||
|
||||
---
|
||||
|
||||
### CQ-MED-2: Boolean Operator Type is Raw String Cast
|
||||
**File:** `src/components/query/FilterBuilder.tsx`, lines 196-198
|
||||
|
||||
```typescript
|
||||
onChange({ ...filter, booleanOperator: e.target.value as 'AND' | 'OR' })
|
||||
```
|
||||
|
||||
Should use a const assertion or enum to avoid typo risk:
|
||||
```typescript
|
||||
const BOOLEAN_OPERATORS = ['AND', 'OR'] as const
|
||||
type BooleanOperator = typeof BOOLEAN_OPERATORS[number]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### CQ-MED-3: FieldValue — Repeated URL/Email Pattern Matching
|
||||
**File:** `src/components/records/FieldValue.tsx`, lines 294, 339, 358
|
||||
|
||||
Three separate URL/email regex checks scattered across the file. Extract to:
|
||||
```typescript
|
||||
// src/lib/utils/string-utils.ts
|
||||
export const isUrl = (s: string) => /^https?:\/\//i.test(s) || s.startsWith('/')
|
||||
export const isEmail = (s: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### CQ-MED-4: RecordView — Redundant Section Fallback Logic
|
||||
**File:** `src/components/records/RecordView.tsx`, lines 234-289
|
||||
|
||||
```typescript
|
||||
const primarySections = visibleSections.filter(s => !s.tier || s.tier === 'T1' || s.tier === 'basic')
|
||||
// ...
|
||||
const t1Sections = primarySections.length > 0 ? primarySections : visibleSections
|
||||
```
|
||||
|
||||
The fallback `? primarySections : visibleSections` is redundant if all sections without explicit tiers are already included in `primarySections`. Simplify or add a comment explaining when this fallback fires.
|
||||
|
||||
---
|
||||
|
||||
### CQ-MED-5: Missing Error Boundaries Below Route Level
|
||||
Only `src/app/error.tsx` provides an error boundary. Forms and record views should have their own boundaries to prevent full-page crashes from single component errors.
|
||||
|
||||
---
|
||||
|
||||
### CQ-MED-6: `FilterBuilder` — `subs` Variable Name
|
||||
**File:** `src/components/query/FilterBuilder.tsx`, line 178
|
||||
|
||||
```typescript
|
||||
const subs = filter.subFilters ?? []
|
||||
```
|
||||
|
||||
Should be `subFilters` for clarity.
|
||||
|
||||
---
|
||||
|
||||
## LOW / Style Issues
|
||||
|
||||
- **`data-qqq-id` inconsistency** — Mix of `grid-select-row-${row.index}` (underscores), `filter-field-${depth}-${index}` (hyphens). Standardize to kebab-case throughout.
|
||||
- **Abbreviations:** `initVals` → `initialValues` (`use-record-query.ts:257`), `ej` → `exposedJoin` (`use-record-query.ts:348`), `pk` → `primaryKey` (DataGrid, RecordView), `subs` → `subFilters`
|
||||
- **Import ordering:** `FilterBuilder.tsx` imports lucide-react after custom hooks. Project convention is: React → External → Types → Lib → Components.
|
||||
- **Missing JSDoc:** `buildQuickFilterFromState()` (use-record-query.ts), `getErrorStatusCode()` (RecordView.tsx), `zodFieldFromMetadata()` (zod-from-metadata.ts) — all complex enough to warrant a one-liner doc comment.
|
||||
- **Unnecessary `useMemo`:** `joins` calculation in `use-record-query.ts:345-354` is a simple object map — too cheap to justify memoization overhead.
|
||||
- **Unused import:** `SelectField` in `DynamicFormField.tsx` (identified in prior review).
|
||||
- **`window.innerWidth` in callback** — `RecordQuery.tsx:127-134` — not reactive to resize events.
|
||||
|
||||
---
|
||||
|
||||
## Positive Patterns Worth Preserving
|
||||
|
||||
1. **Metadata-Driven Rendering** — entire UI derived from backend metadata, zero hardcoded field/table names.
|
||||
2. **Query Key Factory Pattern** — `queryKeys.tableRecords(tableName)` prevents string-based cache bugs.
|
||||
3. **TanStack Query Usage** — proper staleTime, placeholderData, manual pagination, invalidation. No over-fetching.
|
||||
4. **Typed API Layer** — all `src/lib/api/` functions properly typed, good use of discriminated unions for responses.
|
||||
5. **Filter Serialization** — `serializeFilter()` / `deserializeFilter()` correctly handle shareable URLs.
|
||||
6. **Debouncing** — SearchInput and FilterBuilder correctly debounce and clean up.
|
||||
7. **Tailwind + CSS Custom Properties** — consistent theming without inline styles.
|
||||
8. **Accessibility Fundamentals** — ARIA labels, roles, keyboard handling are present throughout.
|
||||
|
||||
---
|
||||
|
||||
## Recommended Fix Priority
|
||||
|
||||
| Priority | Item | Effort |
|
||||
|----------|------|--------|
|
||||
| 1 | CQ-CRIT-2: Extract `useAsyncCombobox` hook (FilterBuilder) | Medium |
|
||||
| 2 | CQ-CRIT-3: `buildStringSchema()` helper in zod-from-metadata | Small |
|
||||
| 3 | CQ-CRIT-4: Centralize `getErrorStatusCode()` utility | Small |
|
||||
| 4 | CQ-HIGH-3: Discriminated union types for adornments | Medium |
|
||||
| 5 | CQ-HIGH-4: Extract constants to `src/lib/constants.ts` | Small |
|
||||
| 6 | CQ-HIGH-6: Remove or wire dead toolbar button | Trivial |
|
||||
| 7 | CQ-HIGH-2: `React.memo` on FilterBuilder subcomponents | Small |
|
||||
| 8 | CQ-MED-3: Extract URL/email utils | Small |
|
||||
| 9 | CQ-CRIT-1: Split `use-record-query` into focused hooks | Large |
|
||||
| 10 | CQ-HIGH-1: Extract RecordQuery toolbar subcomponents | Medium |
|
||||
@@ -0,0 +1,390 @@
|
||||
# QQQ Frontend Next — Code Review
|
||||
**Date:** 2026-02-27
|
||||
**Reviewer:** Claude (automated)
|
||||
**Scope:** Full `src/` codebase audit
|
||||
**Commit:** df8e2e6
|
||||
|
||||
---
|
||||
|
||||
## CRITICAL (7 findings)
|
||||
|
||||
---
|
||||
|
||||
### CRIT-1: XSS — unsanitized `dangerouslySetInnerHTML` in `FieldValue`
|
||||
|
||||
**File:** `src/components/records/FieldValue.tsx:136–143`
|
||||
|
||||
Raw backend HTML value injected into DOM with zero sanitization:
|
||||
```tsx
|
||||
dangerouslySetInnerHTML={{ __html: String(value) }}
|
||||
```
|
||||
|
||||
**Fix:** Use DOMPurify:
|
||||
```tsx
|
||||
import DOMPurify from 'dompurify'
|
||||
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(String(value)) }}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### CRIT-2: XSS — unsanitized `dangerouslySetInnerHTML` in `BlockWidget` (two locations)
|
||||
|
||||
**File:** `src/components/widgets/BlockWidget.tsx:40–48, 278–285`
|
||||
|
||||
Both the legacy `data.html` path and the `case 'html'` block renderer pass backend HTML directly to the DOM.
|
||||
|
||||
**Fix:** Apply `DOMPurify.sanitize()` to both HTML rendering paths.
|
||||
|
||||
---
|
||||
|
||||
### CRIT-3: XSS — regex script-stripping in `ProcessHtmlStep` is not real sanitization
|
||||
|
||||
**File:** `src/components/process/ProcessHtmlStep.tsx:14–17, 100`
|
||||
|
||||
```ts
|
||||
// TODO: Replace with DOMPurify for full sanitization
|
||||
function stripScripts(html: string): string {
|
||||
return html.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
|
||||
}
|
||||
```
|
||||
|
||||
Bypassed by: `<img onerror="...">`, `<svg onload="...">`, `<a href="javascript:...">`, and dozens of other vectors.
|
||||
|
||||
**Fix:** Replace `stripScripts` with `DOMPurify.sanitize()`.
|
||||
|
||||
---
|
||||
|
||||
### CRIT-4: Auth bypass — `handleOAuthCallback` sets `isAuthenticated=true` without token exchange
|
||||
|
||||
**File:** `src/lib/auth/auth-provider.tsx:164–176`
|
||||
|
||||
```tsx
|
||||
async function handleOAuthCallback(code: string, state: string) {
|
||||
// For now, just mark as authenticated
|
||||
setIsAuthenticated(true)
|
||||
}
|
||||
```
|
||||
|
||||
`code` and `state` params are accepted but unused. No CSRF state validation, no token exchange, no session creation.
|
||||
|
||||
**Fix:** Implement PKCE: validate `state` against `sessionStorage`, exchange `code`+`code_verifier` for tokens via backend `/manageSession`, then set `isAuthenticated` only on success.
|
||||
|
||||
---
|
||||
|
||||
### CRIT-5: Auth bypass — Auth0/OAuth2 sessions authenticate without server validation
|
||||
|
||||
**File:** `src/lib/auth/auth-provider.tsx:123–138`
|
||||
|
||||
Both `setupAuth0Session` and `setupOAuth2Session` call no protected endpoint. Any app configured with AUTH_0 or OAUTH2 shows as authenticated for any page load. Default user is hardcoded: `{ name: 'User', email: 'user@example.com' }`.
|
||||
|
||||
**Fix:** Call a protected endpoint (e.g., `GET /session`) to validate existing session cookie. Redirect to provider if 401.
|
||||
|
||||
---
|
||||
|
||||
### CRIT-6: Auth state corruption — 401 interceptor doesn't clear auth metadata cache
|
||||
|
||||
**File:** `src/lib/auth/auth-provider.tsx:65–70`
|
||||
|
||||
The 401 interceptor redirects to login but does NOT call `clearAuthMetadataCache()`, leaving stale auth metadata. On next page load the cached auth type governs initialization without a fresh server check.
|
||||
|
||||
**Fix:** Call `clearAuthMetadataCache()` inside the 401 callback before redirect.
|
||||
|
||||
---
|
||||
|
||||
### CRIT-7: Silent export failure — `ExportButton` swallows errors with no user feedback
|
||||
|
||||
**File:** `src/components/query/ExportButton.tsx:88–89`
|
||||
|
||||
```tsx
|
||||
} catch (err) {
|
||||
console.error('[ExportButton] Export failed:', err)
|
||||
}
|
||||
```
|
||||
|
||||
Users believe export succeeded when it silently failed.
|
||||
|
||||
**Fix:** Add `toast.error('Export failed. Please try again.')` in the catch block.
|
||||
|
||||
---
|
||||
|
||||
## HIGH (8 findings)
|
||||
|
||||
---
|
||||
|
||||
### HIGH-1: Stale closures — missing deps in URL sync + column persistence effects
|
||||
|
||||
**File:** `src/lib/hooks/use-record-query.ts:280–311`
|
||||
|
||||
Four `useEffect` hooks suppress `react-hooks/exhaustive-deps`. The URL-sync effect is missing `router` and `pathname` — stale closures can fire URL updates on the wrong route in concurrent mode.
|
||||
|
||||
**Fix:** Add `router` and `pathname` to the URL-sync effect deps. Use `useRef` for stable callback references if needed.
|
||||
|
||||
---
|
||||
|
||||
### HIGH-2: Performance — `JSON.stringify` key comparison on every query cache event in `useProcess`
|
||||
|
||||
**File:** `src/lib/hooks/use-process.ts:251–268`
|
||||
|
||||
```ts
|
||||
if (JSON.stringify(key) !== JSON.stringify(expectedKey)) return
|
||||
```
|
||||
|
||||
This serializes two arrays on every cache event during rapid polling intervals.
|
||||
|
||||
**Fix:** Use `queryClient.getQueryData(expectedKey)` or shallow key comparison utility.
|
||||
|
||||
---
|
||||
|
||||
### HIGH-3: Logic bug — `processUUID` unsafe cast falls back to empty string
|
||||
|
||||
**File:** `src/lib/hooks/use-process.ts:274–278`
|
||||
|
||||
```ts
|
||||
const pUUID = (response as QJobResponse & { processUUID?: string }).processUUID ?? ''
|
||||
```
|
||||
|
||||
If `processUUID` is absent, all subsequent step calls use `''` as the UUID, producing 404 API calls.
|
||||
|
||||
**Fix:** Add `processUUID` to `QJobResponse` type. Validate it is non-empty before proceeding.
|
||||
|
||||
---
|
||||
|
||||
### HIGH-4: Logic bug — type guard fallthrough on unexpected `QJobResponse` shapes
|
||||
|
||||
**File:** `src/lib/hooks/use-process.ts:76–82`
|
||||
|
||||
A response with both `error` and `values` fields, or with both `jobUUID` and `values`, falls through all type guards silently. Process state freezes indefinitely.
|
||||
|
||||
**Fix:** Use a discriminated union with an explicit `type` field. Add a default catch-all error case.
|
||||
|
||||
---
|
||||
|
||||
### HIGH-5: Stale auth — metadata cached globally without API base URL scoping
|
||||
|
||||
**File:** `src/lib/api/auth.ts:29–37`
|
||||
|
||||
Auth metadata cached for 1 hour under a single key `qqqAuthMetadata` regardless of API base URL. Backend auth type changes are ignored for up to 1 hour. Multi-instance deployments cross-contaminate caches.
|
||||
|
||||
**Fix:** Include API base URL in cache key. Consider shorter TTL (5–10 min) or TanStack Query instead.
|
||||
|
||||
---
|
||||
|
||||
### HIGH-6: Auth bypass — `globalSearch` silently swallows 401/403 errors
|
||||
|
||||
**File:** `src/lib/api/tables.ts:132–136`
|
||||
|
||||
```ts
|
||||
} catch {
|
||||
// If /search endpoint is not available (404), return empty results
|
||||
return []
|
||||
}
|
||||
```
|
||||
|
||||
Catches all errors including 401 (session expired). A search 401 bypasses the global logout interceptor.
|
||||
|
||||
**Fix:**
|
||||
```ts
|
||||
} catch (err) {
|
||||
if (isAxiosError(err) && err.response?.status === 404) return []
|
||||
throw err
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### HIGH-7: Wrong record selection — `selectedRecordIds` computed from array index, not stable PK
|
||||
|
||||
**File:** `src/lib/hooks/use-record-query.ts:411–421`
|
||||
|
||||
`rowSelection` is keyed by array index but records can reorder after sort/filter/pagination. Index 5 after a filter may point to a completely different record than before.
|
||||
|
||||
**Fix:** Key `rowSelection` by primary key string (same as `getRowId` in DataGrid). Update the `selectedRecordIds` computation to use PK-based lookup.
|
||||
|
||||
---
|
||||
|
||||
### HIGH-8: Dead retry button — `setPage(rq.pageNum)` is a no-op
|
||||
|
||||
**File:** `src/components/query/RecordQuery.tsx:503–508`
|
||||
|
||||
The error-state retry button dispatches `SET_PAGE` with the current page number — TanStack Query sees the same query key and does not re-fetch.
|
||||
|
||||
**Fix:** Use `queryClient.invalidateQueries({ queryKey: queryKeys.tableRecords(tableName) })`.
|
||||
|
||||
---
|
||||
|
||||
## MEDIUM (14 findings)
|
||||
|
||||
---
|
||||
|
||||
### MED-1: `AuthProvider` effect suppresses `onAuthError` dep without documentation
|
||||
|
||||
**File:** `src/lib/auth/auth-provider.tsx:121`
|
||||
|
||||
**Fix:** Document intent: `// intentional: runs once on mount — auth initialization is not repeatable`
|
||||
|
||||
---
|
||||
|
||||
### MED-2: CSS injection — raw `customCss` injected into `<style>` tag without validation
|
||||
|
||||
**File:** `src/app/(dashboard)/layout.tsx:108–119`
|
||||
|
||||
Arbitrary CSS from the backend can overlay phishing forms, exfiltrate keystrokes via CSS selectors, or perform UI redressing.
|
||||
|
||||
**Fix:** Use a CSS sanitizer or restrict to CSS custom property overrides only.
|
||||
|
||||
---
|
||||
|
||||
### MED-3: CSS injection — `accentColor` set on CSS custom properties without format validation
|
||||
|
||||
**File:** `src/app/(dashboard)/layout.tsx:71–89`
|
||||
|
||||
`accentColor: 'red; --qqq-primary: url(https://evil.com)'` would be applied as-is.
|
||||
|
||||
**Fix:** Validate `accentColor` matches `/^#[0-9a-fA-F]{3,8}$|^rgb|^hsl/` before applying.
|
||||
|
||||
---
|
||||
|
||||
### MED-4: Stale `initialValue` reference in `useLocalStorage.removeValue`
|
||||
|
||||
**File:** `src/lib/hooks/use-local-storage.ts:36–43`
|
||||
|
||||
`initialValue` is an object passed inline at call sites — new reference on every render. Causes `removeValue` re-creation and downstream `useEffect` churn.
|
||||
|
||||
**Fix:** Stabilize with `useRef`: `const initialValueRef = useRef(initialValue)`, use `initialValueRef.current` in `removeValue`, remove `initialValue` from deps.
|
||||
|
||||
---
|
||||
|
||||
### MED-5: `filterUtils` `btoa`/`atob` may produce overly long base64 for Unicode filter values
|
||||
|
||||
**File:** `src/lib/utils/filter-utils.ts:224–256`
|
||||
|
||||
Double-encoding (`encodeURIComponent` + `btoa`) produces unnecessarily long strings that may exceed URL limits.
|
||||
|
||||
**Fix:** Use `btoa(unescape(encodeURIComponent(json)))` for true Unicode-safe base64.
|
||||
|
||||
---
|
||||
|
||||
### MED-6: Open redirect — `fromPath` URL param used as `href` without origin validation
|
||||
|
||||
**File:** `src/components/records/RecordView.tsx:409–416`
|
||||
|
||||
`/app/person/1?from=javascript:alert(1)` creates a clickable XSS link.
|
||||
|
||||
**Fix:**
|
||||
```ts
|
||||
const safeFromPath = fromPath?.startsWith('/') ? fromPath : null
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### MED-7: Hydration mismatch — `window.innerWidth` in `useState` initializer
|
||||
|
||||
**File:** `src/components/query/RecordQuery.tsx:92–97`
|
||||
|
||||
Server renders one view mode, client may initialize another, causing React hydration warning.
|
||||
|
||||
**Fix:** Initialize from `preferences.tableDefaultViewMode`, apply mobile override in `useEffect`.
|
||||
|
||||
---
|
||||
|
||||
### MED-8: `goBack` in `useProcess` is client-only, not a server step submission
|
||||
|
||||
**File:** `src/lib/hooks/use-process.ts:341–355`
|
||||
|
||||
Server-side state is not rolled back. If the server expects a `_goBack` step, the process goes out of sync.
|
||||
|
||||
**Fix:** Submit `processStep(processName, processUUID, '_goBack', {})` and let server dictate `nextStep`.
|
||||
|
||||
---
|
||||
|
||||
### MED-9: Missing `useCallback` on `EntityForm.onSubmit`
|
||||
|
||||
**File:** `src/components/forms/EntityForm.tsx:195–197`
|
||||
|
||||
`onSubmit` re-created on every render, causing unnecessary re-renders of all form fields.
|
||||
|
||||
**Fix:** `const onSubmit = useCallback(async (values) => { await activeMutation.mutateAsync(values) }, [activeMutation])`
|
||||
|
||||
---
|
||||
|
||||
### MED-10: Unvalidated `JSON.parse` from localStorage in `getStoredUser`
|
||||
|
||||
**File:** `src/lib/auth/auth-provider.tsx:150–161`
|
||||
|
||||
**Fix:** Add type guard or schema validation.
|
||||
|
||||
---
|
||||
|
||||
### MED-11: `TOGGLE_COLUMN` initializes from `undefined` for never-toggled columns
|
||||
|
||||
**File:** `src/lib/hooks/use-record-query.ts:119–126`
|
||||
|
||||
`!undefined === true` means first toggle always shows column, losing the "default" state distinction.
|
||||
|
||||
**Fix:**
|
||||
```ts
|
||||
const currentVal = state.columnVisibility[action.fieldName] ?? true
|
||||
return { ...state, columnVisibility: { ...state.columnVisibility, [action.fieldName]: !currentVal } }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### MED-12: Memory leak — column resize `mousemove`/`mouseup` listeners not removed on unmount
|
||||
|
||||
**File:** `src/components/query/DataGrid.tsx:231–253`
|
||||
|
||||
If `DataGrid` unmounts during a resize drag, the document event listeners remain.
|
||||
|
||||
**Fix:** Track active listeners in a `useRef`; clean them up in a `useEffect` cleanup.
|
||||
|
||||
---
|
||||
|
||||
### MED-13: Premature error state when `record` is undefined but `isError` is false
|
||||
|
||||
**File:** `src/components/records/RecordView.tsx:120–121`
|
||||
|
||||
```ts
|
||||
if (isError || !record) { /* renders error UI */ }
|
||||
```
|
||||
|
||||
Shows error flash before loading state.
|
||||
|
||||
**Fix:** Split: `if (isError) { ... }` then `if (!record && !isLoading) { ... }`.
|
||||
|
||||
---
|
||||
|
||||
### MED-14: Stateful regex in `HighlightedText` — `gi` flag causes incorrect `test()` after `split()`
|
||||
|
||||
**File:** `src/components/feedback/SearchDialog.tsx:31–47`
|
||||
|
||||
`split(regex)` advances `lastIndex`, making subsequent `regex.test(part)` calls unreliable.
|
||||
|
||||
**Fix:** Create a new regex for `test()`, or use `part.toLowerCase() === query.toLowerCase()`.
|
||||
|
||||
---
|
||||
|
||||
## LOW (8 findings)
|
||||
|
||||
- **LOW-1** `auth.ts:64` — Duplicate `clearAuthMetadataCache()` call (harmless)
|
||||
- **LOW-2** `query-client.ts:55` — Widget cache key unstable with unordered `params` objects
|
||||
- **LOW-3** `Sidebar.tsx:128` — Missing comment explaining intentional dep omission
|
||||
- **LOW-4** `RecordView.tsx:37–44` — `getInitials('')` throws TypeError (empty label)
|
||||
- **LOW-5** `DynamicFormField.tsx` — Unused `SelectField` import (dead code)
|
||||
- **LOW-6** `RecordQuery.tsx:127–134` — `window.innerWidth` in callback not reactive to resize
|
||||
- **LOW-7** `ExportButton.tsx:81–86` — Legacy DOM insertion pattern for file download trigger
|
||||
- **LOW-8** `process-utils.ts`, `use-routes.ts` — Verify `hasPermission`/`readPermission` filtering for nav items
|
||||
|
||||
---
|
||||
|
||||
## Priority Order for Fixes
|
||||
|
||||
1. **CRIT-1, CRIT-2, CRIT-3** — Install DOMPurify, apply to all 4 HTML rendering sites
|
||||
2. **CRIT-4, CRIT-5** — Auth stub implementations need proper server validation
|
||||
3. **CRIT-6** — One-line fix: add `clearAuthMetadataCache()` to 401 callback
|
||||
4. **HIGH-6** — One-line fix: re-throw non-404 errors in `globalSearch`
|
||||
5. **HIGH-7** — Fix record selection to use stable PKs
|
||||
6. **HIGH-8** — Fix retry button to call `invalidateQueries`
|
||||
7. **CRIT-7** — Add error toast to `ExportButton`
|
||||
8. **MED-6** — Validate `fromPath` starts with `/`
|
||||
9. **HIGH-1** — Fix stale closures in `useRecordQuery` effects
|
||||
10. Remaining HIGH/MED items by team priority
|
||||
@@ -0,0 +1,398 @@
|
||||
# Component Patterns
|
||||
|
||||
This guide describes the recurring patterns used throughout the QQQ Frontend Next codebase. Understanding these patterns is essential before adding new components or modifying existing ones.
|
||||
|
||||
---
|
||||
|
||||
## Pattern 1: Metadata-Driven Rendering
|
||||
|
||||
QQQ is a metadata-driven framework. The backend declares all structure — table names, field lists, widget layouts, and navigation items — and the frontend renders from it dynamically. No table names, field names, or navigation entries are hardcoded anywhere in the component tree.
|
||||
|
||||
### How it works
|
||||
|
||||
**Server-side (or near-server) layers** fetch metadata and pass it down as props. **Client Components** never fetch metadata themselves — they receive it through their props and render from it.
|
||||
|
||||
The routing entry point is `src/app/(dashboard)/app/[slug]/page.tsx`. It is a Client Component that fetches the full QQQ instance metadata from the server, resolves the slug against the metadata, and dispatches to the appropriate page component:
|
||||
|
||||
```tsx
|
||||
// src/app/(dashboard)/app/[slug]/page.tsx (simplified)
|
||||
export default function SlugPage() {
|
||||
const params = useParams<{ slug: string }>()
|
||||
const slug = params.slug
|
||||
|
||||
const { data: metaData } = useQuery({
|
||||
queryKey: queryKeys.metadataAll(),
|
||||
queryFn: loadMetaData,
|
||||
staleTime: 1000 * 60 * 30, // 30 minutes
|
||||
})
|
||||
|
||||
// Resolve slug against apps, tables, processes, reports in priority order
|
||||
const isTable = Boolean(metaData?.tables?.[slug])
|
||||
const table = metaData?.tables?.[slug]
|
||||
|
||||
// Pass resolved metadata as props — the child component never fetches
|
||||
if (isTable && table) {
|
||||
const tableProcesses = getProcessesForTable(metaData, slug)
|
||||
return <RecordQuery tableName={slug} tableMetaData={table} processes={tableProcesses} />
|
||||
}
|
||||
// ... other cases
|
||||
}
|
||||
```
|
||||
|
||||
`RecordQuery` receives `tableMetaData` as a prop. It does not know or care how that data arrived — it renders from it:
|
||||
|
||||
```tsx
|
||||
// src/components/query/RecordQuery.tsx
|
||||
interface RecordQueryProps {
|
||||
tableName: string
|
||||
tableMetaData: QTableMetaData // always passed in, never fetched internally
|
||||
processes?: QProcessMetaData[]
|
||||
}
|
||||
|
||||
export function RecordQuery({ tableName, tableMetaData, processes }: RecordQueryProps) {
|
||||
// tableMetaData is already resolved — no fetching here
|
||||
const canCreate = tableMetaData.insertPermission
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Key rules
|
||||
|
||||
- `tableMetaData` (and any other metadata object) is `undefined` until TanStack Query resolves it. Every component must handle the loading state. The slug page renders a spinner until metadata is ready; it never passes `undefined` to its child components.
|
||||
- User-facing text always uses `label` from metadata, never `name`. For example, the page header is set to `table?.label ?? slug`, not the raw slug.
|
||||
- The `resolveSlugTarget` function in `SlugPage` is extracted as a pure function precisely so it can be tested in isolation without any rendering.
|
||||
|
||||
### Resolution priority
|
||||
|
||||
The slug page resolves in this order: **app** → **table** → **process** → **report**. The first match wins.
|
||||
|
||||
---
|
||||
|
||||
## Pattern 2: data-qqq-id Attributes
|
||||
|
||||
Every significant or interactive element carries a `data-qqq-id` attribute. This attribute serves two purposes:
|
||||
|
||||
1. **CSS customization hook** — deployers can write `[data-qqq-id="button-save"] { ... }` in their own stylesheets to override the default styling without touching source code.
|
||||
2. **Test selector** — Playwright E2E tests and unit tests target elements by `data-qqq-id` rather than by class name (which is an implementation detail subject to change).
|
||||
|
||||
### Naming convention
|
||||
|
||||
The format is `{component-type}-{identifier}`:
|
||||
|
||||
| Pattern | Example |
|
||||
|---|---|
|
||||
| `record-query-{tableName}` | `record-query-orders` |
|
||||
| `widget-{name}` | `widget-ordersSummary` |
|
||||
| `widget-label-{name}` | `widget-label-ordersSummary` |
|
||||
| `widget-content-{name}` | `widget-content-ordersSummary` |
|
||||
| `button-widget-reload-{name}` | `button-widget-reload-ordersSummary` |
|
||||
| `button-widget-export-{name}` | `button-widget-export-ordersSummary` |
|
||||
| `button-widget-help-{name}` | `button-widget-help-ordersSummary` |
|
||||
| `button-save` | `button-save` |
|
||||
| `button-cancel` | `button-cancel` |
|
||||
| `field-value-{fieldName}` | `field-value-status` |
|
||||
| `filter-panel-close` | `filter-panel-close` |
|
||||
| `link-back-to-source` | `link-back-to-source` |
|
||||
| `unknown-slug-{slug}` | `unknown-slug-foo` |
|
||||
|
||||
### Examples from the actual code
|
||||
|
||||
`WidgetBlock.tsx` — the outer card, its label, and its icon buttons:
|
||||
```tsx
|
||||
<section
|
||||
data-qqq-id={`widget-${name}`}
|
||||
aria-label={label}
|
||||
>
|
||||
<h3 data-qqq-id={`widget-label-${name}`}>{label}</h3>
|
||||
<button data-qqq-id={`button-widget-help-${name}`}>...</button>
|
||||
<button data-qqq-id={`button-widget-export-${name}`}>...</button>
|
||||
<button data-qqq-id={`button-widget-reload-${name}`}>...</button>
|
||||
<div data-qqq-id={`widget-content-${name}`}>...</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
`RecordQuery.tsx` — the outer container, the filter close button, and the back link:
|
||||
```tsx
|
||||
<div data-qqq-id={`record-query-${tableName}`}>
|
||||
<Link data-qqq-id="link-back-to-source">...</Link>
|
||||
<button data-qqq-id="filter-panel-close">...</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
`EntityForm.tsx` — the form itself and its action buttons:
|
||||
```tsx
|
||||
<form data-qqq-id={`entity-form-${tableMetaData.name}`}>
|
||||
<button data-qqq-id="button-cancel">Cancel</button>
|
||||
<button data-qqq-id="button-save">Save</button>
|
||||
</form>
|
||||
```
|
||||
|
||||
`FieldValue.tsx` — every field value element is tagged with its field name:
|
||||
```tsx
|
||||
<span data-qqq-id={`field-value-${field.name}`}>...</span>
|
||||
```
|
||||
|
||||
### Rules
|
||||
|
||||
- Do not use Tailwind class names as test selectors. If a test needs to click a button, it should use `page.getByTestId` or `[data-qqq-id="..."]` selectors, not `.bg-primary` or `.rounded-md`.
|
||||
- Identifier segments use the backend `name` field (e.g., `orders`), not the `label` (e.g., `"Customer Orders"`). Labels can contain spaces and special characters; names are safe identifiers.
|
||||
|
||||
---
|
||||
|
||||
## Pattern 3: Async Combobox (`useAsyncCombobox`)
|
||||
|
||||
Any dropdown that loads its options from the API with user-typed search uses the `useAsyncCombobox` hook (`src/lib/hooks/use-async-combobox.ts`). The hook encapsulates fetch, debounce, AbortController cancellation, and click-outside-to-close logic that would otherwise be duplicated in every combobox component.
|
||||
|
||||
### When to use it
|
||||
|
||||
Use `useAsyncCombobox` when:
|
||||
- The options come from a QQQ possible-value source (table-backed lookup).
|
||||
- The user can type to search and the list is fetched on demand.
|
||||
- The option list is too large to load all at once (more than ~50 items).
|
||||
|
||||
Use a plain `<select>` or a static shadcn/ui `<Select>` when:
|
||||
- The options are a small static list (e.g., sort direction: "Ascending / Descending").
|
||||
- The options are already available in memory (e.g., column names from `tableMetaData.fields`).
|
||||
|
||||
### Usage
|
||||
|
||||
```tsx
|
||||
import { useAsyncCombobox } from '@/lib/hooks/use-async-combobox'
|
||||
|
||||
function MyPossibleValueInput({ tableName, fieldName, onChange }) {
|
||||
const {
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
searchTerm,
|
||||
setSearchTerm,
|
||||
options, // QPossibleValue[] — the fetched options to display
|
||||
isLoading,
|
||||
containerRef, // attach to outermost wrapper for click-outside close
|
||||
inputRef, // attach to the <input> for focus management
|
||||
debouncedFetch, // call in onChange handler to trigger debounced fetch
|
||||
} = useAsyncCombobox({
|
||||
tableName,
|
||||
fieldName,
|
||||
onOptionsFetched: (results) => {
|
||||
// optional: update a label map or other derived state
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="relative">
|
||||
<input
|
||||
ref={inputRef}
|
||||
value={searchTerm}
|
||||
onChange={(e) => {
|
||||
setSearchTerm(e.target.value)
|
||||
debouncedFetch(e.target.value)
|
||||
}}
|
||||
onFocus={() => setIsOpen(true)}
|
||||
/>
|
||||
{isOpen && (
|
||||
<ul role="listbox">
|
||||
{isLoading && <li>Loading...</li>}
|
||||
{options.map((opt) => (
|
||||
<li
|
||||
key={opt.id}
|
||||
role="option"
|
||||
onClick={() => {
|
||||
onChange(opt.id)
|
||||
setIsOpen(false)
|
||||
}}
|
||||
>
|
||||
{opt.label}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### What the hook manages internally
|
||||
|
||||
| Concern | Mechanism |
|
||||
|---|---|
|
||||
| Debouncing | `setTimeout` cleared on each call; fires after `COMBOBOX_DEBOUNCE_MS` (200 ms) |
|
||||
| Request cancellation | `AbortController` — aborts the previous fetch before starting a new one |
|
||||
| Click-outside close | `document.addEventListener('mousedown', handler)` attached to `containerRef` |
|
||||
| Auto-fetch on open | `useEffect` on `isOpen` — fetches the full list when the dropdown opens |
|
||||
| Cleanup on unmount | Clears pending timer and aborts any in-flight request |
|
||||
|
||||
### Return values
|
||||
|
||||
| Field | Type | Purpose |
|
||||
|---|---|---|
|
||||
| `isOpen` | `boolean` | Drive conditional rendering of the dropdown list |
|
||||
| `setIsOpen` | `Dispatch` | Programmatically open/close (e.g., on focus, on select) |
|
||||
| `searchTerm` | `string` | Controlled value for the text input |
|
||||
| `setSearchTerm` | `Dispatch` | Update the text input value |
|
||||
| `options` | `QPossibleValue[]` | The fetched options — map these to `<li>` elements |
|
||||
| `isLoading` | `boolean` | Show a spinner or loading state while the fetch is in-flight |
|
||||
| `containerRef` | `RefObject<HTMLDivElement>` | Attach to the outermost wrapper element |
|
||||
| `inputRef` | `RefObject<HTMLInputElement>` | Attach to the text `<input>` element |
|
||||
| `fetchOptions` | `(term: string) => Promise<void>` | Immediate fetch (bypasses debounce) |
|
||||
| `debouncedFetch` | `(term: string) => void` | Debounced fetch — use this in `onChange` |
|
||||
|
||||
---
|
||||
|
||||
## Pattern 4: Widget Rendering Pipeline
|
||||
|
||||
All dashboard widgets flow through a three-layer pipeline:
|
||||
|
||||
```
|
||||
ConnectedWidget
|
||||
└─ WidgetBlock (card chrome, loading, error, dropdown selects)
|
||||
└─ WidgetRenderer (type dispatcher)
|
||||
└─ StatisticsWidget | BarChartWidget | BlockWidget | ...
|
||||
```
|
||||
|
||||
### Layer 1: ConnectedWidget
|
||||
|
||||
`src/components/widgets/ConnectedWidget.tsx` is the primary entrypoint. It:
|
||||
|
||||
1. Reads `widgetMetaData.dropdowns` and initializes dropdown state.
|
||||
2. Fetches dropdown option lists from the possible-value API.
|
||||
3. Calls `useWidget(widgetMetaData.name, mergedParams)` to fetch the widget's data payload.
|
||||
4. Returns `null` immediately when `widgetMetaData.hasPermission === false`.
|
||||
5. Renders `<WidgetBlock>` with loading/error props and passes the fetched `data` to `<WidgetRenderer>`.
|
||||
|
||||
```tsx
|
||||
// ConnectedWidget orchestrates everything
|
||||
export function ConnectedWidget({ widgetMetaData, params, className }) {
|
||||
const { data, isLoading, isError, error, refetch } = useWidget(widgetMetaData.name, mergedParams)
|
||||
|
||||
return (
|
||||
<WidgetBlock
|
||||
widgetMetaData={widgetMetaData}
|
||||
isLoading={isLoading}
|
||||
isError={isError}
|
||||
error={error}
|
||||
onReload={refetch}
|
||||
// ... dropdown props
|
||||
>
|
||||
{data && <WidgetRenderer widgetMetaData={widgetMetaData} data={data} />}
|
||||
</WidgetBlock>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Layer 2: WidgetBlock
|
||||
|
||||
`src/components/widgets/WidgetBlock.tsx` provides the standard card shell:
|
||||
- Header with `label`, optional help tooltip, optional export button, optional reload button, and optional dropdown `<select>` elements.
|
||||
- Body area that renders a skeleton, an error state, or its `children`.
|
||||
- A `bare` prop that strips all chrome for use inside composite widgets.
|
||||
|
||||
The `WidgetBlock` does not know what type of widget it wraps. It only knows about loading/error state.
|
||||
|
||||
### Layer 3: WidgetRenderer
|
||||
|
||||
`src/components/widgets/WidgetRenderer.tsx` is the type dispatcher. It receives `widgetMetaData` and the raw `data` payload, reads `widgetMetaData.type`, and renders the appropriate typed component via a `switch` statement.
|
||||
|
||||
Supported types:
|
||||
|
||||
| `widgetMetaData.type` | Rendered component |
|
||||
|---|---|
|
||||
| `'statistics'` | `StatisticsWidget` |
|
||||
| `'barChart'` | `BarChartWidget` (lazy) |
|
||||
| `'lineChart'` | `LineChartWidget` (lazy) |
|
||||
| `'pieChart'` | `PieChartWidget` (lazy) |
|
||||
| `'recordGrid'` | `RecordGridWidget` |
|
||||
| `'html'` / `'block'` | `BlockWidget` |
|
||||
| `'divider'` | `DividerWidget` |
|
||||
| `'quickLinks'` | `QuickLinksWidget` |
|
||||
| `'alert'` | `AlertWidget` |
|
||||
| `'processSummary'` | `ProcessSummaryWidget` |
|
||||
| `'composite'` / `'parent'` | `CompositeWidget` |
|
||||
| `'chart'` | `ChartTypeDispatcher` (secondary `chartType` dispatch) |
|
||||
|
||||
Chart widgets (`barChart`, `lineChart`, `pieChart`) are loaded with `React.lazy` + `Suspense` to split the Recharts library into a separate bundle chunk. This keeps the initial JS bundle small on pages that contain no charts.
|
||||
|
||||
### How to add a new widget type
|
||||
|
||||
1. Create a new component file, e.g., `src/components/widgets/HeatmapWidget.tsx`. Export a typed payload interface and the component:
|
||||
|
||||
```tsx
|
||||
// src/components/widgets/HeatmapWidget.tsx
|
||||
export interface HeatmapWidgetPayload {
|
||||
rows: Array<{ label: string; cells: number[] }>
|
||||
columnLabels: string[]
|
||||
}
|
||||
|
||||
export function HeatmapWidget({ data, widgetName }: { data: HeatmapWidgetPayload; widgetName: string }) {
|
||||
// render the heatmap
|
||||
}
|
||||
```
|
||||
|
||||
2. Add a `case` to the `switch` in `WidgetRenderer.tsx`:
|
||||
|
||||
```tsx
|
||||
import { HeatmapWidget } from './HeatmapWidget'
|
||||
import type { HeatmapWidgetPayload } from './HeatmapWidget'
|
||||
|
||||
// Inside the switch:
|
||||
case 'heatmap':
|
||||
return <HeatmapWidget data={data as HeatmapWidgetPayload} widgetName={name} />
|
||||
```
|
||||
|
||||
3. The backend returns `widgetMetaData.type = 'heatmap'` for this widget. No other changes are needed — `ConnectedWidget` picks it up automatically.
|
||||
|
||||
---
|
||||
|
||||
## Pattern 5: Field Adornment Priority
|
||||
|
||||
`FieldValue` (`src/components/records/FieldValue.tsx`) renders a single QQQ field value in read-only mode. When a field has adornments (decorations declared in `field.adornments[]`), `FieldValue` applies them in strict priority order. The first matching adornment wins; subsequent ones are skipped.
|
||||
|
||||
### Priority order
|
||||
|
||||
1. **LINK** — renders the value as an `<a>` tag with an external link icon.
|
||||
2. **FILE_DOWNLOAD** — renders a download anchor using the display value as the URL.
|
||||
3. **SIZE** — formats the raw number as a human-readable byte size (e.g., `1.5 MB`).
|
||||
4. **CHIP** — renders the value as a colored badge using a `colorMap` from adornment values.
|
||||
5. **RENDER_HTML** (or field type `HTML`) — renders sanitized HTML via `DOMPurify.sanitize`.
|
||||
6. **CODE_EDITOR** — renders the value in a monospace `<pre><code>` block.
|
||||
7. **TOOLTIP** — wraps the value in a Radix tooltip. Falls through to type-based rendering when no tooltip text is resolved.
|
||||
8. **ERROR** — renders the value with a destructive icon and styling.
|
||||
9. Record reference link — when `field.possibleValueSourceName` matches a known table, renders as a Next.js `<Link>` with a `RecordHoverCard` preview.
|
||||
10. Type-based fallbacks — `BOOLEAN`, `PASSWORD`, `BLOB`, `TEXT` each have dedicated renderers.
|
||||
11. Default — auto-links bare `http(s)://` URLs and `mailto:` email addresses.
|
||||
|
||||
### How adornment detection works
|
||||
|
||||
Each adornment is checked with `Array.some()` before its rendering block. The discriminated union type `FieldAdornment` is narrowed inside each block using a type predicate:
|
||||
|
||||
```tsx
|
||||
// Detection
|
||||
const hasLink = field.adornments?.some((a) => a.type === 'LINK')
|
||||
|
||||
// Narrowing within the block
|
||||
if (hasLink) {
|
||||
const linkAdornment = field.adornments?.find(
|
||||
(a): a is Extract<FieldAdornment, { type: 'LINK' }> => a.type === 'LINK'
|
||||
)
|
||||
const href = linkAdornment?.values?.linkURL ?? String(value)
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
The `Extract<FieldAdornment, { type: 'LINK' }>` predicate gives TypeScript full knowledge of which `values` keys are available for that specific adornment type without a cast.
|
||||
|
||||
### TOOLTIP fall-through
|
||||
|
||||
The TOOLTIP case has one deliberate exception to the "first match wins" rule: if the `TOOLTIP` adornment is present but none of the `values` keys (`tooltipText`, `text`, `tooltip`) resolve to a non-empty string, the block falls through to type-based rendering. The value is still displayed — only the tooltip wrapper is omitted because there is nothing to show. This is documented with a comment in the source.
|
||||
|
||||
### DOMPurify sanitization
|
||||
|
||||
HTML values are sanitized once via `useMemo` that only re-runs when `value` changes:
|
||||
|
||||
```tsx
|
||||
const sanitizedHtml = useMemo(
|
||||
() => DOMPurify.sanitize(String(value)),
|
||||
[value]
|
||||
)
|
||||
```
|
||||
|
||||
This prevents unnecessary sanitization work on every render while also preventing XSS from backend-provided HTML content.
|
||||
@@ -0,0 +1,36 @@
|
||||
# Material CSS hooks in Next UI
|
||||
|
||||
QQQ application themes can provide `supplementalInstanceMetaData.materialDashboardTheme.customCss`. Next UI keeps the main Material Dashboard `data-qqq-id` hooks so application CSS can target metadata-driven controls. The source of truth for ID generation is [`src/lib/utils/qqq-id.ts`](../src/lib/utils/qqq-id.ts); [NAV-055](acceptance/feature-matrix.md) exercises representative selectors against the real sample server.
|
||||
|
||||
## ID rules
|
||||
|
||||
The ID suffix is lowercased, each run of characters outside `a-z` and `0-9` becomes one dash, leading and trailing dashes are removed, and the result is cut at 50 characters. Camel case is not split: `firstName` becomes `firstname`. Use the metadata label for navigation entries and the metadata name for fields, tables and sections.
|
||||
|
||||
| Control | Pattern | Example |
|
||||
|---|---|---|
|
||||
| Button | `button-{id or text}` | `button-save`, `button-query-mode-advanced` |
|
||||
| Editable text field | `input-{field}` | `input-firstname` |
|
||||
| Possible-value field | `select-{field}` | `select-speciesid` |
|
||||
| Boolean field | `switch-{field}` | `switch-isemployed` |
|
||||
| Navigation item | `sidenav-{label}` | `sidenav-people-app` |
|
||||
| Menu item | `menu-item-{text}` | `menu-item-bulk-edit` |
|
||||
| Tab | `tab-{label}` | `tab-related` |
|
||||
| Table header | `table-header-{field}` | `table-header-firstname` |
|
||||
| Link | `link-{text}` | `link-back-to-person` |
|
||||
|
||||
The ID may be on a layout-neutral wrapper around a native button or menu item. Select the wrapper when applying layout or visibility rules, and select its button descendant for button-specific styles. Inspect the rendered element before changing an application's CSS.
|
||||
|
||||
## Stable named hooks
|
||||
|
||||
- Navigation: `sidenav-root`, `sidenav-logo-area`, `sidenav-menu-list`, `sidenav-logout-button`; `.qqq-sidebar-active` marks the active item. `data-qqq-sidenav-item-type` identifies `user-profile` and `top-level-parent-app` entries.
|
||||
- App home and chrome: `app-card-{appName}-icon`, `.banner.info`/`.banner.warning`/`.banner.error`, and `.qqq-branded-header-bar`.
|
||||
- Query: `button-filter-builder`, `button-views`, `quick-views-container`, `button-query-mode-basic`, `button-query-mode-advanced`, `table-header-{field}`, and `menu-item-{action}`.
|
||||
- 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`.
|
||||
|
||||
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.
|
||||
|
||||
## Display mode and primary text
|
||||
|
||||
Next's persisted light/dark choice controls both theme tokens and Tailwind `dark:` variants. OS color preference does not override it; a configured Material application theme keeps light mode active. `--color-primary` controls branded fills, while `--text-color-primary` controls the existing `text-primary` classes and opacity variants. In light mode the text token follows the primary color; in dark mode it mixes the brand hue with white for readable text on dark surfaces. Explicit application theme and custom CSS rules still take precedence. Verify text and filled-button contrast separately when overriding these tokens.
|
||||
@@ -0,0 +1,278 @@
|
||||
# Design & UX Review — QQQ Frontend Next
|
||||
**Date:** 2026-03-01
|
||||
**Reviewer:** Claude Code (automated static analysis)
|
||||
**Scope:** Full codebase UX/design audit — layout, pages, forms, process, widgets, feedback, theme, responsive, and a11y
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
The new frontend has strong bones: metadata-driven rendering is correctly implemented, component composition is clean, TanStack Query manages server state well, and the base accessibility implementation (labels, aria-required, focus rings, skip nav on breadcrumbs) is solid. The primary gaps fall into three categories:
|
||||
|
||||
1. **Accessibility** — several WCAG 2.1 AA gaps in the data grid, animations, and motion preferences
|
||||
2. **Mobile/responsive polish** — search hidden on mobile, touch targets below 44px minimum in several places
|
||||
3. **Loading/error state coverage** — a handful of async operations show no spinner or fallback
|
||||
|
||||
No showstopper UX regressions from the Material UI frontend were found. The issues below are incremental improvements.
|
||||
|
||||
---
|
||||
|
||||
## Severity Legend
|
||||
|
||||
| Severity | Meaning |
|
||||
|----------|---------|
|
||||
| **CRIT** | WCAG 2.1 Level A violation or severe usability break |
|
||||
| **HIGH** | WCAG 2.1 AA violation or significant UX regression |
|
||||
| **MED** | Noticeable UX gap, inconsistency, or missing polish |
|
||||
| **LOW** | Minor polish, nice-to-have, or future consideration |
|
||||
|
||||
---
|
||||
|
||||
## 1. Layout Shell
|
||||
|
||||
**Files:** `src/components/layout/Sidebar.tsx`, `Header.tsx`, `Breadcrumbs.tsx`, `Banner.tsx`, `src/app/(dashboard)/layout.tsx`
|
||||
|
||||
### Strengths
|
||||
- Desktop sidebar + mobile drawer correctly split at `md` breakpoint
|
||||
- Breadcrumbs have `aria-label="Breadcrumb"` and `aria-current="page"`
|
||||
- Escape key closes mobile drawer; sidebar items have focus rings
|
||||
- Banner severity states (info/warning/error) correctly scoped with icons
|
||||
|
||||
### Findings
|
||||
|
||||
| ID | Severity | Finding |
|
||||
|----|----------|---------|
|
||||
| D-L-1 | **HIGH** | **No skip-to-content link.** Keyboard users must tab through the entire sidebar before reaching page content. Add `<a href="#main-content" className="sr-only focus:not-sr-only">Skip to main content</a>` as the first child of `<body>`. |
|
||||
| D-L-2 | **HIGH** | **Animations ignore `prefers-reduced-motion`.** Sidebar slide-in, banner fade-in, and skeleton `animate-pulse` all run unconditionally. Add `@media (prefers-reduced-motion: reduce) { *, ::before, ::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } }` to `globals.css`. |
|
||||
| D-L-3 | **MED** | **Header search hidden on mobile** (`hidden md:block`). Mobile users lose global search entirely. Add a search icon button on mobile that opens `SearchDialog`. |
|
||||
| D-L-4 | **MED** | **Dismissed banners not persisted.** Banners dismissed via local state reappear on reload. Save dismissed banner keys to `localStorage` under `qqq:dismissed-banners`. |
|
||||
| D-L-5 | **MED** | **Mobile sidebar `aria-expanded` not set.** The mobile drawer open/closed state isn't announced to screen readers. Add `aria-expanded={mobileOpen}` to the nav landmark element. |
|
||||
| D-L-6 | **LOW** | **Breadcrumb overflow on very narrow viewports** (<320px). Long breadcrumb chains don't truncate; last segment can overflow. Add `truncate` + `max-w-[8rem]` on intermediate segments. |
|
||||
| D-L-7 | **LOW** | **Sidebar nav has no loading skeleton.** During the initial metadata fetch the sidebar is empty with no placeholder. Add skeleton nav items matching the expected structure. |
|
||||
|
||||
---
|
||||
|
||||
## 2. Record Query (List View)
|
||||
|
||||
**Files:** `src/components/query/DataGrid.tsx`, `FilterBuilder.tsx`, `Pagination.tsx`, `RecordQueryToolbar.tsx`, `RecordCardView.tsx`, `ColumnConfig.tsx`
|
||||
|
||||
### Strengths
|
||||
- Skeleton rows during fetch match final layout (no layout shift)
|
||||
- Three row density variants (compact/standard/comfortable) with persistence
|
||||
- Empty state with icon, title, and "Clear filters" CTA
|
||||
- Quick-search debounced at 300ms
|
||||
|
||||
### Findings
|
||||
|
||||
| ID | Severity | Finding |
|
||||
|----|----------|---------|
|
||||
| D-Q-1 | **CRIT** | **DataGrid missing `aria-sort` on column headers.** When a column is sorted, screen readers don't announce direction. Add `aria-sort="ascending" \| "descending" \| "none"` to each `<th>` based on current sort state. |
|
||||
| D-Q-2 | **HIGH** | **Row selection checkboxes have no label.** Each row checkbox announces "checkbox" with no context. Add `aria-label={\`Select ${recordLabel}\`}` to each. |
|
||||
| D-Q-3 | **HIGH** | **Column resize handles not keyboard accessible.** Resize is mouse-drag only. Keyboard users cannot adjust column widths. Add `onKeyDown` handler supporting `ArrowLeft`/`ArrowRight` to the resize handle element. |
|
||||
| D-Q-4 | **MED** | **FilterBuilder async combobox has no loading indicator.** While possible-value options are fetching, the dropdown appears empty with no spinner. Add a `Loader2` icon while `isLoading` is true. |
|
||||
| D-Q-5 | **MED** | **"Go to page" input silently ignores out-of-range values.** Typing a page number beyond the last page does nothing with no feedback. Show an inline validation message like "Page must be between 1 and N". |
|
||||
| D-Q-6 | **MED** | **No `aria-live` region for fetch-in-progress.** Background re-fetches (sort/filter changes) are visually indicated by the progress bar but not announced. Add `aria-live="polite"` region announcing "Loading results". |
|
||||
| D-Q-7 | **MED** | **Column visibility toggle state not announced.** Toggling a column in ColumnConfig doesn't notify screen readers. Add a `role="status"` region that announces "Column X hidden" / "Column X visible". |
|
||||
| D-Q-8 | **LOW** | **No frozen/pinned columns.** The primary key / record label column scrolls out of view on wide tables. Consider pinning the first column on horizontal scroll. |
|
||||
| D-Q-9 | **LOW** | **RecordCardView touch targets may be too small.** Cards are clickable but no explicit 44px minimum hit area. Ensure card tap targets meet the 44px minimum on mobile. |
|
||||
|
||||
---
|
||||
|
||||
## 3. Record View (Detail Page)
|
||||
|
||||
**Files:** `src/components/records/RecordView.tsx`, `RecordViewHeader.tsx`, `FieldValue.tsx`, `RecordViewAssociated.tsx`
|
||||
|
||||
### Strengths
|
||||
- DOMPurify sanitization applied before rendering HTML field values
|
||||
- 403/404/500 errors shown with contextual messages
|
||||
- Tab layout for T2/T3 sections with proper semantic structure
|
||||
- Field adornment priority correctly handled (LINK → FILE_DOWNLOAD → SIZE → CHIP)
|
||||
|
||||
### Findings
|
||||
|
||||
| ID | Severity | Finding |
|
||||
|----|----------|---------|
|
||||
| D-V-1 | **HIGH** | **Edit/Delete buttons shown regardless of user permissions.** If table metadata indicates the user lacks UPDATE/DELETE rights, buttons should be hidden (or disabled with tooltip). Currently they render and fail with a 403 on action. |
|
||||
| D-V-2 | **MED** | **Associated records list not paginated.** Many-to-many joins can return thousands of records, all rendered to DOM. Add pagination (or a "Show more" pattern) capped at 25 by default. |
|
||||
| D-V-3 | **MED** | **Avatar initials fail for non-Latin scripts.** `getInitials()` splits on whitespace which produces empty strings for CJK/Arabic names. Add a fallback to a generic user icon when initials can't be derived. |
|
||||
| D-V-4 | **MED** | **No delete confirmation dialog.** The delete action currently triggers immediately. Add a confirm dialog ("Delete this record? This cannot be undone.") before the API call. |
|
||||
| D-V-5 | **LOW** | **No "Copy record ID" quick action.** Admin users frequently need the primary key for support/debugging. Add a small copy-to-clipboard icon next to the record ID. |
|
||||
| D-V-6 | **LOW** | **No audit trail footer.** No "Last modified by / at" metadata shown. If the backend provides this, surface it at the bottom of the record view. |
|
||||
|
||||
---
|
||||
|
||||
## 4. Create / Edit Forms
|
||||
|
||||
**Files:** `src/components/forms/DynamicForm.tsx`, `EntityForm.tsx`, `DynamicFormField.tsx`, `src/components/forms/field-types/`
|
||||
|
||||
### Strengths
|
||||
- All inputs have `aria-required`, `aria-invalid`, `aria-describedby` wired correctly
|
||||
- Unsaved-changes guard (`beforeunload` + modal) prevents accidental data loss
|
||||
- Help tooltips use Radix Tooltip with keyboard support
|
||||
- Required asterisk is `aria-hidden="true"` to avoid screen-reader repetition
|
||||
- Disabled state styling consistent across field types
|
||||
|
||||
### Findings
|
||||
|
||||
| ID | Severity | Finding |
|
||||
|----|----------|---------|
|
||||
| D-F-1 | **HIGH** | **Submit button stays enabled during mutation.** User can click Save multiple times before the first request completes, creating duplicate records. Add `disabled={isSubmitting}` and a loading spinner to the submit button. |
|
||||
| D-F-2 | **MED** | **Number fields don't display min/max constraints.** If field metadata defines `minValue`/`maxValue`, the user doesn't see them until a validation error fires. Show the range as hint text below the field (`0 – 100`). |
|
||||
| D-F-3 | **MED** | **DateTime fields don't indicate time zone.** The input accepts a time but doesn't show which time zone is assumed. Show the user's local TZ abbreviation next to the field. |
|
||||
| D-F-4 | **MED** | **Help tooltips not touch-friendly on mobile.** The `?` icon is hover-only and < 44px. On mobile, show the help text inline beneath the label instead. |
|
||||
| D-F-5 | **MED** | **File upload field has no drag-and-drop visual feedback.** The drop zone changes border on `dragover` but has no text like "Drop file here". Add clear drop-zone copy and a highlight color change. |
|
||||
| D-F-6 | **LOW** | **No "which fields changed" diff in edit mode.** Users editing a long form can't tell what they've modified at a glance. Consider highlighting changed fields with a left border accent. |
|
||||
| D-F-7 | **LOW** | **Large forms show all fields at once.** Sections with 20+ fields have no collapse/expand. Consider optional `<details>` sections for secondary field groups. |
|
||||
|
||||
---
|
||||
|
||||
## 5. Process Execution
|
||||
|
||||
**Files:** `src/components/process/ProcessRun.tsx`, `StepWizard.tsx`, `ProcessFormStep.tsx`, `ValidationReviewStep.tsx`, `ProcessUploadFormStep.tsx`, `BulkLoadStep.tsx`
|
||||
|
||||
### Strengths
|
||||
- Step wizard gives clear numbered/checkmark visual progress
|
||||
- Cancellation confirm dialog prevents accidental process abort
|
||||
- Back navigation available when not on the first step
|
||||
|
||||
### Findings
|
||||
|
||||
| ID | Severity | Finding |
|
||||
|----|----------|---------|
|
||||
| D-P-1 | **HIGH** | **Current step not auto-focused after "Next".** When advancing to a new step, focus remains on the Next button (now gone), leaving screen reader users disoriented. Move focus to the step heading or first field on step change. |
|
||||
| D-P-2 | **MED** | **Step wizard steps not keyboard-navigable (back).** Completed steps have no click handler to jump back. Add click-to-navigate on completed step indicators. |
|
||||
| D-P-3 | **MED** | **No process timeout message.** If the backend hangs, the polling spinner runs indefinitely. After ~60s show "This is taking longer than expected" with an option to wait or cancel. |
|
||||
| D-P-4 | **MED** | **Validation review results not filterable.** With 1000+ errors, the flat list is unmanageable. Add severity filter buttons (All / Errors / Warnings) and a field-name search. |
|
||||
| D-P-5 | **MED** | **File upload step has no progress bar.** Large uploads show no byte-level progress. Add a `<progress>` element or Radix Progress during multipart upload. |
|
||||
| D-P-6 | **MED** | **Step label truncation has no mobile tooltip.** Long step names are truncated at `max-w-[6rem]` with only a `title` attribute, which doesn't appear on touch. Add a Radix Tooltip wrapper. |
|
||||
| D-P-7 | **LOW** | **No "retry" on process step failure.** Users must restart the entire process from step 1 if one step fails. Add a "Retry this step" button where the backend supports re-running a step. |
|
||||
| D-P-8 | **LOW** | **Bulk load column mappings not saveable.** Users re-map the same CSV columns every run. Add a "Save this mapping" option persisted to localStorage. |
|
||||
|
||||
---
|
||||
|
||||
## 6. Widgets & Dashboard
|
||||
|
||||
**Files:** `src/components/widgets/WidgetRenderer.tsx`, `StatisticsWidget.tsx`, `BlockWidget.tsx`, `BarChartWidget.tsx`, `LineChartWidget.tsx`, `PieChartWidget.tsx`, `RecordGridWidget.tsx`
|
||||
|
||||
### Strengths
|
||||
- Charts lazy-loaded via dynamic import (non-chart pages don't load Recharts)
|
||||
- Suspense skeleton placeholder shown while chart chunk loads
|
||||
- Exhaustive `switch` on widget type with unknown fallback
|
||||
|
||||
### Findings
|
||||
|
||||
| ID | Severity | Finding |
|
||||
|----|----------|---------|
|
||||
| D-W-1 | **HIGH** | **Statistics trend arrows not accessible.** `TrendingUp`/`TrendingDown`/`Minus` icons are `aria-hidden` with no text alternative. Screen readers don't announce whether a metric is up or down. Add `<span className="sr-only">trending up</span>` next to each icon. |
|
||||
| D-W-2 | **MED** | **Widget data fetch errors show blank space.** If a widget's API call fails there is no error boundary or fallback UI. Add a per-widget error state (icon + "Could not load widget" message + retry). |
|
||||
| D-W-3 | **MED** | **Chart legends overflow horizontally on mobile.** Recharts legends don't reflow on narrow screens. Use `layout="vertical"` or hide the legend on mobile with a `<ResponsiveContainer>` width check. |
|
||||
| D-W-4 | **MED** | **RecordGrid widget not paginated.** Renders the full result set to DOM. Cap at 25 rows and add a "View all" link to the full record query page. |
|
||||
| D-W-5 | **MED** | **No manual widget refresh.** Stale widget data can't be refreshed without a full page reload. Add a small refresh icon button on each widget card header. |
|
||||
| D-W-6 | **LOW** | **Only 5 chart color tokens defined.** Dashboards with 6+ data series reuse colors, making series indistinguishable. Extend `--color-chart-*` to at least 10 distinct values. |
|
||||
|
||||
---
|
||||
|
||||
## 7. Feedback Components
|
||||
|
||||
**Files:** `src/components/feedback/GlobalSearch.tsx`, `SearchDialog.tsx`, `CommandMenu.tsx`, `EmptyState.tsx`, `ErrorBoundary.tsx`, `KeyboardShortcutsDialog.tsx`
|
||||
|
||||
### Strengths
|
||||
- `EmptyState` uses `role="status"` + `aria-live="polite"` correctly
|
||||
- Skeleton components have `role="status"` + `aria-busy="true"`
|
||||
- SearchDialog has full ArrowUp/ArrowDown/Enter/Escape keyboard nav
|
||||
- `HighlightedText` now correctly uses `i % 2 !== 0` (recently fixed)
|
||||
|
||||
### Findings
|
||||
|
||||
| ID | Severity | Finding |
|
||||
|----|----------|---------|
|
||||
| D-FB-1 | **MED** | **Keyboard Shortcuts dialog not discoverable.** `?` opens it but there is no visible hint in the UI. Add a `?` icon button in the header, or a footer note "Press ? for keyboard shortcuts". |
|
||||
| D-FB-2 | **MED** | **Toasts have no visible close/dismiss button.** Users must wait for auto-dismiss. Add an × button to each toast (sonner supports `closeButton` prop). |
|
||||
| D-FB-3 | **MED** | **CommandMenu icon buttons below 44px touch target.** Small icon-only buttons in the command menu are hard to tap on mobile. Ensure `min-h-[44px] min-w-[44px]` on interactive icons. |
|
||||
| D-FB-4 | **LOW** | **Search results missing table/app context in header.** Results are grouped by table in logic but group labels may not be visually prominent enough. Ensure group headers have sufficient weight/spacing. |
|
||||
|
||||
---
|
||||
|
||||
## 8. Theme & Global Styles
|
||||
|
||||
**Files:** `src/styles/globals.css`, `src/styles/qqq-theme.css`, `src/lib/theme/theme-provider.tsx`, `src/lib/theme/tokens.ts`
|
||||
|
||||
### Strengths
|
||||
- 60+ CSS custom property tokens for colors, spacing, shadows
|
||||
- Complete dark mode override section
|
||||
- `:focus-visible` with proper outline and offset
|
||||
- Muted text color darkened to meet WCAG AA contrast
|
||||
|
||||
### Findings
|
||||
|
||||
| ID | Severity | Finding |
|
||||
|----|----------|---------|
|
||||
| D-T-1 | **HIGH** | **No `prefers-reduced-motion` override.** All animations (skeleton pulse, slide-in transitions, toast fades) run unconditionally. See D-L-2 above — one global CSS rule resolves this. |
|
||||
| D-T-2 | **HIGH** | **No `prefers-contrast: more` support.** Users with OS high-contrast mode enabled get no enhanced styling. Add a `@media (prefers-contrast: more)` block that increases border weights and ensures 7:1 contrast ratios. |
|
||||
| D-T-3 | **MED** | **Dual theming systems create token confusion.** `--qqq-primary-color` and `--color-primary` both define the primary color. Components reference both inconsistently. Consolidate to one system (Tailwind v4 `--color-*` preferred) and alias the `--qqq-*` tokens to the canonical values for backwards compatibility. |
|
||||
| D-T-4 | **MED** | **Z-index tokens defined but not used consistently.** `--qqq-z-sidebar: 100`, `--qqq-z-modal: 1000` exist in the token file but some components use hardcoded values. Use the tokens everywhere. |
|
||||
| D-T-5 | **LOW** | **Focus ring may be invisible in dark mode.** The `color-mix`-based focus outline color may not contrast sufficiently on dark backgrounds. Test with browser dark mode enabled. |
|
||||
| D-T-6 | **LOW** | **No disabled-state semantic token.** All disabled states use `opacity-50`. A dedicated `--color-disabled` token would allow more nuanced disabled styling (e.g., different for text vs background). |
|
||||
|
||||
---
|
||||
|
||||
## 9. Responsive / Mobile
|
||||
|
||||
### Strengths
|
||||
- Sidebar correctly switches between desktop rail and mobile drawer at `md`
|
||||
- RecordQuery shows bottom-sheet filter panel on mobile
|
||||
- RecordCardView provides card layout alternative to data grid on mobile
|
||||
- Pagination uses `flex-wrap` to reflow on narrow screens
|
||||
|
||||
### Findings
|
||||
|
||||
| ID | Severity | Finding |
|
||||
|----|----------|---------|
|
||||
| D-R-1 | **HIGH** | **Mobile `<input>` elements trigger browser zoom.** Inputs with `font-size < 16px` cause iOS Safari to auto-zoom on focus, breaking the layout. Ensure all form inputs have `text-base` (16px) as minimum font size. |
|
||||
| D-R-2 | **MED** | **Touch targets below 44px in multiple places.** Pagination size selector, density buttons, column config toggles. Audit all interactive elements and ensure `min-h-[44px]` or adequate padding. |
|
||||
| D-R-3 | **MED** | **No 320px (iPhone SE) layout testing.** Several components assume ≥375px. Run a responsive check at 320px and fix any overflows. |
|
||||
| D-R-4 | **LOW** | **Landscape tablet layout untested.** At ~768px landscape some components may show desktop layout with insufficient room. Test iPad Mini landscape. |
|
||||
|
||||
---
|
||||
|
||||
## Priority Matrix
|
||||
|
||||
### Must Fix (WCAG 2.1 AA / CRIT / HIGH)
|
||||
|
||||
| ID | Area | Fix |
|
||||
|----|------|-----|
|
||||
| D-L-1 | Layout | Skip-to-content link |
|
||||
| D-L-2 / D-T-1 | Layout / Theme | `prefers-reduced-motion` CSS rule |
|
||||
| D-T-2 | Theme | `prefers-contrast: more` support |
|
||||
| D-Q-1 | Record Query | `aria-sort` on DataGrid column headers |
|
||||
| D-Q-2 | Record Query | Row checkbox `aria-label` |
|
||||
| D-Q-3 | Record Query | Keyboard-accessible column resize |
|
||||
| D-V-1 | Record View | Permission-gated Edit/Delete buttons |
|
||||
| D-F-1 | Forms | Disable submit button while submitting |
|
||||
| D-P-1 | Process | Auto-focus step heading on step change |
|
||||
| D-W-1 | Widgets | Screen-reader text for trend arrows |
|
||||
| D-R-1 | Responsive | 16px minimum font size on inputs |
|
||||
|
||||
### Should Fix (MED — noticeable UX gaps)
|
||||
|
||||
D-L-3 (mobile search), D-L-4 (banner persistence), D-L-5 (sidebar aria-expanded), D-Q-4 (FilterBuilder spinner), D-Q-5 (pagination range feedback), D-Q-6 (aria-live for fetch), D-V-2 (associated records pagination), D-V-4 (delete confirmation), D-F-2 (number field min/max hint), D-F-3 (DateTime TZ indicator), D-F-4 (mobile tooltip → inline help), D-P-2 (step back navigation), D-P-3 (process timeout message), D-P-4 (validation review filter), D-P-5 (upload progress bar), D-W-2 (widget error boundary), D-W-3 (chart legend mobile), D-W-5 (widget refresh button), D-FB-1 (keyboard shortcuts discoverability), D-FB-2 (toast close button), D-T-3 (dual theming consolidation), D-T-4 (z-index token usage), D-R-2 (touch targets), D-R-3 (320px layout)
|
||||
|
||||
### Nice to Have (LOW — polish)
|
||||
|
||||
D-L-6 (breadcrumb truncation), D-L-7 (sidebar skeleton), D-Q-8 (frozen columns), D-V-5 (copy record ID), D-V-6 (audit trail), D-F-6 (edit diff), D-F-7 (collapsible form sections), D-P-7 (retry step), D-P-8 (save bulk load mapping), D-W-6 (chart color tokens), D-FB-4 (search result context), D-T-5 (dark mode focus ring), D-T-6 (disabled token), D-R-4 (landscape tablet)
|
||||
|
||||
---
|
||||
|
||||
## What's Working Well
|
||||
|
||||
The following areas are solid and don't require significant rework:
|
||||
|
||||
- **Metadata-driven rendering** — no hardcoded table/field names anywhere
|
||||
- **API layer** — all calls through typed `src/lib/api/` functions, consistent error handling
|
||||
- **Form accessibility baseline** — `aria-required`, `aria-invalid`, `aria-describedby` correctly wired
|
||||
- **Unsaved changes guard** — `beforeunload` + confirm modal works correctly
|
||||
- **TanStack Query** — caching, deduplication, and revalidation correctly configured
|
||||
- **Dark mode** — complete token coverage, no visual regressions found
|
||||
- **Skeleton loading** — components have `role="status"` and `aria-busy="true"`
|
||||
- **DOMPurify** — applied consistently on all user-controlled HTML output
|
||||
- **URL state** — filters, pagination, and sort are shareable via query params
|
||||
@@ -0,0 +1,194 @@
|
||||
<!--
|
||||
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.
|
||||
-->
|
||||
|
||||
# QQQ Frontend Next — Documentation Review
|
||||
**Date:** 2026-03-01
|
||||
**Scope:** Full `src/` + `docs/` + project root
|
||||
**Commit:** 12edc4c
|
||||
|
||||
---
|
||||
|
||||
## Overall Score: 7.5 / 10
|
||||
|
||||
**Strengths:** JSDoc coverage is high across the API client, utilities, and most hooks. Type definitions are well-documented. `CLAUDE.md` and the implementation plans give a strong architectural overview.
|
||||
|
||||
**Key gaps:** `README.md` is essentially empty, `useRecordQuery` has no return-type documentation, and there are no architecture guides for state management or component composition patterns — the two things a new contributor needs most.
|
||||
|
||||
---
|
||||
|
||||
## CRITICAL — Must Address
|
||||
|
||||
### DOC-CRIT-1: README.md is empty
|
||||
|
||||
**File:** `README.md`
|
||||
**Issue:** The file contains only a title. No setup instructions, command list, project overview, or links to docs.
|
||||
**Impact:** A new developer cannot start contributing without reading `CLAUDE.md` and inferring the rest.
|
||||
|
||||
**Fix:** Add the following sections at minimum:
|
||||
- What this project is (2–3 sentences)
|
||||
- Prerequisites (Node.js version, pnpm)
|
||||
- Quick start (`git clone` → `pnpm install` → `pnpm dev`)
|
||||
- Available commands (dev, build, test, test:e2e, lint, tsc)
|
||||
- Pointer to `CLAUDE.md` for architecture and conventions
|
||||
- Pointer to `docs/` for implementation plans
|
||||
|
||||
---
|
||||
|
||||
### DOC-CRIT-2: `useRecordQuery` return type is undocumented
|
||||
|
||||
**File:** `src/lib/hooks/use-record-query.ts` (~line 60, main hook export)
|
||||
**Issue:** The hook returns a large composite object (pagination, filter, selection, sort, query state, plus delegated `columns` and `views` namespaces from sub-hooks). None of the return properties are documented. Only two components consume this hook today, but that will grow.
|
||||
**Impact:** Callers must trace through 685 lines of source to discover which properties exist.
|
||||
|
||||
**Fix:** Add a `@returns` block enumerating the top-level namespaces and key properties, or define an explicit `UseRecordQueryResult` interface with JSDoc on each field.
|
||||
|
||||
---
|
||||
|
||||
### DOC-CRIT-3: No state-management decision guide
|
||||
|
||||
**Issue:** The project uses five different state containers (React Context, TanStack Query, React Hook Form, URL search params, localStorage), each for different purposes. There is a brief table in `CLAUDE.md` but no guidance on the decision criteria.
|
||||
**Impact:** New contributors pick the wrong state container; inconsistent patterns accumulate over time.
|
||||
|
||||
**Fix:** Create `docs/STATE-MANAGEMENT.md` covering:
|
||||
- TanStack Query: all server state (records, metadata, widgets)
|
||||
- React Hook Form: form field state only
|
||||
- URL search params: filters, pagination, sort (shareable links)
|
||||
- localStorage: user preferences, column config, saved views
|
||||
- QContext: application-level UI state (modals, toast queue)
|
||||
- When NOT to use useState for server-derived state
|
||||
|
||||
---
|
||||
|
||||
## SIGNIFICANT — Should Address
|
||||
|
||||
### DOC-SIG-1: No component composition patterns guide
|
||||
|
||||
**Issue:** The codebase uses several non-obvious patterns not documented anywhere outside of scattered code comments:
|
||||
- Metadata-driven rendering (metadata always flows down; children never fetch)
|
||||
- `data-qqq-id` naming convention (`{component-type}-{identifier}`)
|
||||
- Async combobox pattern (`useAsyncCombobox` wrapping)
|
||||
- Field adornment priority chain (documented in `FieldValue.tsx` but not shared)
|
||||
|
||||
**Fix:** Create `docs/COMPONENT-PATTERNS.md` with each pattern described, the rationale, and a code example.
|
||||
|
||||
---
|
||||
|
||||
### DOC-SIG-2: No type glossary
|
||||
|
||||
**Issue:** The `src/types/` directory exports ~40 interfaces and type aliases. There is no index or glossary. Finding the right type requires `grep`-ing.
|
||||
**Impact:** Types are duplicated or approximated when developers can't find the canonical definition.
|
||||
|
||||
**Fix:** Create `docs/TYPE-GLOSSARY.md` with a table: Type | File | Purpose | Example usage. Focus on the ~15 most-used types (`QInstance`, `QTableMetaData`, `QFieldMetaData`, `SavedView`, `QProcessMetaData`, `StatTile`, `FilterModel`, etc.).
|
||||
|
||||
---
|
||||
|
||||
### DOC-SIG-3: API error handling patterns undocumented
|
||||
|
||||
**File:** `src/lib/api/client.ts` (referenced)
|
||||
**Issue:** The 401 interceptor is documented, but there is no guide to how callers should handle 400 (validation), 403 (permission), 404 (not found), and 500 (server error) responses. Components handle these inconsistently today.
|
||||
|
||||
**Fix:** Add a section to the API client file header or create `docs/API-ERROR-HANDLING.md` with the standard response patterns per HTTP status code.
|
||||
|
||||
---
|
||||
|
||||
### DOC-SIG-4: Zod schema generation lacks examples
|
||||
|
||||
**File:** `src/lib/utils/zod-from-metadata.ts`
|
||||
**Issue:** The function and helpers are well-documented, but there is no `@example` showing how a field metadata object maps to a generated Zod schema. This is the most opaque part of the form validation pipeline.
|
||||
|
||||
**Fix:** Add a 6-line `@example` to `zodFieldFromMetadata` showing an input `QFieldMetaData` object and the corresponding schema.
|
||||
|
||||
---
|
||||
|
||||
### DOC-SIG-5: Form validation strategy not centralized
|
||||
|
||||
**Issue:** Zod schema generation is in `zod-from-metadata.ts`, field-level validation rules are in individual field components, and server-side validation errors are mapped in `EntityForm.tsx`. No single document explains the end-to-end validation strategy.
|
||||
|
||||
**Fix:** Create `docs/FORM-VALIDATION.md` covering schema generation, custom refinements, and mapping server errors to form fields.
|
||||
|
||||
---
|
||||
|
||||
### DOC-SIG-6: TanStack Query key factory undocumented
|
||||
|
||||
**File:** `src/lib/query-client.ts`
|
||||
**Issue:** The query key factory pattern is used throughout the codebase but the naming convention and invalidation patterns are not documented. Developers risk creating overlapping keys.
|
||||
|
||||
**Fix:** Document the key structure and add `@example` tags showing how to invalidate a table's record list after a mutation.
|
||||
|
||||
---
|
||||
|
||||
## MINOR — Nice to Have
|
||||
|
||||
### DOC-MIN-1: Test file headers are sparse
|
||||
|
||||
Many test files (`*.test.tsx`) have no JSDoc file header explaining what behavior is under test and what is explicitly out of scope. This makes it harder to assess test coverage gaps at a glance.
|
||||
|
||||
**Files:** Most files in `src/components/**/*.test.tsx`, `src/lib/hooks/*.test.ts`
|
||||
|
||||
---
|
||||
|
||||
### DOC-MIN-2: Performance decisions lack inline rationale
|
||||
|
||||
Files like `use-record-query.ts` have several `useMemo` / `useCallback` / `useRef` optimizations with comments referencing ticket numbers but no explanation of the underlying performance problem being solved.
|
||||
|
||||
---
|
||||
|
||||
### DOC-MIN-3: Accessibility decisions not consolidated
|
||||
|
||||
WCAG compliance work is scattered across code comments and the design review document. A single `docs/ACCESSIBILITY.md` checklist would make it easy to see what has been done and what remains.
|
||||
|
||||
---
|
||||
|
||||
### DOC-MIN-4: Widget creation guide missing
|
||||
|
||||
The widget rendering pipeline (`WidgetBlock` → `WidgetRenderer` → specific widget component) is non-obvious. A short guide in `docs/` or a code comment in `WidgetRenderer` explaining how to add a new widget type would help.
|
||||
|
||||
---
|
||||
|
||||
## Already Excellent
|
||||
|
||||
| Area | Quality | Notes |
|
||||
|------|---------|-------|
|
||||
| API client (`src/lib/api/`) | 9/10 | 100% JSDoc; error conditions noted; param types clear |
|
||||
| Utilities (`src/lib/utils/`) | 8.5/10 | `filter-utils`, `process-utils`, `zod-from-metadata` all thorough |
|
||||
| Sub-hooks (`use-column-config`, `use-saved-views`, `use-async-combobox`) | 9/10 | Return shapes documented; edge cases noted |
|
||||
| Type definitions (`src/types/metadata.ts`) | 8.5/10 | All major interfaces have field-level JSDoc |
|
||||
| Core query components (`DataGrid`, `FilterBuilder`, `Pagination`, `ColumnConfig`) | 8/10 | Props and behavior well-described |
|
||||
| Records components (`FieldValue`, `RecordView`, `EntityForm`) | 7.5/10 | Good headers; adornment priority documented |
|
||||
| `CLAUDE.md` | 9/10 | Architecture, conventions, file structure all clear |
|
||||
| Implementation plans (`docs/implementation-plans/`) | 8/10 | Phased clearly; shared context well-organized |
|
||||
|
||||
---
|
||||
|
||||
## Recommended Work Order
|
||||
|
||||
### Phase 1 — Unblock contributors (half day)
|
||||
1. **DOC-CRIT-1** — Expand README.md
|
||||
2. **DOC-CRIT-2** — Document `useRecordQuery` return type
|
||||
|
||||
### Phase 2 — Improve onboarding (1 day)
|
||||
3. **DOC-CRIT-3** — Create `docs/STATE-MANAGEMENT.md`
|
||||
4. **DOC-SIG-1** — Create `docs/COMPONENT-PATTERNS.md`
|
||||
5. **DOC-SIG-2** — Create `docs/TYPE-GLOSSARY.md`
|
||||
|
||||
### Phase 3 — Fill API/validation gaps (half day)
|
||||
6. **DOC-SIG-3** — API error handling guide
|
||||
7. **DOC-SIG-4** — Add `@example` to `zodFieldFromMetadata`
|
||||
8. **DOC-SIG-5** — Create `docs/FORM-VALIDATION.md`
|
||||
9. **DOC-SIG-6** — Document query key factory
|
||||
|
||||
### Phase 4 — Polish (as time allows)
|
||||
10–13. **DOC-MIN-1 through DOC-MIN-4** — Test headers, perf rationale, a11y checklist, widget guide
|
||||
@@ -0,0 +1,333 @@
|
||||
# Form Validation
|
||||
|
||||
This guide covers how forms are validated in QQQ Frontend Next. Validation is schema-driven: Zod schemas are generated automatically from the same backend field metadata that drives rendering, so there is no need to maintain a separate validation definition alongside the form.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The validation stack is:
|
||||
|
||||
- **Zod** — runtime schema definition and validation.
|
||||
- **React Hook Form** + `@hookform/resolvers/zod` — drives form state; the Zod schema is passed as the resolver.
|
||||
- **`zodFieldFromMetadata`** — the core function that maps a single `QFieldMetaData` to a Zod validator.
|
||||
- **`zodSchemaFromTableMetadata`** — builds a full `z.ZodObject` from all editable fields in a table.
|
||||
- **`zodSchemaFromFields`** — builds a `z.ZodObject` from a flat array of fields (used for process steps).
|
||||
|
||||
The source for the schema generation utilities is `src/lib/utils/zod-from-metadata.ts`.
|
||||
|
||||
---
|
||||
|
||||
## Schema generation
|
||||
|
||||
### For table-based forms
|
||||
|
||||
`EntityForm` (`src/components/forms/EntityForm.tsx`) builds its schema on mount:
|
||||
|
||||
```tsx
|
||||
const schema = useMemo(
|
||||
() => zodSchemaFromTableMetadata(tableMetaData, fieldNamesToInclude),
|
||||
[tableMetaData, fieldNamesToInclude]
|
||||
)
|
||||
|
||||
const { register, control, handleSubmit, formState: { errors } } = useForm({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: mergedDefaults,
|
||||
})
|
||||
```
|
||||
|
||||
`zodSchemaFromTableMetadata` iterates the table's fields, skips hidden (`isHidden`) and non-editable (`!isEditable`) fields, and calls `zodFieldFromMetadata` for each remaining field:
|
||||
|
||||
```tsx
|
||||
export function zodSchemaFromTableMetadata(
|
||||
tableMetaData: QTableMetaData,
|
||||
fieldNamesToInclude?: string[]
|
||||
): z.ZodObject<Record<string, z.ZodTypeAny>> {
|
||||
const shape: Record<string, z.ZodTypeAny> = {}
|
||||
|
||||
for (const fieldName of fieldsToProcess) {
|
||||
const field = tableMetaData.fields[fieldName]
|
||||
if (!field) continue
|
||||
if (field.isHidden) continue
|
||||
if (!field.isEditable) continue
|
||||
shape[fieldName] = zodFieldFromMetadata(field)
|
||||
}
|
||||
|
||||
return z.object(shape)
|
||||
}
|
||||
```
|
||||
|
||||
When `fieldNamesToInclude` is provided, only those fields are included. This is useful for partial-edit dialogs or multi-step wizards where only a subset of fields are shown per step.
|
||||
|
||||
### For process step forms
|
||||
|
||||
Process steps receive an ordered array of fields rather than a table metadata object. Use `zodSchemaFromFields`:
|
||||
|
||||
```tsx
|
||||
import { zodSchemaFromFields } from '@/lib/utils/zod-from-metadata'
|
||||
|
||||
const schema = zodSchemaFromFields(stepFields) // QFieldMetaData[]
|
||||
```
|
||||
|
||||
Hidden fields are excluded automatically. The resulting schema shape is identical to the table-based variant.
|
||||
|
||||
### For single fields
|
||||
|
||||
Call `zodFieldFromMetadata` directly when you need the Zod type for a single field:
|
||||
|
||||
```tsx
|
||||
import { zodFieldFromMetadata } from '@/lib/utils/zod-from-metadata'
|
||||
|
||||
const fieldSchema = zodFieldFromMetadata(tableMetaData.fields['email'])
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Field type to Zod type mapping
|
||||
|
||||
The table below shows exactly how each QQQ field type is converted. All mappings are implemented in `zodFieldFromMetadata`.
|
||||
|
||||
| QQQ field type | Zod type (required) | Zod type (optional) | Notes |
|
||||
|---|---|---|---|
|
||||
| `STRING` | `z.string().min(1, ...)` | `z.string().optional()` | `maxLength` adds `.max()` when set |
|
||||
| `TEXT` | `z.string().min(1, ...)` | `z.string().optional()` | Same as STRING |
|
||||
| `HTML` | `z.string().min(1, ...)` | `z.string().optional()` | Same as STRING |
|
||||
| `PASSWORD` | `z.string().min(1, ...)` | `z.string().optional()` | Same as STRING |
|
||||
| `INTEGER` | `z.coerce.number().int(...)` | `z.union([z.literal(''), z.coerce.number().int(...)]).optional()` | Empty `<input type="number">` submits `""` |
|
||||
| `LONG` | `z.coerce.number().int(...)` | `z.union([z.literal(''), z.coerce.number().int(...)]).optional()` | Same as INTEGER |
|
||||
| `DECIMAL` | `z.coerce.number(...)` | `z.union([z.literal(''), z.coerce.number(...)]).optional()` | No `.int()` — allows fractional values |
|
||||
| `BOOLEAN` | `z.boolean().optional()` | `z.boolean().optional()` | Always optional — checkboxes are never "required" |
|
||||
| `DATE` | `z.string().min(1, ...)` | `z.string().optional()` | HTML date inputs submit ISO-8601 strings |
|
||||
| `TIME` | `z.string().min(1, ...)` | `z.string().optional()` | Same as DATE |
|
||||
| `DATE_TIME` | `z.string().min(1, ...)` | `z.string().optional()` | Same as DATE |
|
||||
| `BLOB` | `z.union([z.instanceof(File), z.string()]).optional()` | same | Accepts File objects or existing URL strings |
|
||||
|
||||
### Why numbers use `z.literal('')` in the optional case
|
||||
|
||||
An `<input type="number">` submits an empty string (`""`) when left blank, not `null` or `undefined`. A plain `z.coerce.number()` would coerce `""` to `0`, silently making an empty field appear as zero. The `z.union([z.literal(''), schema])` pattern allows the empty string to pass through as-is so the form can distinguish "nothing entered" from "zero entered".
|
||||
|
||||
### Why dates stay as strings
|
||||
|
||||
Date and time values come from `<input type="date">` and `<input type="datetime-local">` as ISO-8601 strings (`"2026-03-01"`, `"2026-03-01T14:30"`). The schema validates them as strings. Any coercion to JavaScript `Date` objects — if required by the API — happens in the submit handler, not in the Zod schema.
|
||||
|
||||
---
|
||||
|
||||
## Required vs optional
|
||||
|
||||
The `isRequired` flag on `QFieldMetaData` is the single source of truth for whether a field is required:
|
||||
|
||||
- **`isRequired: true`** → `.min(1, "${label} is required")` for string types; plain `z.coerce.number()` for numeric types (empty string fails coercion).
|
||||
- **`isRequired: false` or absent** → `.optional()` for string types; `z.union([z.literal(''), ...]).optional()` for numeric types.
|
||||
|
||||
All error messages use the human-readable `field.label` (not `field.name`), so they read naturally in the UI: `"First Name is required"`, not `"firstName is required"`.
|
||||
|
||||
---
|
||||
|
||||
## Number bounds
|
||||
|
||||
When a field's metadata declares `minValue` or `maxValue`, those bounds are applied to the Zod schema:
|
||||
|
||||
```tsx
|
||||
function buildNumberSchema(isRequired, label, isInteger, minValue, maxValue) {
|
||||
let schema = isInteger
|
||||
? z.coerce.number().int(`${label} must be a whole number`)
|
||||
: z.coerce.number({ message: `${label} must be a number` })
|
||||
|
||||
if (minValue !== undefined && minValue !== null) {
|
||||
schema = schema.min(Number(minValue), `${label} must be at least ${minValue}`)
|
||||
}
|
||||
if (maxValue !== undefined && maxValue !== null) {
|
||||
schema = schema.max(Number(maxValue), `${label} must be at most ${maxValue}`)
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
The bounds come directly from metadata — no hardcoded ranges exist in the frontend. `DynamicFormField` also forwards `minValue` and `maxValue` as HTML attributes (`min`, `max`) on `<NumberField>` so the browser provides native range feedback, but Zod is the authoritative validator on submit.
|
||||
|
||||
---
|
||||
|
||||
## String length constraints
|
||||
|
||||
`maxLength` from field metadata maps to Zod's `.max()`:
|
||||
|
||||
```tsx
|
||||
function buildStringSchema(isRequired, label, maxLength) {
|
||||
let schema = z.string()
|
||||
if (isRequired) schema = schema.min(1, `${label} is required`)
|
||||
if (maxLength) schema = schema.max(maxLength, `${label} must be at most ${maxLength} characters`)
|
||||
return isRequired ? schema : schema.optional()
|
||||
}
|
||||
```
|
||||
|
||||
`DynamicFormField` also passes `maxLength` to `<TextField>` as an HTML `maxlength` attribute, providing in-browser character limiting, but the Zod schema validates the constraint on submit as well.
|
||||
|
||||
---
|
||||
|
||||
## Default values
|
||||
|
||||
`defaultValuesFromRecord` (`src/lib/utils/zod-from-metadata.ts`) builds the `defaultValues` object for `useForm`:
|
||||
|
||||
```tsx
|
||||
export function defaultValuesFromRecord(
|
||||
tableMetaData: QTableMetaData,
|
||||
recordValues: Record<string, unknown>
|
||||
): Record<string, unknown>
|
||||
```
|
||||
|
||||
For each editable, visible field it applies this priority:
|
||||
1. Use the record value (from the API response), with type coercion:
|
||||
- `DATE`, `TIME`, `DATE_TIME` → `String(value)` (ISO-8601 string for the HTML input).
|
||||
- `BOOLEAN` → `Boolean(value)`.
|
||||
- All other types → value as-is.
|
||||
2. Fall back to `field.defaultValue` from metadata.
|
||||
3. Fall back to `false` for `BOOLEAN` fields.
|
||||
4. Fall back to `''` (empty string) for everything else.
|
||||
|
||||
`EntityForm` calls this inside `useMemo` and passes the result to `useForm({ defaultValues })`. When the record changes (e.g., navigating between records), the form is reset via `reset(defaultValuesFromRecord(...))` in a `useEffect`.
|
||||
|
||||
---
|
||||
|
||||
## Client vs server validation
|
||||
|
||||
### Client-side (Zod)
|
||||
|
||||
Zod runs on the client when the user submits the form. React Hook Form calls the Zod resolver, collects all field errors, and populates `formState.errors`. Fields display their error messages inline — `DynamicFormField` renders a `<p role="alert">` with `id="${fieldId}-error"` below each failing input, and the input gains `aria-invalid="true"` and `aria-describedby` pointing at the error element.
|
||||
|
||||
```tsx
|
||||
{fieldError && (
|
||||
<p id={`${fieldId}-error`} className="mt-1 text-sm text-destructive" role="alert">
|
||||
{fieldError.message}
|
||||
</p>
|
||||
)}
|
||||
```
|
||||
|
||||
### Server-side (API)
|
||||
|
||||
The backend can return validation errors after a save attempt. `EntityForm` handles this via TanStack Query mutation `onError` callbacks. Currently, API errors are shown as a top-level error banner rather than per-field:
|
||||
|
||||
```tsx
|
||||
const insertMutation = useMutation({
|
||||
mutationFn: (values) => insertRecord(tableMetaData.name, values),
|
||||
onError: (err: Error) => {
|
||||
toast.error(`Failed to create ${tableMetaData.label}: ${err.message}`)
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
When the API returns structured per-field errors (a map of `{ fieldName: errorMessage }`), map them to React Hook Form using `form.setError`:
|
||||
|
||||
```tsx
|
||||
onError: (err) => {
|
||||
const fieldErrors = parseApiFieldErrors(err)
|
||||
if (fieldErrors) {
|
||||
for (const [fieldName, message] of Object.entries(fieldErrors)) {
|
||||
form.setError(fieldName, { type: 'server', message })
|
||||
}
|
||||
} else {
|
||||
toast.error(err.message)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This makes server validation errors appear in the same inline error elements as client-side Zod errors, giving the user a consistent experience.
|
||||
|
||||
---
|
||||
|
||||
## DynamicFormField dispatch
|
||||
|
||||
`DynamicFormField` (`src/components/forms/DynamicFormField.tsx`) is the component that renders a single field inside a metadata-driven form. It dispatches to a typed input component based on field properties in this priority order:
|
||||
|
||||
1. **`field.possibleValueSourceName` is set** → `<PossibleValueSelect>` (async combobox).
|
||||
2. **`FILE_UPLOAD` adornment or `BLOB` type** → `<FileUploadField>`.
|
||||
3. **`field.type` switch** → one of `TextField`, `NumberField`, `BooleanField`, `DateField`, `DateTimeField`, `PasswordField`, or inline `<textarea>` (TEXT), `<RichTextField>` (HTML), `<input type="time">` (TIME).
|
||||
4. **Default case** → `<TextField>` (treats unknown types as plain text).
|
||||
|
||||
Hidden fields (`isHidden: true`) and non-editable fields return `null` immediately.
|
||||
|
||||
### Dirty field indicator
|
||||
|
||||
When a field's value has changed from its default, `DynamicFormField` wraps the input in a left-border accent:
|
||||
|
||||
```tsx
|
||||
function DirtyWrapper({ isDirty, children }) {
|
||||
if (!isDirty) return <>{children}</>
|
||||
return (
|
||||
<div className="border-l-2 border-primary pl-2">
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
The `isDirty` prop comes from `formState.dirtyFields[fieldName]` in `EntityForm`, which passes the full `dirtyFields` map to `DynamicForm`, which passes each field's individual dirty state to `DynamicFormField`.
|
||||
|
||||
---
|
||||
|
||||
## Adding custom Zod refinements
|
||||
|
||||
The generated schemas cover all constraints that can be expressed in QQQ field metadata. When you need a validation rule that metadata cannot express (for example, cross-field validation or a format check), add a `.superRefine()` or `.refine()` on the schema returned by `zodSchemaFromTableMetadata`:
|
||||
|
||||
```tsx
|
||||
// In the component that builds the form
|
||||
const baseSchema = zodSchemaFromTableMetadata(tableMetaData)
|
||||
|
||||
const schema = baseSchema.superRefine((values, ctx) => {
|
||||
// Cross-field: end date must be after start date
|
||||
if (values.startDate && values.endDate && values.endDate < values.startDate) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['endDate'],
|
||||
message: 'End date must be after start date',
|
||||
})
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Prefer `.superRefine()` over `.refine()` when you need to report the error on a specific field path rather than at the root of the object, so React Hook Form can display it inline next to the correct input.
|
||||
|
||||
---
|
||||
|
||||
## Complete example
|
||||
|
||||
### Field metadata (from the API)
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "quantity",
|
||||
"label": "Quantity",
|
||||
"type": "INTEGER",
|
||||
"isRequired": true,
|
||||
"isEditable": true,
|
||||
"isHidden": false,
|
||||
"minValue": 1,
|
||||
"maxValue": 9999
|
||||
}
|
||||
```
|
||||
|
||||
### Generated Zod schema for this field
|
||||
|
||||
`zodFieldFromMetadata` produces:
|
||||
|
||||
```ts
|
||||
z.coerce.number()
|
||||
.int("Quantity must be a whole number")
|
||||
.min(1, "Quantity must be at least 1")
|
||||
.max(9999, "Quantity must be at most 9999")
|
||||
```
|
||||
|
||||
Because `isRequired` is `true`, this is the final schema (no `z.literal('')` union wrapping). An empty `<input type="number">` submits `""`, `z.coerce.number()` coerces that to `NaN`, and `NaN` fails the `.int()` check — so the required constraint is enforced implicitly.
|
||||
|
||||
### Full table schema (example)
|
||||
|
||||
```ts
|
||||
const schema = zodSchemaFromTableMetadata(orderTableMetaData)
|
||||
// Produces:
|
||||
z.object({
|
||||
customerId: z.string().min(1, 'Customer is required'), // STRING, required
|
||||
quantity: z.coerce.number().int(...).min(1).max(9999), // INTEGER, required
|
||||
notes: z.string().optional(), // TEXT, optional
|
||||
shipDate: z.string().optional(), // DATE, optional
|
||||
isRush: z.boolean().optional(), // BOOLEAN, always optional
|
||||
})
|
||||
```
|
||||
@@ -0,0 +1,61 @@
|
||||
# Next UI issue tracking
|
||||
|
||||
GitHub issues are the source of truth for scope, current status, remaining work and release decisions. Start at [QRun-IO/qqq#713](https://github.com/QRun-IO/qqq/issues/713), which links the release baseline, known failures, visual review, external-provider checks and existing feature tickets.
|
||||
|
||||
## Where work lives
|
||||
|
||||
| Area | Authoritative issue |
|
||||
|---|---|
|
||||
| Roadmap and complete issue index | [qqq#713](https://github.com/QRun-IO/qqq/issues/713) |
|
||||
| Feature acceptance and default adoption | [qqq#649](https://github.com/QRun-IO/qqq/issues/649) |
|
||||
| Behavior and metadata compatibility | [qqq#714](https://github.com/QRun-IO/qqq/issues/714) |
|
||||
| Original Next visual review and owner approval | [qqq#711](https://github.com/QRun-IO/qqq/issues/711) |
|
||||
| Browser failures and incomplete run evidence | [qqq#985](https://github.com/QRun-IO/qqq/issues/985) |
|
||||
| Real application soak and final release hold | [qqq#712](https://github.com/QRun-IO/qqq/issues/712) |
|
||||
| Cross-application launch/restart acceptance | [qqq#650](https://github.com/QRun-IO/qqq/issues/650) |
|
||||
| ESB integration and acceptance | [qqq#986](https://github.com/QRun-IO/qqq/issues/986) |
|
||||
| Paired runtime provenance and CI handoff | [qqq#905](https://github.com/QRun-IO/qqq/issues/905) |
|
||||
|
||||
Create **all Next UI tickets in [QRun-IO/qqq](https://github.com/QRun-IO/qqq/issues)** and add them to the [QQQ Roadmap, Project 12](https://github.com/orgs/QRun-IO/projects/12). Use existing issues for the same requirement or failure, and link each focused ticket to #713 and its feature or investigation parent. Frontend issues #12 and #10 were transferred to central issues #985 and #986; use the central identifiers and preserve their report history.
|
||||
|
||||
## Required ticket fields
|
||||
|
||||
Every new ticket needs these fields on the issue and its Project 12 item; complete them before work advances to **Ready**:
|
||||
|
||||
| Field | Required value |
|
||||
|---|---|
|
||||
| Issue Type | Native issue: Bug, Feature, Task or Epic. Mirror it in the Project field, using Planning Epic for Epic; Improvement is a Project-only category. |
|
||||
| Assignees | The person accountable for completing or coordinating the ticket |
|
||||
| Milestone | The agreed delivery milestone; keep it consistent with the target version |
|
||||
| Priority | Critical, High, Medium or Low, with an impact-based rationale in the issue |
|
||||
| Component | The owning implementation component: `qqq-frontend-next` for Next source, or the actual backend/sample component for a dependency. |
|
||||
| Target Version | The agreed version of the owning component or an explicit pending release decision. A backend dependency in the Next UI 1.0 milestone does not imply a backend 1.0 release. Never imply delivery in an already published RC. |
|
||||
| Status | The current workflow state below |
|
||||
|
||||
Record unresolved ownership or release decisions explicitly while a ticket remains in **Backlog**. Do not invent dates, estimates or a new release version to fill a field. A browser assertion failure alone does not establish high product severity.
|
||||
|
||||
## Workflow and evidence
|
||||
|
||||
- **Backlog / Ready:** capture the expected behavior, scope, acceptance criteria, required fields and dependencies; Ready means actionable work with an owner.
|
||||
- **In progress:** link the active branch or work and record reproduction, confirmed cause, implementation progress and remaining blockers.
|
||||
- **In review:** link the reviewable PR, source commit and verification evidence; keep unmet acceptance criteria visible. A local fix or passing subset does not imply merge or release.
|
||||
- **Done:** close only after the accepted fix is integrated and every acceptance criterion is verified, or an explicit approved disposition is recorded. Link the merged PR/commit and evidence. Identify whether the change is unreleased or delivered, with the version and release link when published.
|
||||
|
||||
## Updating an issue
|
||||
|
||||
- Put current status and remaining acceptance criteria at the top. Preserve the original report as history.
|
||||
- Record expected/actual behavior, reproduction, environment, source commit, run and artifact evidence. State whether the cause is established.
|
||||
- Separate implementation evidence, passing scoped tests, owner visual approval and final release approval. They are distinct requirements.
|
||||
- Resolve all original acceptance criteria before closing, or document an explicit approved disposition. A later passing run alone does not explain an intermittent failure.
|
||||
- Keep external-provider exclusions visible. Controlled provider fixtures do not prove a real Auth0 tenant, Google account or QuickSight dashboard.
|
||||
- Update issue links after a fix or new finding. Repository plans, session notes, release notes and test/visual ledgers support the issues; they are not competing live backlogs.
|
||||
|
||||
## Release and design constraints
|
||||
|
||||
Preserve the original Next design at `e42ad2b2bcdc76311e13002a22dd70e3ec437192`. Material Dashboard is the behavior and metadata compatibility reference. Prioritize primary-browser functional depth and disclose secondary-browser failures.
|
||||
|
||||
[RC9](releases/1.0.0-RC.9.md) is the published prerelease for application testing. Its immutable-tag acceptance run has known Firefox/tablet failures; consult the release notes and #985 for the exact evidence. The browser fixes in [PR #18](https://github.com/QRun-IO/qqq-frontend-next/pull/18) are [unreleased](releases/unreleased.md).
|
||||
|
||||
Every release and changelog entry must link its central QQQ issues and distinguish product changes, test/infrastructure corrections, verification and unresolved work. A published RC's tag and artifacts remain immutable; subsequent fixes require a separately authorized version and release. Update tickets and Project 12 with the actual delivery version and release link after publication.
|
||||
|
||||
Final 1.0 remains held for the owner's requested application soak and renewed, explicit final approval ([#712](https://github.com/QRun-IO/qqq/issues/712)). Passing CI, an issue marked Done or elapsed time does not release that hold. This project does not authorize releasing QQQ 4.1.
|
||||
@@ -0,0 +1,28 @@
|
||||
# PLAN: Next UI complete feature acceptance and default adoption (QRun-IO/qqq#649)
|
||||
|
||||
## Goal
|
||||
Make Next the fully tested, working default QQQ admin UI: every inventoried supported
|
||||
feature has executable acceptance coverage that passes against the real sample backend and
|
||||
a production Next build, and every documented default launch path starts Next.
|
||||
|
||||
## Approach
|
||||
1. Inventory UI-facing features from source (sample metadata, backend enums/routes, Material
|
||||
behavior) into `docs/acceptance/feature-matrix.md`; each row names its fixture, scenarios,
|
||||
test IDs, result and issue links.
|
||||
2. Add a Playwright acceptance project (`tests/acceptance/`) that boots the QRun-owned sample
|
||||
JAR on an ephemeral loopback port and serves `next build` output with `next start`, using
|
||||
same-origin forwarding. Tests carry matrix IDs; `scripts/acceptance-gate.mjs` fails when a
|
||||
required row has no passing test or a test is skipped/flaky.
|
||||
3. Fix demonstrated defects with regression tests (#550, #645, #538, #539, #541, others found),
|
||||
filing or reusing a QQQ issue for each.
|
||||
4. Update default entry points (quickstart, sample README, root README, website, wiki,
|
||||
starter/template) while keeping Material selectable.
|
||||
5. Run all gates, publish versioned artifacts through normal workflows, record evidence.
|
||||
|
||||
## Branches
|
||||
- `qqq-frontend-next`: `feature/GH-649-next-acceptance` (from `main`)
|
||||
- `qqq`: worktree `../qqq-649`, branch `feature/GH-649-next-default` (from `origin/develop`)
|
||||
|
||||
## Open questions (for James)
|
||||
- Commits, pushes, PRs, image/tag publication and dependency changes need explicit approval
|
||||
per repository rules; work proceeds locally until then.
|
||||
@@ -0,0 +1,22 @@
|
||||
# Plan: API playground contrast (#724)
|
||||
|
||||
## Reproduction and scope
|
||||
|
||||
Fresh, fingerprinted desktop captures preserve the original Next composition. RapiDoc's Expand all text in both the original design and RC6 renders RGB(29, 78, 216) on RGB(42, 43, 44), a contrast ratio of 2.12:1 at 16px normal weight. This inherited defect fails the 4.5:1 requirement. Browser regression checks also found light-mode contact-link contrast of 2.36:1 and authentication-status contrast of 3.00:1.
|
||||
|
||||
## Implementation and verification
|
||||
|
||||
- [x] Retain failing browser contrast checks inside RapiDoc's shadow DOM.
|
||||
- [x] Apply existing dashboard primary-text and error tokens at the component boundary. Pair dark accent backgrounds with inverse button text. Preserve layout and dependencies.
|
||||
- [x] Verify normal and hovered controls in light/dark Chromium desktop and mobile, plus live Preferences theme switching.
|
||||
- [x] Run types, lint, production export, all 1,901 unit tests and existing bundle budgets.
|
||||
- [x] Directly inspect four fresh screenshots after color transitions settle.
|
||||
- [x] Independent review approved with no remaining findings; prepare signed isolated commit.
|
||||
|
||||
Evidence: `test-results/visual-review/api-playground-contrast/` in the original checkout. Final focused report: four passed, zero failures, skips, flaky outcomes or report errors. These are targeted checks, not a complete acceptance gate. Initial toggle verification incorrectly asserted an HTML attribute: React 19 sets RapiDoc's non-reflected Lit property. A subsequent audit sampled active color transitions. Both runs are retained; the corrected test awaits the property, Lit render and actual shadow-root animations without fixed sleeps.
|
||||
|
||||
## Remaining limits
|
||||
|
||||
This proves the fixture's visible playground content, hover states and theme switching, not all expanded endpoint/authentication states or arbitrary custom palettes. The existing narrow-screen inner horizontal clipping remains open; this color correction does not redesign the playground.
|
||||
|
||||
This branch is based on RC6 source `7ddf45f` and excludes the separate TIME fix `276b65b`. Neither follow-up is part of the immutable RC6 tag. Final 1.0 remains on hold.
|
||||
@@ -0,0 +1,31 @@
|
||||
# PLAN: Next UI 1.0 query filter parity
|
||||
|
||||
## Goal
|
||||
|
||||
Complete the supported Material Dashboard query-filter workflows in QRun-IO/qqq#715 and #718, and prove them with real-backend acceptance rows.
|
||||
|
||||
## Approach
|
||||
|
||||
Build on the preserved wave 2 worktree. Keep one `QQueryFilter` as the source of truth for basic and advanced modes, with the existing `useRecordQuery` state and saved-view serialization. Share the existing metadata-driven operator and value controls between quick-filter chips and the advanced builder. Use the v1 metadata settings from backend PR #787 for weekday options and help.
|
||||
|
||||
## Files affected
|
||||
|
||||
- `src/components/query/RecordQuery.tsx`, `RecordQueryToolbar.tsx`: mode switch, quick filters, sort, clear confirmation, advanced preview.
|
||||
- `src/components/query/QuickFilterBar.tsx`: basic-mode chips, field picker, operator/value editor and accessible clear/remove actions.
|
||||
- `src/components/query/FilterBuilder.tsx` and its extracted helpers: complete advanced operator and value behavior from #718.
|
||||
- `src/lib/utils/quick-filter-utils.ts`, `filter-utils.ts`, `saved-view-utils.ts`, `src/lib/hooks/use-record-query.ts`: metadata defaults, criteria edits, mode reconciliation, persistence and view round-trips.
|
||||
- `tests/acceptance/matrix/query.json` and `tests/acceptance/specs/query/`: real-backend evidence for every applicable #715/#718 behavior.
|
||||
|
||||
## Steps
|
||||
|
||||
1. [x] Add meaningful unit tests for quick-filter defaults, complex-filter rejection, edits, weekday operators, date expressions and saved-view round-trips; finish any missing utility behavior.
|
||||
2. [x] Wire instance filter settings and add the basic/advanced switch with a reason tooltip when basic cannot represent the current filter.
|
||||
3. [x] Build and test basic quick-filter chips, add-field menu, operator/value editing, clear/remove behavior and advanced preview.
|
||||
4. [x] Add sort picker, clear-all confirmation that preserves sort, and per-row validation; cover keyboard and touch behavior.
|
||||
5. [ ] Add real-backend acceptance rows and tests, then run typecheck, lint, unit, static-export budget and the five-project browser gate after integration.
|
||||
|
||||
## Constraints
|
||||
|
||||
- Render field labels from metadata and preserve readable join and virtual-field behavior.
|
||||
- Keep the filters and saved views interoperable with Material Dashboard JSON.
|
||||
- Preserve unrelated wave 2 worktrees. Do not release QQQ 4.1.
|
||||
@@ -0,0 +1,11 @@
|
||||
# Script completion compatibility
|
||||
|
||||
Goal: close the autocomplete gap in the parity ledger while retaining the original Next textarea editor and its keyboard accessibility. Authorized by the active Next UI 1.0 completion goal.
|
||||
|
||||
- Preserve syntax overlay, split panes, per-file language, undo, read-only behavior and Tab escape.
|
||||
- Opt script editors into a caret-adjacent suggestion list: QQQ API/logger methods, current-file identifiers, and highlighter language keywords. No new dependency or backend contract.
|
||||
- Typing a prefix or Ctrl+Space opens suggestions; arrows select, Enter/Tab insert, Escape dismisses; pointer selection retains textarea focus. Composition input must not be intercepted.
|
||||
- Replace the current token at the caret, preserve surrounding text, and avoid duplicating an existing call parenthesis.
|
||||
- Verify unit regressions, real-backend script edit/test/save/undo behavior, focus/keyboard and visual states. Update guides and ledger only to the extent proven.
|
||||
|
||||
Verified: baseline Chromium 95847c2 passed 556 tests; backend PR #913 merged. Autocomplete passes 1,840 unit tests, types, affected lint, production export and bundle budgets; 15 Chromium developer/script workflows and 10 focused cases across all five profiles. Both review findings fixed after failing regressions. A native textarea probe explained WebKit undo grouping; completion now has a loaded-revision undo/redo test without changing native history behavior. Light/dark screenshots inspected; whole-family visual review remains open.
|
||||
@@ -0,0 +1,17 @@
|
||||
# Sidebar icon label assertion (#728)
|
||||
|
||||
## Evidence
|
||||
|
||||
RC6 candidate WebKit failed NAV-002 because raw link text included decorative legacy icon codepoints. The same assertion was reproduced against the public RC6 JAR after explicitly awaiting the fixture's legacy glyph. This is a test assertion defect: accessible link names should match metadata labels, while decorative SVG text remains hidden from assistive technology.
|
||||
|
||||
## Correction
|
||||
|
||||
Await the legacy glyph, then assert exact link count and each accessible label in backend order. Check that rendered legacy SVGs remain `aria-hidden`. Retain nested-app, parent expansion and child label assertions. No product code or release artifact changes.
|
||||
|
||||
## Verification
|
||||
|
||||
Retain red and focused Chromium/WebKit/mobile reports under `test-results/visual-review/sidebar-icon-labels/` in the original checkout. The focused check exercises the actual public RC6 JAR with backend `e0d57dd`. Targeted ESLint and diff checks are required; independent review precedes integration. Original CI failure remains recorded, not erased by a later focused pass.
|
||||
|
||||
Focused result: three passed, zero failures, skips, flaky outcomes or report errors. Targeted ESLint and diff checks passed.
|
||||
|
||||
Independent review approved with no findings. This change verifies navigation semantics; glyph appearance remains covered by the separate legacy-icon workflows.
|
||||
@@ -0,0 +1,10 @@
|
||||
# Plan: TIME block seconds compatibility (#728)
|
||||
|
||||
Preserve Next layout while making standalone and process TIME inputs accept and submit seconds, matching the shared form editor and QQQ TIME values.
|
||||
|
||||
1. Reproduce native step validation failure with standalone and process tests; retain a regular form control.
|
||||
2. Apply the existing one-second input step to both plain block paths.
|
||||
3. Verify edited/seeded values through actual Chromium workflows, including backend process readback; run affected tests and relevant quality gates.
|
||||
4. Keep this follow-up separate from the unchanged RC6 candidate while its hosted CI runs.
|
||||
|
||||
Verification: two focused component regressions failed before the fix (regular form control passed); three real-backend Chromium scenarios failed at native step assertions. After the two-line correction, all 66 affected component tests and eight desktop/mobile backend workflows passed, including independent SQL checks. Full unit suite, type/lint, production build and budgets passed. This is isolated follow-up work, not part of RC6 candidate `7ddf45f`.
|
||||
@@ -0,0 +1,332 @@
|
||||
# State Management Guide
|
||||
|
||||
This document explains when to use each state container in the QQQ Frontend Next codebase. Each container has a specific purpose — choosing the wrong one leads to stale data, lost state on navigation, or unnecessary complexity.
|
||||
|
||||
## Decision Matrix
|
||||
|
||||
| If the state is… | Use… |
|
||||
|---|---|
|
||||
| Data fetched from the server (records, metadata, widgets, processes) | TanStack Query |
|
||||
| Form field values and validation errors | React Hook Form |
|
||||
| Filters, pagination, sort order — things a user should be able to bookmark or share | URL search params |
|
||||
| User preferences that survive a session (density, column widths, saved views) | `useLocalStorage` |
|
||||
| Application-wide UI state (modal stack, page header, accent color, branding) | QContext |
|
||||
| Ephemeral UI state local to one component (dropdown open, hover, toggle) | `useState` / `useReducer` |
|
||||
|
||||
---
|
||||
|
||||
## TanStack Query — Server State
|
||||
|
||||
**Rule:** Never use `useState` for data that comes from the server. TanStack Query owns all server state.
|
||||
|
||||
The query client is configured in `src/lib/query-client.ts` with:
|
||||
- 5-minute stale time — metadata and record lists stay fresh without excessive refetching
|
||||
- 10-minute GC time — recently visited pages are snappy when navigating back
|
||||
- Smart retry: never retries 4xx errors; retries 5xx and network failures up to 3 times with exponential back-off
|
||||
- Global `QueryCache.onError` and `MutationCache.onError` — most errors are automatically toasted without per-call handling
|
||||
|
||||
### Query key factory
|
||||
|
||||
All query keys are defined in `queryKeys` in `src/lib/query-client.ts`. Always use the factory, never inline string arrays.
|
||||
|
||||
```typescript
|
||||
import { queryKeys } from '@/lib/query-client'
|
||||
|
||||
// Fetch a single record
|
||||
useQuery({
|
||||
queryKey: queryKeys.tableRecord('order', recordId),
|
||||
queryFn: () => getRecord('order', recordId),
|
||||
})
|
||||
|
||||
// Invalidate all records for a table after a mutation
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.tableRecords('order') })
|
||||
|
||||
// Invalidate everything (e.g. after logout)
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.all() })
|
||||
```
|
||||
|
||||
### Key hierarchy (broadest → narrowest)
|
||||
|
||||
```
|
||||
['qqq']
|
||||
['qqq', 'metadata', 'all']
|
||||
['qqq', 'metadata', 'table', tableName]
|
||||
['qqq', 'records', tableName]
|
||||
['qqq', 'records', tableName, recordId]
|
||||
['qqq', 'records', tableName, 'count', filterHash]
|
||||
['qqq', 'processes', processName, processUUID, 'status', jobUUID]
|
||||
['qqq', 'widgets', widgetName, params]
|
||||
['qqq', 'possibleValues', 'table', tableName, fieldName, searchTerm]
|
||||
['qqq', 'search', searchTerm]
|
||||
```
|
||||
|
||||
Broad invalidations cascade down. For example, `queryKeys.tableRecords('order')` invalidates both the list query and all individual record queries for that table.
|
||||
|
||||
### What belongs in TanStack Query
|
||||
|
||||
- Record lists and counts
|
||||
- Single record fetches
|
||||
- All metadata (tables, processes, apps)
|
||||
- Widget data
|
||||
- Process status polling
|
||||
- Possible values / enum lookups
|
||||
- Global search results
|
||||
- Auth session metadata
|
||||
|
||||
### What does NOT belong
|
||||
|
||||
Do not use TanStack Query for UI state (modal open/close, form field values, user preferences). Those belong in the containers described below.
|
||||
|
||||
---
|
||||
|
||||
## React Hook Form — Form Field State
|
||||
|
||||
**Rule:** React Hook Form owns field values and validation state for forms only. Do not use it for non-form UI, and do not reach into the form state from outside the form.
|
||||
|
||||
Forms are built with Zod schemas derived from field metadata. The schema is constructed by `zodFromMetadata()` in `src/lib/utils/zod-from-metadata.ts`.
|
||||
|
||||
```typescript
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { zodFromMetadata } from '@/lib/utils/zod-from-metadata'
|
||||
|
||||
function EntityForm({ tableMetaData, record }: Props) {
|
||||
const schema = zodFromMetadata(tableMetaData)
|
||||
const form = useForm({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: record ?? {},
|
||||
})
|
||||
|
||||
// Map a 400 validation error back to individual fields
|
||||
const onSubmit = async (data: FieldValues) => {
|
||||
try {
|
||||
await saveRecord('order', data)
|
||||
} catch (error) {
|
||||
if (getErrorStatusCode(error) === 400) {
|
||||
const fieldErrors = extractFieldErrors(error)
|
||||
for (const [field, message] of Object.entries(fieldErrors)) {
|
||||
form.setError(field, { message })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### What belongs in React Hook Form
|
||||
|
||||
- All form field values
|
||||
- Validation errors (field-level and form-level)
|
||||
- Dirty/touched state
|
||||
- Submit handling
|
||||
|
||||
### What does NOT belong
|
||||
|
||||
Do not use React Hook Form for filter criteria in the query page — those live in URL params and reducer state managed by `useRecordQuery`.
|
||||
|
||||
---
|
||||
|
||||
## URL Search Params — Shareable State
|
||||
|
||||
**Rule:** Any state a user should be able to bookmark, share, or restore after a page refresh goes in the URL.
|
||||
|
||||
Use Next.js `useSearchParams` and `useRouter` to read and write URL state. This is the right home for:
|
||||
- Active filters and quick search term
|
||||
- Current page and page size
|
||||
- Sort column and direction
|
||||
|
||||
The `useRecordQuery` hook syncs filter/pagination state to and from the URL automatically. Components should read these values from the hook, not from `useSearchParams` directly.
|
||||
|
||||
```typescript
|
||||
import { useSearchParams, useRouter } from 'next/navigation'
|
||||
|
||||
// Reading
|
||||
const searchParams = useSearchParams()
|
||||
const page = Number(searchParams.get('page') ?? '1')
|
||||
const sort = searchParams.get('sort') ?? ''
|
||||
|
||||
// Writing — replace so the back button doesn't accumulate filter changes
|
||||
const router = useRouter()
|
||||
const params = new URLSearchParams(searchParams)
|
||||
params.set('page', String(nextPage))
|
||||
router.replace(`?${params.toString()}`)
|
||||
```
|
||||
|
||||
### Key naming conventions in URL params
|
||||
|
||||
| Param | Values | Purpose |
|
||||
|---|---|---|
|
||||
| `page` | integer | Current page (1-based) |
|
||||
| `pageSize` | 10, 25, 50, 100 | Rows per page |
|
||||
| `sort` | `fieldName:asc` or `fieldName:desc` | Active sort |
|
||||
| `q` | string | Quick search term |
|
||||
| `filter` | base64-encoded JSON | Serialized filter criteria |
|
||||
|
||||
### What belongs in URL params
|
||||
|
||||
- Filters, quick search
|
||||
- Pagination (page, page size)
|
||||
- Sort order
|
||||
- Any state that makes a link shareable
|
||||
|
||||
### What does NOT belong
|
||||
|
||||
Do not put ephemeral UI state (modal open, panel open) in the URL. Do not put server data in the URL.
|
||||
|
||||
---
|
||||
|
||||
## localStorage — User Preferences
|
||||
|
||||
**Rule:** Preferences that should survive across sessions and browser restarts go in localStorage via the `useLocalStorage` hook.
|
||||
|
||||
The hook lives at `src/lib/hooks/use-local-storage.ts`. It is SSR-safe (returns `initialValue` on the server), cross-tab synced (listens to `storage` events), and has a stable setter that accepts a value or an updater function matching the `useState` API.
|
||||
|
||||
```typescript
|
||||
import { useLocalStorage } from '@/lib/hooks/use-local-storage'
|
||||
|
||||
const [density, setDensity, removeDensity] = useLocalStorage<'compact' | 'standard' | 'comfortable'>(
|
||||
'qqq-order-density',
|
||||
'standard'
|
||||
)
|
||||
```
|
||||
|
||||
### Higher-level hooks built on `useLocalStorage`
|
||||
|
||||
| Hook | Key | Contents |
|
||||
|---|---|---|
|
||||
| `useUserPreferences` | `qqq-user-preferences` | Default page size, density, view mode |
|
||||
| `useColumnConfig` | `qqq-{tableName}-columns`, `qqq-{tableName}-column-order`, `qqq-{tableName}-column-widths` | Column visibility, order, pixel widths |
|
||||
| `useSavedViews` | `qqq-{tableName}-saved-views` | Named filter snapshots |
|
||||
|
||||
### Key naming convention
|
||||
|
||||
```
|
||||
qqq-{tableName}-{purpose} # table-scoped
|
||||
qqq-user-preferences # global user preferences
|
||||
qqq:{scope}:{purpose} # alternative colon-separated form
|
||||
```
|
||||
|
||||
Always prefix with `qqq-` to avoid collisions with other apps that might share the same origin.
|
||||
|
||||
### What belongs in localStorage
|
||||
|
||||
- Table density (compact / standard / comfortable)
|
||||
- Column visibility and order per table
|
||||
- Column pixel widths per table
|
||||
- Saved views (named filter snapshots)
|
||||
- Global user preferences (default page size, default view mode)
|
||||
- Dismissed banners or one-time notices
|
||||
|
||||
### What does NOT belong
|
||||
|
||||
Do not store server data in localStorage — use TanStack Query's cache. Do not store state that should be shareable — use URL params.
|
||||
|
||||
---
|
||||
|
||||
## QContext — Application-Wide UI State
|
||||
|
||||
**Rule:** Use QContext for state that must be accessible anywhere in the authenticated app tree and does not fit a more specific container.
|
||||
|
||||
The provider is `QContextProvider` in `src/lib/context/q-context.tsx`. Access it with the `useQContext()` hook. The hook throws at runtime if called outside a provider, which catches integration mistakes early.
|
||||
|
||||
```typescript
|
||||
import { useQContext } from '@/lib/context/q-context'
|
||||
|
||||
function RecordQueryPage({ tableMetaData }: Props) {
|
||||
const { setPageHeader, setTableMetaData } = useQContext()
|
||||
|
||||
useEffect(() => {
|
||||
setPageHeader(tableMetaData.label)
|
||||
setTableMetaData(tableMetaData)
|
||||
}, [tableMetaData])
|
||||
}
|
||||
```
|
||||
|
||||
### What QContext contains
|
||||
|
||||
| Field | Type | Purpose |
|
||||
|---|---|---|
|
||||
| `pageHeader` | `string \| ReactNode` | Content rendered in the page header area |
|
||||
| `accentColor` / `accentColorLight` | `string` | Theme accent colors (hex) |
|
||||
| `tableMetaData` | `QTableMetaData \| null` | Metadata for the currently viewed table |
|
||||
| `tableProcesses` | `QProcessMetaData[] \| null` | Processes for the current table |
|
||||
| `dotMenuOpen` | `boolean` | Whether the ellipsis action menu is open |
|
||||
| `modalStack` | `string[]` | Ordered list of open modal identifiers for keyboard nav |
|
||||
| `pushModalOnStack(id)` | function | Called when a modal opens |
|
||||
| `popModalOffStack(id)` | function | Called when a modal closes; warns if id is not on top |
|
||||
| `clearModalStack()` | function | Clears all modal identifiers (e.g. on navigation) |
|
||||
| `keyboardHelpOpen` | `boolean` | Keyboard shortcut overlay visibility |
|
||||
| `userId` | `string \| undefined` | Current authenticated user identifier |
|
||||
| `branding` | `QBrandingMetaData \| null` | Company name, app name, URLs from backend |
|
||||
| `pathToLabelMap` | `Record<string, string>` | URL path segment → human-readable breadcrumb label |
|
||||
|
||||
### Modal stack usage
|
||||
|
||||
The modal stack ensures that `Escape` closes only the top-most modal and that focus is restored correctly. Every modal or sheet component must push on open and pop on close.
|
||||
|
||||
```typescript
|
||||
const { pushModalOnStack, popModalOffStack, modalStack } = useQContext()
|
||||
const modalId = 'edit-record-modal'
|
||||
|
||||
// On open
|
||||
pushModalOnStack(modalId)
|
||||
|
||||
// On close
|
||||
popModalOffStack(modalId)
|
||||
|
||||
// Check if this modal is on top (for Escape handling)
|
||||
const isTopModal = modalStack[modalStack.length - 1] === modalId
|
||||
```
|
||||
|
||||
### What belongs in QContext
|
||||
|
||||
- Application-wide UI state that has no more specific home
|
||||
- Page header content (set by each page on mount)
|
||||
- Modal stack (for coordinated keyboard navigation)
|
||||
- Accent colors and branding
|
||||
- The currently active table metadata (consumed by header, breadcrumbs, action menus)
|
||||
|
||||
### What does NOT belong
|
||||
|
||||
Do not put server data in QContext (use TanStack Query). Do not put form state in QContext (use React Hook Form). Do not put user preferences in QContext (use localStorage).
|
||||
|
||||
---
|
||||
|
||||
## useState / useReducer — Local Transient State
|
||||
|
||||
**Rule:** Use `useState` or `useReducer` only for state that is local to a single component or a small subtree, ephemeral (lost on unmount is acceptable), and neither server data nor shareable state.
|
||||
|
||||
```typescript
|
||||
// Good — ephemeral open/close that doesn't need to survive navigation
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false)
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
|
||||
// Good — complex multi-field local state with clear action types
|
||||
const [state, dispatch] = useReducer(recordQueryReducer, initialState)
|
||||
```
|
||||
|
||||
### What belongs in useState / useReducer
|
||||
|
||||
- Dialog, dropdown, tooltip open/close
|
||||
- Hover and focus state
|
||||
- Local form wizard step (if not shareable)
|
||||
- Complex local state with many fields that transition together (handled by `useReducer`)
|
||||
|
||||
### What does NOT belong
|
||||
|
||||
- Server data (use TanStack Query)
|
||||
- State that should survive navigation or page refresh (use URL params or localStorage)
|
||||
- State needed by distant parts of the tree (use QContext or lift appropriately)
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
```
|
||||
Server data → TanStack Query (queryKeys factory, automatic toasts on error)
|
||||
Form fields → React Hook Form + Zod
|
||||
Shareable/bookmarkable → URL search params
|
||||
User preferences → useLocalStorage (key: qqq-{tableName}-{purpose})
|
||||
App-wide UI state → QContext (useQContext hook)
|
||||
Local/ephemeral UI → useState / useReducer
|
||||
```
|
||||
@@ -0,0 +1,190 @@
|
||||
# QQQ Frontend Type Glossary
|
||||
|
||||
All types listed here are re-exported from `@/types` (the `src/types/index.ts` barrel).
|
||||
Application code should always import from `@/types`, not from individual module files,
|
||||
so that internal type organisation can change without affecting import paths.
|
||||
|
||||
```typescript
|
||||
import type { QInstance, QTableMetaData, QRecord, QQueryFilter } from '@/types'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 1. App & Auth
|
||||
|
||||
| Type | Source file | Purpose |
|
||||
|------|-------------|---------|
|
||||
| `QInstance` | `metadata.ts` | Top-level descriptor returned by `/metaData/instance`; the single source of truth for all apps, tables, processes, reports, widgets, and branding. **Key fields:** `apps`, `tables`, `processes`, `widgets`, `branding`, `appTree`, `environmentValues` |
|
||||
| `QAuthenticationMetaData` | `metadata.ts` | Describes the auth provider type (`AUTH_0`, `OAUTH2`, `TABLE_BASED`, `FULLY_ANONYMOUS`, `MOCK`), the provider-specific values (client ID, base URL, audience) needed to boot the correct auth flow, and the pre-sign-in `branding` (`QLoginBranding`) the login page shows. |
|
||||
| `QBrandingMetaData` | `metadata.ts` | Controls visual identity: company name, app name, logo URL, accent color, notification banners, and custom CSS injected via `data-qqq-id` selectors. |
|
||||
| `QThemeMetaData` | `metadata.ts` | Backend-supplied Tailwind CSS token overrides (`primaryColor`, `accentColor`, `mode`). Applied on top of the default theme by the theme provider. |
|
||||
| `Banner` | `metadata.ts` | A single notification banner (text, severity `info|warning|error`, optional color, dismissible flag) rendered at the top of the dashboard layout. |
|
||||
|
||||
### QInstance example
|
||||
|
||||
```typescript
|
||||
const instance: QInstance = {
|
||||
apps: { crm: { name: 'crm', label: 'CRM', children: [], ... } },
|
||||
appTree: [{ name: 'crm', label: 'CRM', type: 'APP', ... }],
|
||||
tables: { person: { name: 'person', label: 'People', fields: {}, ... } },
|
||||
processes: {},
|
||||
reports: {},
|
||||
widgets: {},
|
||||
branding: { companyName: 'Acme', companyUrl: 'https://acme.com', appName: 'Admin' },
|
||||
helpContents: {},
|
||||
environmentValues: {},
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Metadata
|
||||
|
||||
| Type | Source file | Purpose |
|
||||
|------|-------------|---------|
|
||||
| `QTableMetaData` | `metadata.ts` | Full descriptor for a QQQ table: fields, sections, join relationships, capabilities, and per-user CRUD permissions. Drives the entire record query, view, create, and edit experience. **Key fields:** `name`, `label`, `primaryKeyField`, `fields`, `sections`, `capabilities`, `readPermission`, `insertPermission`, `editPermission`, `deletePermission` |
|
||||
| `QFieldMetaData` | `metadata.ts` | Descriptor for a single table column: data type, required/editable/hidden flags, max length, numeric bounds, possible-value source, display format, and adornments. **Key fields:** `name`, `label`, `type`, `isRequired`, `isEditable`, `isHidden`, `possibleValueSourceName`, `adornments` |
|
||||
| `QProcessMetaData` | `metadata.ts` | Descriptor for a multi-step wizard process: identity, permissions, input record bounds, and the ordered list of `QFrontendStepMetaData` objects to render. |
|
||||
| `QFrontendStepMetaData` | `metadata.ts` | A single step in a process wizard: label, ordered component list, and optional form/view/record-list field arrays used by the step renderer. |
|
||||
| `QFrontendComponent` | `metadata.ts` | A single UI component descriptor within a process step — a `type` (from `QComponentType`) plus arbitrary `values` passed as props to the matching renderer. |
|
||||
| `QAppMetaData` | `metadata.ts` | A top-level app grouping: sidebar navigation children, dashboard widget names, and labeled sections grouping tables, processes, and reports. |
|
||||
| `QAppTreeNode` | `metadata.ts` | One node in the recursive sidebar navigation tree; the `type` field (`TABLE`, `PROCESS`, `REPORT`, `APP`) determines the route. |
|
||||
| `QAppSection` | `metadata.ts` | A labeled card group on an app dashboard page, listing the table, process, and report names belonging to that group. |
|
||||
| `QWidgetMetaData` | `metadata.ts` | Dashboard widget descriptor: renderer type string, permissions, grid column span, optional toolbar dropdowns, reload/export button flags, and help content. |
|
||||
| `QTableSection` | `metadata.ts` | A collapsible card section on record view/edit pages: field names to display, optional embedded widget name, display tier, and grid column span. |
|
||||
| `QExposedJoin` | `metadata.ts` | A join relationship made available for query filters and record views: cardinality (`isMany`), joined table metadata, and the path of `QJoinMetaData` hops. |
|
||||
| `FieldAdornment` | `metadata.ts` | Discriminated union of all adornment shapes (`LINK`, `CHIP`, `TOOLTIP`, `ERROR`, `FILE_DOWNLOAD`, `SIZE`, `REVEAL`, `CODE_EDITOR`, `RENDER_HTML`, `FILE_UPLOAD`). Narrow with `Extract<FieldAdornment, { type: 'LINK' }>`. |
|
||||
|
||||
### QFieldMetaData example
|
||||
|
||||
```typescript
|
||||
const field: QFieldMetaData = {
|
||||
name: 'firstName',
|
||||
label: 'First Name',
|
||||
type: 'STRING',
|
||||
isRequired: true,
|
||||
isEditable: true,
|
||||
isHidden: false,
|
||||
isHeavy: false,
|
||||
maxLength: 100,
|
||||
adornments: [],
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Records & Queries
|
||||
|
||||
| Type | Source file | Purpose |
|
||||
|------|-------------|---------|
|
||||
| `QRecord` | `records.ts` | A single backend record returned by the tables API. `values` holds raw typed data; `displayValues` holds pre-formatted strings ready for rendering. May include `associatedRecords` for joined/child data, and `errors`/`warnings` for bulk operations. |
|
||||
| `QPossibleValue` | `records.ts` | A single option from a possible-value source, used to populate autocomplete and select inputs. `id` is the stored value; `label` is the user-visible text. |
|
||||
| `QAuditRecord` | `records.ts` | An audit log entry for a record: who changed it, when, what action (`INSERT`/`UPDATE`/`DELETE`), and the list of `QAuditFieldChange` field-level diffs. |
|
||||
| `QQueryFilter` | `query.ts` | Root filter object sent to `/tables/{name}/query` and `/tables/{name}/count`. Combines `criteria` with `AND`/`OR`, supports nested `subFilters`, and carries `skip`/`limit` for pagination. |
|
||||
| `QFilterCriteria` | `query.ts` | A single field-level predicate: a `fieldName`, a `QCriteriaOperator`, and a `values` array that may contain literals or dynamic expressions (`NowExpression`, `NowWithOffsetExpression`, etc.). |
|
||||
| `QFilterOrderBy` | `query.ts` | A sort clause: `fieldName` and `isAscending`. Multiple entries form a compound sort. |
|
||||
| `QueryJoin` | `query.ts` | A join instruction sent alongside a `QQueryFilter` when fields from a related table are needed in query results. Specifies `joinTable`, join type (`INNER`/`LEFT`/etc.), and optional alias. |
|
||||
| `NowWithOffsetExpression` | `query.ts` | A dynamic filter value that resolves to "now minus/plus N days/weeks/months/years" at query time, enabling "last 7 days" style saved filters. |
|
||||
|
||||
### QQueryFilter example
|
||||
|
||||
```typescript
|
||||
const filter: QQueryFilter = {
|
||||
criteria: [
|
||||
{ fieldName: 'status', operator: 'EQUALS', values: ['ACTIVE'] },
|
||||
{ fieldName: 'createdDate', operator: 'GREATER_THAN', values: [{ type: 'NOW_WITH_OFFSET', offsetValue: 30, offsetUnit: 'DAY', isNegativeOffset: true }] },
|
||||
],
|
||||
booleanOperator: 'AND',
|
||||
orderBys: [{ fieldName: 'lastName', isAscending: true }],
|
||||
skip: 0,
|
||||
limit: 25,
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Processes
|
||||
|
||||
| Type | Source file | Purpose |
|
||||
|------|-------------|---------|
|
||||
| `QJobStarted` | `processes.ts` | Initial response after submitting a process step; provides `processUUID` and `jobUUID` for subsequent status polling. |
|
||||
| `QJobRunning` | `processes.ts` | Intermediate polling response while a job executes asynchronously; provides a `message` and optional `current`/`total` progress counts for the progress bar. |
|
||||
| `QJobComplete` | `processes.ts` | Terminal success response; carries output `values`, optional `nextStep`/`backStep` navigation hints, and any `processMetaDataAdjustment` that modifies remaining wizard steps. |
|
||||
| `QJobError` | `processes.ts` | Terminal failure response; provides a technical `error` string and an optional `userFacingError` message for display in the wizard UI. |
|
||||
| `QJobResponse` | `processes.ts` | Discriminated union of `QJobStarted \| QJobRunning \| QJobComplete \| QJobError`. Narrow by checking for the discriminating property (`jobUUID`, `message`, `values`, or `error`). |
|
||||
| `ProcessMetaDataAdjustment` | `processes.ts` | Runtime modifications to the process wizard applied after a step completes: `addedSteps`, `removedSteps`, and partial `modifiedFields` overrides. |
|
||||
|
||||
### QJobResponse narrowing example
|
||||
|
||||
```typescript
|
||||
function handleJobResponse(response: QJobResponse) {
|
||||
if ('error' in response) {
|
||||
// QJobError
|
||||
showToast(response.userFacingError ?? response.error)
|
||||
} else if ('values' in response) {
|
||||
// QJobComplete
|
||||
advanceToStep(response.nextStep)
|
||||
} else if ('message' in response) {
|
||||
// QJobRunning — update progress bar
|
||||
setProgress({ message: response.message, current: response.current, total: response.total })
|
||||
} else {
|
||||
// QJobStarted — store jobUUID and start polling
|
||||
setJobUUID(response.jobUUID)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Widgets
|
||||
|
||||
| Type | Source file | Purpose |
|
||||
|------|-------------|---------|
|
||||
| `WidgetData` | `widgets.ts` | Base shape for all widget data payloads; the `type` string is the discriminant used to select the correct renderer. |
|
||||
| `ChartWidgetData` | `widgets.ts` | Recharts widget payload: `chartType` (`line`/`bar`/`pie`/`area`), `labels` array, and `datasets` array of `ChartDataset` series. |
|
||||
| `ChartDataset` | `widgets.ts` | A single Recharts data series: human-readable `label`, ordered `data` values aligned with the parent chart's `labels`, and optional `color`. |
|
||||
| `StatisticsWidgetData` | `widgets.ts` | KPI tile payload: headline `title`, `value`, optional `unit`, and optional `trend` object with direction (`up`/`down`/`flat`), numeric magnitude, and period label. |
|
||||
| `HtmlWidgetData` | `widgets.ts` | Raw HTML widget payload; the `html` string **must** be sanitized with DOMPurify before use in `dangerouslySetInnerHTML`. |
|
||||
| `RecordGridWidgetData` | `widgets.ts` | Tabular record list widget: `tableName`, optional `columns` (field name strings) or `fields` (full `QFieldMetaData[]`), `records` array, and optional `totalCount`. |
|
||||
| `BlockData` | `widgets.ts` | Discriminated union of all block element types renderable inside a `BlockWidgetData`: `text`, `big_number`, `up_or_down`, `progress`, `button`, `icon`, `image`, `audio`, `divider`, `input`, `html`. |
|
||||
| `BlockWidgetData` | `widgets.ts` | Ordered heterogeneous block widget: a `blocks` array of `BlockData` variants and an optional `layout` (`vertical`/`horizontal`/`grid`). |
|
||||
|
||||
---
|
||||
|
||||
## 6. Enums
|
||||
|
||||
| Type | Source file | Purpose |
|
||||
|------|-------------|---------|
|
||||
| `QFieldType` | `enums.ts` | String-union of all primitive field data types: `STRING`, `INTEGER`, `LONG`, `DECIMAL`, `BOOLEAN`, `DATE`, `TIME`, `DATE_TIME`, `TEXT`, `HTML`, `PASSWORD`, `BLOB`. Determines which renderer, input component, and Zod validator to use. |
|
||||
| `QCriteriaOperator` | `enums.ts` | String-union of all filter comparison operators used in `QFilterCriteria.operator`: `EQUALS`, `NOT_EQUALS`, `IN`, `NOT_IN`, `CONTAINS`, `STARTS_WITH`, `LESS_THAN`, `GREATER_THAN`, `IS_BLANK`, `IS_NOT_BLANK`, `BETWEEN`, and more. |
|
||||
| `Capability` | `enums.ts` | String-union of backend-declared table capabilities (`TABLE_QUERY`, `TABLE_GET`, `TABLE_COUNT`, `TABLE_INSERT`, `TABLE_UPDATE`, `TABLE_DELETE`) that gate UI elements such as Create, Edit, and Delete buttons. |
|
||||
| `QAppNodeType` | `enums.ts` | String-union identifying the entity kind of a sidebar navigation node: `TABLE`, `PROCESS`, `REPORT`, `APP`. Drives route prefix selection in `QAppTreeNode`. |
|
||||
| `QComponentType` | `enums.ts` | String-union of all UI component types renderable inside a process step: `EDIT_FORM`, `VIEW_FORM`, `RECORD_LIST`, `VALIDATION_REVIEW_SCREEN`, `BULK_LOAD_FILE_MAPPING_FORM`, `PROCESS_SUMMARY_RESULTS`, `HTML`, `WIDGET`, and more. |
|
||||
| `AdornmentType` | `enums.ts` | String-union of all field adornment identifiers (`LINK`, `CHIP`, `SIZE`, `ERROR`, `RENDER_HTML`, `REVEAL`, `CODE_EDITOR`, `FILE_DOWNLOAD`, `FILE_UPLOAD`, `TOOLTIP`). Mirrors the discriminant property of the `FieldAdornment` union for use in switch statements and type-guard utilities. |
|
||||
|
||||
---
|
||||
|
||||
## Quick-Reference Import Cheat Sheet
|
||||
|
||||
```typescript
|
||||
// App & auth
|
||||
import type { QInstance, QAuthenticationMetaData, QBrandingMetaData, Banner } from '@/types'
|
||||
|
||||
// Metadata
|
||||
import type {
|
||||
QTableMetaData, QFieldMetaData, QProcessMetaData,
|
||||
QFrontendStepMetaData, QWidgetMetaData, FieldAdornment,
|
||||
} from '@/types'
|
||||
|
||||
// Records & queries
|
||||
import type { QRecord, QPossibleValue, QQueryFilter, QFilterCriteria } from '@/types'
|
||||
|
||||
// Processes
|
||||
import type { QJobResponse, QJobComplete, QJobError, ProcessMetaDataAdjustment } from '@/types'
|
||||
|
||||
// Widgets
|
||||
import type { WidgetData, ChartWidgetData, StatisticsWidgetData, BlockData } from '@/types'
|
||||
|
||||
// Enums
|
||||
import type { QFieldType, QCriteriaOperator, Capability } from '@/types'
|
||||
```
|
||||
@@ -0,0 +1,339 @@
|
||||
# Browser matrix — real-backend acceptance (QRun-IO/qqq#649, #708)
|
||||
|
||||
> **Current status lives in GitHub:** [Next UI 1.0 roadmap](https://github.com/QRun-IO/qqq/issues/713). This document records scoped evidence and historical checkpoints; older open/closed wording and summary counts do not override current issue status. See [tracking policy](../ISSUE-TRACKING.md). Final 1.0 remains held for owner testing and explicit approval.
|
||||
|
||||
## CI layout
|
||||
|
||||
`plan` and `build` run concurrently. The build produces one static export, dashboard
|
||||
JAR and sample backend; the sample and middleware API JARs plus `out/` are shared
|
||||
with all shards. Each shard has its own backend, one worker and zero retries.
|
||||
The dependency graph is `plan + build → shards → acceptance gate`, with an
|
||||
independent, non-blocking `quarantine` job after `plan + build` in full mode.
|
||||
|
||||
| Trigger | Chromium | Firefox | WebKit | Mobile | Tablet | Selection |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| PR | 2 | — | — | 2 | — | Changed areas plus landing/navigation/CRUD/query/dashboard smoke |
|
||||
| main, `v*` tag, workflow dispatch | 3 | 3 | 4 | 4 | 4 | Entire suite, respecting touch tags |
|
||||
| PR labeled `full-acceptance` | 3 | 3 | 4 | 4 | 4 | Same as full |
|
||||
|
||||
Counts are independent Playwright shards. Mobile and tablet are separate projects
|
||||
and jobs. `scripts/acceptance-select.mjs` owns path selection: docs-only runs smoke;
|
||||
shared infrastructure changes select all areas on the two PR projects. Label changes
|
||||
trigger replanning. No change narrows full runs on main or tags except the explicit
|
||||
quarantine below.
|
||||
|
||||
Require **`acceptance gate`** in branch protection. It always runs, merges blobs
|
||||
into JSON and HTML, checks the matrix (`--partial` only for PR mode), and requires
|
||||
all planned shards to succeed. Its `acceptance` artifact retains the merged report,
|
||||
gate summary and failure attachments for 30 days. Full browser coverage cannot be
|
||||
claimed from a partial PR run. The under-30-minute PR target needs hosted timing
|
||||
validation; local syntax/unit checks do not measure it.
|
||||
|
||||
`WID-029` ([#93](https://github.com/QRun-IO/qqq-frontend-next/issues/93)) has
|
||||
`@quarantine` tags and an entry in `tests/acceptance/quarantine.json`. The default
|
||||
suite excludes it and reports it in `summary.quarantined`. Full runs execute it
|
||||
separately on Chromium with `QQQ_ACCEPTANCE_QUARANTINE=only` and
|
||||
`continue-on-error: true`, retaining `acceptance-quarantine` results. This is a
|
||||
visible known failure, not evidence of a passing feature. Missing issue links or
|
||||
unknown quarantine IDs fail the gate. See the [acceptance README](../../tests/acceptance/README.md)
|
||||
for local commands, artifact contents and cache/retry behavior.
|
||||
|
||||
## September 30 — post-RC4 report and menu checkpoint
|
||||
|
||||
Local production export at `a8e4d3b`:583 Chromium tests and all391 required feature rows passed, zero failures/skips/flaky results, three approved external-service exclusions. This includes the uniform table backgrounds, record overview spacing and hidePreview compatibility fixes. Retained full report/gate: `report-preview-full-chromium/` in local visual-review evidence.
|
||||
|
||||
The subsequent column-menu height correction passes28 targeted Chromium/WebKit cases. The valid red run failed both320px-window cases and passed both720px cases; final checks cover bounds, keyboard/focus, Copy/sort, standalone query actions and variants. Hosted predecessor36739388053 WebKit remains failed578/1: Copy full query values was outside the viewport before variant switching. Its exact scenario passed locally before correction; hosted resolution is unproven. Other predecessor jobs remain pending at this checkpoint. These scoped results do not establish complete browser compatibility.
|
||||
|
||||
|
||||
The acceptance suite runs against the owned sample backend and the production static
|
||||
export (javalin mode). The default gate runs Chromium only; the documented matrix is the
|
||||
five configured Playwright projects below. CI shards each project separately and merges
|
||||
their evidence into the single gate described above. Prioritize Chromium
|
||||
workflow depth for RC1 and final 1.0; the complete final browser claim still requires all five projects.
|
||||
|
||||
## Verification order
|
||||
|
||||
First complete workflows in desktop Chromium against the real backend: normal defaults,
|
||||
required and invalid inputs, dependent controls, edit/cancel, save/reopen, permission
|
||||
boundaries, error recovery, and database or downloaded-file results. Expand browser and
|
||||
viewport coverage after those behaviors work. Keep existing secondary results visible;
|
||||
a passing row or test count does not establish that every path within a feature works.
|
||||
|
||||
For example, WID-073 checks report column totals and the exported value distribution
|
||||
against SQL for the active filter, then verifies cancellation leaves saved data unchanged.
|
||||
Opening the statistics dialog alone is insufficient evidence of that workflow.
|
||||
|
||||
## RC3 published checkpoint — 2026-09-29
|
||||
|
||||
Published source `f1a6a3a` / `v1.0.0-RC.3` passed **570 Chromium tests and all 391 required rows** in the [candidate run](https://github.com/QRun-IO/qqq-frontend-next/actions/runs/36652168977), with zero failures, skips or flaky results. The tested PR merge has the same Git tree as the tag; backend commit was `cba758507dcc54cf282a6638ab50a7d3a1904c7c`.
|
||||
|
||||
The candidate run is complete: mobile **545 passed**; Firefox **569 passed, one failed** (WID-005: Widget Gallery heading timeout); WebKit **568 passed, two failed** (NAV-056: application-request access-control errors; QRY-030: density-menu timeout); tablet **543 passed, two failed** (QRY-030: density-menu timeout; REC-046: WebKit internal navigation error). No skips or flaky results. Reports and traces are retained; causes remain unproven. The overall candidate run failed. The [tag run](https://github.com/QRun-IO/qqq-frontend-next/actions/runs/36654410984) passed **570 Chromium tests and all 391 required rows**. Tag WebKit passed **568 and failed two** (NAV-016 application-request access-control errors; QRY-030 density-menu timeout); tag Firefox passed **568 and failed two** (QRY-007 error toast not found; WID-073 Render Report action timeout). Tag mobile passed **545 tests**; tablet passed **543 and failed two** (NAV-026 widget-request access-control errors; QRY-030 density-menu timeout). The tag run is complete and failed overall, with zero skips or flaky results. Reports and traces are retained; no complete cross-browser pass is claimed.
|
||||
|
||||
The public Maven JAR passed signature/checksum checks, normal dependency resolution in a separate consumer, and seven affected Chromium workflows with independent database readback. Both image architectures are published; the actual arm64 image passed app-route and all 21 referenced JavaScript asset checks. See [RC3 release notes](../releases/1.0.0-RC.3.md) for immutable artifact checksums and limitations. Final 1.0 remains on hold.
|
||||
|
||||
## RC2 published checkpoint — 2026-09-29
|
||||
|
||||
Published source `0905cfb` / `v1.0.0-RC.2` passed **563 Chromium tests and all 390 required rows** in both its PR and tag runs. The [completed tag run](https://github.com/QRun-IO/qqq-frontend-next/actions/runs/36642612560) also passed all **538 mobile tests**. WebKit passed 561 and failed QRY-030 (density menu) and RPT-012 (internal navigation error); Firefox passed 561 and failed PRC-061 (bulk-load default selection) and INT-011 (failed-save feedback); tablet passed 537 and failed QRY-030. No tests skipped or passed through retries. The overall run failed, and failure causes remain under investigation.
|
||||
|
||||
The earlier PR run had a different set of secondary-browser failures. Both sets remain recorded in the [RC2 release notes](../releases/1.0.0-RC.2.md); a passing later scenario does not establish why an earlier failure occurred. The published Maven JAR was independently downloaded, signature/checksum verified, resolved by a fresh consumer and exercised in five Chromium workflows. Both container architectures are published; the arm64 image passed runtime HTML/JavaScript checks. RC2 is immutable and final 1.0 remains on hold.
|
||||
|
||||
The preparation checkpoints below are historical results and do not supersede this published-artifact evidence.
|
||||
|
||||
## Post-RC1 checkpoint — 2026-09-29
|
||||
|
||||
The report capability candidate `2221999` passed **562 Chromium tests**, all 390 required rows, zero failures/skips/flaky tests in [CI](https://github.com/QRun-IO/qqq-frontend-next/actions/runs/36633453712). WebKit passed 561 with one QRY-030 failure: the density option did not appear after a trigger tap. Its trace is retained and the cause remains unproven. Firefox passed 560 with two failures: NAV-014 reported an owned embedded-page request failure during navigation; SEC-003 timed out awaiting its initial record row, before the create-restriction assertions. The final screenshot shows the row, and the trace shows a roughly 15-second delay before the query was sent; the cause remains unproven. Mobile passed all 537 tests; tablet passed 536 with one QRY-030 density-menu timeout matching the desktop WebKit symptom. The run is complete and failed overall. These results precede the child-export correction.
|
||||
|
||||
The subsequent `542ac47` local full Chromium run passed 562 and failed WID-046: the generic parent CSV export was hidden while child dropdowns awaited selection, despite the parent's independent CSV payload. The correction preserves the selection restriction for specialized child export and restores generic payload export. Its new regression was observed failing before the fix; 29 focused tests, all 1,857 unit tests, and the production build pass. The corrected export then passed **563 Chromium tests and all 390 required rows**, with zero failures/skips/flaky tests. A clean local JAR matches the tested export byte-for-byte. Exact-commit CI and published-artifact verification remain pending; this local result does not establish artifact availability.
|
||||
|
||||
The preceding d937507 [CI run](https://github.com/QRun-IO/qqq-frontend-next/actions/runs/36626338487) passed **558 tests each in Chromium and WebKit**, all 390 required rows. Firefox passed **556 with two failures**: NAV-014 reported `NS_ERROR_FAILURE` for the owned embedded page during breadcrumb navigation; QRY-007 timed out hovering an application-theme error toast after it detached. Logs and traces are retained for diagnosis. All five focused breadcrumb/error-theme scenarios subsequently passed locally in Firefox against RC2 without source or test changes. That recheck does not establish the cause or resolution of the CI failures. Mobile passed 533; tablet passed 532 and failed REC-019 with a WebKit internal navigation error while opening a field-lab record. Its log and trace are retained. These results do not establish a complete cross-browser pass or erase the RC1 results below.
|
||||
|
||||
## RC1 checkpoint — 2026-09-29
|
||||
|
||||
Published candidate `e901df9` / `v1.0.0-RC.1`: Chromium **547 passed**, no failed/skipped/flaky tests, all 389 required rows passed. WebKit **546 passed, 1 failed** in NAV-034 legacy table-process redirect navigation, with an internal WebKit resource-loading error; trace retained for investigation. The completed Firefox run had 546 passes and one WID-021 widget visibility timeout; phone/tablet had 1,043 passes and one tablet SEC-002 internal WebKit navigation error. The separate tag run also had failures; see the release notes for both runs. [CI run](https://github.com/QRun-IO/qqq-frontend-next/actions/runs/36601699322). This does not establish a complete cross-browser pass.
|
||||
|
||||
The actual Maven Central JAR additionally passed Sleep Interactive defaults/validation/completion and report-context filtered statistics/CSV checks. The published arm64 container passed a runtime/HTML smoke check; its manifest also includes amd64. See [RC1 release evidence](../releases/1.0.0-RC.1.md).
|
||||
|
||||
```bash
|
||||
export QQQ_SAMPLE_JAR=/path/to/qqq-sample-project-<version>-jar-with-dependencies.jar
|
||||
# primary workflow gate, fresh static export
|
||||
QQQ_ACCEPTANCE_BROWSERS=chromium node scripts/acceptance.mjs
|
||||
# subsequent compatibility gate, all five projects, fresh static export
|
||||
QQQ_ACCEPTANCE_BROWSERS=chromium,firefox,webkit,mobile,tablet node scripts/acceptance.mjs
|
||||
# the same with explicit ports (use distinct ports for concurrent runs)
|
||||
QQQ_ACCEPTANCE_BROWSERS=chromium,firefox,webkit,mobile,tablet \
|
||||
QQQ_ACCEPTANCE_BACKEND_PORT=18793 QQQ_ACCEPTANCE_FRONTEND_PORT=13793 \
|
||||
node scripts/acceptance.mjs
|
||||
# the touch projects only, reusing out/ (a filtered run is a partial gate)
|
||||
QQQ_ACCEPTANCE_BROWSERS=mobile,tablet node scripts/acceptance.mjs --skip-build
|
||||
```
|
||||
|
||||
| Project | Playwright device | Engine (Playwright 1.64.0-alpha-2026-10-01) | Runs |
|
||||
|----------|-------------------|----------------------------|------|
|
||||
| chromium | Desktop Chrome | Chrome for Testing 155 | every spec |
|
||||
| firefox | Desktop Firefox | Firefox 156 | every spec |
|
||||
| webkit | Desktop Safari | WebKit 26.6 | every spec |
|
||||
| mobile | Pixel 7 (412 × 839, touch) | Chromium | tests tagged `@mobile` |
|
||||
| tablet | iPad (gen 7) (810 × 1080, touch) | WebKit | tests tagged `@mobile` or `@tablet` |
|
||||
|
||||
## Phone and tablet scope (QRun-IO/qqq#708)
|
||||
|
||||
Below 768 px the UI uses a different layout by design:
|
||||
|
||||
- lists are card lists;
|
||||
- filters open in a bottom sheet;
|
||||
- record sections are an accordion;
|
||||
- record actions are in an action sheet;
|
||||
- navigation is a modal drawer.
|
||||
|
||||
The tablet in portrait (810 px) gets the desktop layout (grid, sidebar, tabs) with a touch
|
||||
pointer. Specs that assert the desktop layout (grid columns and cells, column resizing,
|
||||
grid keyboard navigation) do not describe phone behavior, and skipping them is not
|
||||
allowed: skips fail the gate. The touch projects therefore run only tagged tests:
|
||||
|
||||
- `mobile` runs tests whose title carries `@mobile`;
|
||||
- `tablet` runs tests tagged `@mobile` or `@tablet` (`grep` in
|
||||
`tests/acceptance/playwright.config.ts`).
|
||||
|
||||
Tagged tests also run on the desktop projects. They are either layout-neutral (helpers
|
||||
`listCell`, `columnCells`, `listRows`, `navigation`, `recordList`, `openRecord` and
|
||||
`recordAction` pick the grid or the cards, the tabs or the accordion) or pinned to a
|
||||
412 px touch viewport, so every phone scenario is also checked in Firefox and WebKit at
|
||||
phone width.
|
||||
|
||||
### Coverage rule, enforced by the gate
|
||||
|
||||
When a touch project runs, `scripts/acceptance-gate.mjs` requires every required row to
|
||||
have a passing test in `mobile` and in `tablet`. The only exception is a row that records
|
||||
why it is not a touch scenario (`"desktopOnly": "<reason>"`, which currently no row uses).
|
||||
The result is in `summary.phone`: `covered`, `desktopOnly`, `uncovered`. The desktop-layout
|
||||
variants that stay untagged (the grid-keyboard INT-001, the grid-loading INT-009, the
|
||||
desktop REC-004 tab test) have phone counterparts for the same rows.
|
||||
|
||||
### Touch targets and layout checks
|
||||
|
||||
On a coarse pointer every control is at least 44 × 44 CSS px (WCAG 2.5.5). This is a
|
||||
`@media (pointer: coarse)` rule in `src/styles/globals.css`, plus `pointer-coarse:`
|
||||
classes in components. It covers:
|
||||
|
||||
- buttons, inputs, selects, tabs, menu items and options;
|
||||
- a label wrapping a checkbox, radio or switch;
|
||||
- breadcrumbs, sidebar entries, record links and widget block links.
|
||||
|
||||
Links in running text and data values keep their inline size. Mouse layouts are unchanged
|
||||
at every width, because Playwright emulates `pointer: coarse` only when `hasTouch` is set.
|
||||
|
||||
`tests/acceptance/support/touch.ts` provides the checks:
|
||||
|
||||
- `expectNoHorizontalScroll`: the page never scrolls sideways; wide data scrolls inside
|
||||
its own container;
|
||||
- `expectTouchTargets` / `expectTouchReady`: no visible control below 44 px, applied only
|
||||
under a coarse pointer.
|
||||
|
||||
The phone and tablet specs call these on the shell, lists, filter sheet, column
|
||||
configuration, bulk bar, record view and forms, dialogs and sheets, every process screen
|
||||
type, dashboards and reports.
|
||||
|
||||
Phone behaviors with their own rows and tests:
|
||||
|
||||
| Phone behavior | Rows |
|
||||
|----------------|------|
|
||||
| Navigation drawer: tap, backdrop, keyboard focus trap, Escape, focus return | NAV-026, NAV-037 |
|
||||
| Shell on phones and tablets: header, breadcrumbs, sidebar, skip link, not-found | NAV-038, NAV-014 |
|
||||
| Card list: grid columns in order, formatted values, selections, loading | QRY-008, INT-009 |
|
||||
| Filter sheet: modal dialog, focus in and out, Escape, scrolls inside | QRY-009 |
|
||||
| Column configuration by touch | QRY-024 |
|
||||
| Bulk actions from a card selection | QRY-036 |
|
||||
| Saved views and export menus on a phone | QRY-055 |
|
||||
| Record header and phone action sheet | REC-058, INT-003 |
|
||||
| Forms with every editor type on phones and tablets | REC-059, INT-008 |
|
||||
| Tooltips and help by tap (fields, widget labels, widget help) | REC-036, REC-039, WID-043, WID-044 |
|
||||
| Dashboards: one column below 1024 px, wide tables scroll in their card, dropdown/date/reload/export by tap | WID-022, WID-040, WID-019, WID-045–WID-048 |
|
||||
| Process screens of every type touch-ready | PRC-001–PRC-047 |
|
||||
| Sign-in, logout, session expiry; accessibility scan at phone width | SEC-020–SEC-023, INT-014, INT-005 |
|
||||
| TABLE_BASED password sign-in, failure, expiry and logout | SEC-034–SEC-037 (QRun-IO/qqq#700) |
|
||||
| Login page branding before sign-in | NAV-033 (QRun-IO/qqq#703) |
|
||||
| Search dialog with record search | NAV-030 (QRun-IO/qqq#701) |
|
||||
| Keyboard use on a phone (card, action sheet, edit, save) | INT-001 |
|
||||
|
||||
The gate also fails any configured project that ran no tests.
|
||||
|
||||
## Results
|
||||
|
||||
Recorded 2026-09-26 on macOS (Darwin 27) for the Next UI 1.0 integration (QRun-IO/qqq#713):
|
||||
the sample from the qqq `feature/next-1.0` backend (`a55c71a2d`, with the v1 supplemental
|
||||
metadata, developer-mode and table-variant process routes of #714) and the Next static export
|
||||
of `feature/next-1.0` (`e3bea6e`: the batch 1-3 streams, Material parity #714 and phone and
|
||||
tablet coverage #708), built from a clean `.next`. One run of the full gate with the five
|
||||
projects:
|
||||
|
||||
Tests passed / run, per spec area and project (the touch projects run the `@mobile` tests only):
|
||||
|
||||
| Area | chromium | firefox | webkit | mobile | tablet |
|
||||
|---|---|---|---|---|---|
|
||||
| navigation | 59/59 | 59/59 | 59/59 | 56/56 | 56/56 |
|
||||
| performance | 5/5 | 5/5 | 5/5 | 5/5 | 5/5 |
|
||||
| processes | 61/61 | 61/61 | 61/61 | 54/54 | 54/54 |
|
||||
| query | 68/68 | 68/68 | 68/68 | 68/68 | 68/68 |
|
||||
| records | 90/90 | 90/90 | 90/90 | 84/84 | 84/84 |
|
||||
| security | 73/73 | 73/73 | 73/73 | 67/67 | 67/67 |
|
||||
| widgets | 93/93 | 93/93 | 93/93 | 91/91 | 91/91 |
|
||||
| **total** | **449/449** | **449/449** | **449/449** | **425/425** | **425/425** |
|
||||
|
||||
Gate (`test-results/acceptance/gate.json`, 2,197 tests, 44.3 min, retries 0, commit `e3bea6e`):
|
||||
|
||||
```json
|
||||
{
|
||||
"summary": { "passed": 347, "failed": 0, "missing": 0, "excluded": 3,
|
||||
"phone": { "covered": 347, "desktopOnly": 0, "uncovered": 0 } },
|
||||
"byProject": {
|
||||
"chromium": { "passed": 449, "failed": 0, "skipped": 0, "flaky": 0 },
|
||||
"firefox": { "passed": 449, "failed": 0, "skipped": 0, "flaky": 0 },
|
||||
"webkit": { "passed": 449, "failed": 0, "skipped": 0, "flaky": 0 },
|
||||
"mobile": { "passed": 425, "failed": 0, "skipped": 0, "flaky": 0 },
|
||||
"tablet": { "passed": 425, "failed": 0, "skipped": 0, "flaky": 0 }
|
||||
},
|
||||
"problems": []
|
||||
}
|
||||
```
|
||||
|
||||
Every required row passes in every desktop project and in both touch projects; no row is
|
||||
`desktopOnly`. PRC-039, WID-033 and SEC-030 are `required: false` pending approval and are
|
||||
reported as excluded.
|
||||
|
||||
## Differences found between engines
|
||||
|
||||
Fixed:
|
||||
|
||||
- **WebKit, aborted prefetches.** WebKit raises "<url> due to access control checks." as
|
||||
a page error for same-origin Next.js route prefetches and RSC payload fetches that a
|
||||
document navigation cancels; the server answered 200 and Playwright never sees the
|
||||
request. Traces confirmed every report named a `/app/...` route, `__next.*.txt` or
|
||||
`?_rsc=` payload within a millisecond of a main-frame navigation. The shared diagnostics
|
||||
fixture ignores exactly these (same origin, Next route or payload URL, within one second
|
||||
of a navigation) and lists them under `interruptedFetches`; API, cross-origin and
|
||||
unrelated access-control failures still fail the test.
|
||||
- **WebKit, document title.** Next.js re-applies the static metadata title ("QQQ Admin")
|
||||
after client navigations, in WebKit after the layout set the metadata title (NAV-023,
|
||||
NAV-006). `useDocumentTitle` keeps the breadcrumb title in place.
|
||||
- **Firefox, rich text.** Firefox stores a typed space before a formatting change as
|
||||
` `; the editor now stores engine-neutral HTML (REC-020).
|
||||
- **Firefox/WebKit, computed styles.** A `CSSStyleDeclaration` returned from `evaluate()`
|
||||
only serializes in Chromium; WID-057 reads the properties inside the page.
|
||||
- **WebKit keyboard order.** WebKit, like Safari, moves Tab only between form fields;
|
||||
keyboard specs use Option+Tab there (`tabKey` in `specs/security/support/ui.ts`).
|
||||
- **Aborted requests.** A request aborted by the test is reported as `net::ERR_FAILED`
|
||||
(Chromium), `NS_ERROR_FAILURE` (Firefox) or "Blocked by Web Inspector" (WebKit).
|
||||
- **Provider logout in Firefox.** Starting an in-app navigation just before leaving for
|
||||
the identity provider made Firefox log "Failed to fetch RSC payload". Provider logout and
|
||||
sign-in retries now leave the page without a client navigation first.
|
||||
- **Set-Cookie visibility in WebKit.** WebKit hides `Set-Cookie` from page responses, so
|
||||
SEC-033 checks the logout response from Node.
|
||||
- **Phone defects (QRun-IO/qqq#694).** The card list showed "No records found" while
|
||||
loading; it now shows busy placeholder cards. The record action sheet moves focus in on
|
||||
open, closes on Escape, and returns focus to its trigger, also when a dialog opened from
|
||||
one of its items closes.
|
||||
|
||||
The fixes above describe earlier checkpoints. Open failures from the RC1 runs are recorded in the RC1 checkpoint and release notes; they have not been waived.
|
||||
|
||||
|
||||
## RC4 candidate density touch correction
|
||||
|
||||
The recurring QRY-030 density-menu failure was reproduced in Linux WebKit using
|
||||
Playwright 1.58.2's official Noble image. Instrumented events showed that touch
|
||||
pointer-down opened the menu, then synthetic mouse focus returned to its trigger;
|
||||
Radix treated that focus as outside the non-modal menu and dismissed it. macOS
|
||||
WebKit did not reproduce this event sequence.
|
||||
|
||||
The RC4 candidate source applies the existing Selection/Views/Export trigger guard to
|
||||
Density. It ignores outside interactions only when they target that same trigger;
|
||||
other controls still dismiss the menu. No layout, dependency or timeout changed.
|
||||
|
||||
Both the existing density-selection test and the new touch/keyboard/outside-dismissal
|
||||
regression passed with the selection-menu workflow: **nine checks across Linux
|
||||
WebKit, tablet WebKit and Chromium, zero failures/skips/flaky results**. The new
|
||||
regression failed before the fix. An initial added-test run used an incorrect
|
||||
`textbox` locator for the searchbox; it was interrupted, retained, corrected and
|
||||
rerun. The full unit suite passed 1,884 tests; production export, types, affected
|
||||
lint and independent review also passed. The combined candidate at `f2cdf03` passed **577 Chromium tests and all 391
|
||||
required rows**, with zero failures/skips/flaky results and three approved exclusions.
|
||||
The preceding numeric-wheel correction separately passed its 576-test Chromium gate.
|
||||
|
||||
Both corrections were integrated before RC4 publication at
|
||||
`f2cdf03`, superseding its earlier preparation checkpoint `1dbe1d9`. Published
|
||||
RC3 does not contain them. Historical RC3/precursor failures above remain
|
||||
valid evidence for those revisions; this focused result does not certify the whole
|
||||
browser matrix.
|
||||
|
||||
|
||||
## Completed precursor CI before RC4 integration
|
||||
|
||||
Run `36658766595` at `1283bac07e81741bfadc34bf7fe1c7805604d16f` is terminal.
|
||||
Chromium passed 574 tests and all 391 required rows. Mobile passed 547; tablet
|
||||
passed 544 and failed three: NAV-015 internal WebKit navigation, NAV-056 API
|
||||
access-control errors, and QRY-030 density touch. Firefox passed 573 and failed
|
||||
WID-024 waiting for the record heading. WebKit passed 573 and failed QRY-030.
|
||||
All profiles had zero skips or flaky outcomes; general CI and image checks passed.
|
||||
These results predate the report-editor, numeric-wheel and density corrections.
|
||||
The Linux density regression is corrected and locally verified as described above;
|
||||
other secondary failures remain open pending diagnosis or new candidate evidence.
|
||||
Hosted checks for the integrated RC4 candidate and tag are complete; see the RC4 published checkpoint below.
|
||||
|
||||
|
||||
## RC4 published checkpoint — 2026-09-30
|
||||
|
||||
Published source `3c409f9` / `v1.0.0-RC.4` passed **577 Chromium tests and all 391 required rows** in both [candidate](https://github.com/QRun-IO/qqq-frontend-next/actions/runs/36664932061) and [tag](https://github.com/QRun-IO/qqq-frontend-next/actions/runs/36714593674) acceptance. Both used backend `e0d57dd31b979b17215370546ef4e26cd8a38d21`; the candidate merge tree matches the tagged source.
|
||||
|
||||
Both acceptance runs completed and failed overall because of secondary-browser failures. All profiles had zero skips or flaky outcomes; failures were retained, not waived or silently retried.
|
||||
|
||||
| Browser | Candidate result | Tag result |
|
||||
| --- | --- | --- |
|
||||
| Chromium | 577 passed | 577 passed |
|
||||
| Mobile | 547 passed, 1 failed: REC-054 script-suggestion listbox bounding-box timeout in dark mode | 548 passed |
|
||||
| Firefox | 576 passed, 1 failed: WID-026 record-heading timeout | 576 passed, 1 failed: WID-019 dashboard-heading timeout |
|
||||
| WebKit | 576 passed, 1 failed: REC-046 internal navigation error on a read-only user's copy route | 576 passed, 1 failed: REL-005 internal navigation error returning to the pet list |
|
||||
| Tablet | 547 passed, 1 failed: NAV-026 widget-request access-control errors during drawer navigation | 547 passed, 1 failed: SEC-002 internal navigation error opening a denied ledger create route |
|
||||
|
||||
Density scenarios passed in every profile of both runs. The other failures remain open; a later pass does not erase the earlier failure or prove full browser compatibility.
|
||||
|
||||
|
||||
The actual public Maven JAR passed signature/checksum verification, fresh-consumer dependency resolution and 17 affected real-backend Chromium workflows. The public ARM64 container served the application and all 21 referenced JavaScript assets. These checks establish artifact delivery and the stated workflow coverage, not full compatibility or visual approval. See [RC4 release notes](../releases/1.0.0-RC.4.md) for checksums and remaining requirements. Final 1.0 remains on hold for owner testing and renewed approval.
|
||||
@@ -0,0 +1,5 @@
|
||||
# ESB integration acceptance
|
||||
|
||||
The ten required [ESB matrix scenarios](../../tests/acceptance/matrix/esb.json) pass in Chromium, Firefox and WebKit: 30 cases with no failures, skips or retries. They exercise the stock sample’s embedded Artemis broker, table-event delivery into `syncPerson`, service/table/process views, permission denial, subscription browsing, trigger pause/resume/restart and confirmed dead-letter replay. A further 38 Chromium navigation, permission and table Developer-view cases, plus 12 security-variant and search cases, pass without skips or retries. The main and security fixtures use separate broker ports. These are focused ESB results; general Next browser/parity and final release acceptance remain tracked separately.
|
||||
|
||||
The backend integration fixes are [QQQ PR #980](https://github.com/QRun-IO/qqq/pull/980), based on `5bf02d54`: normal sample ESB registration, API documentation route isolation and nullable subscription/dead-letter pause state. Frontend verification passes 105 focused ESB tests, all 1,951 unit tests with coverage thresholds, type checking, the production static export and bundle budgets. The [acceptance README](../../tests/acceptance/README.md) gives the commands and fixture boundaries; [Next #10](https://github.com/QRun-IO/qqq-frontend-next/issues/10) and [QQQ #739](https://github.com/QRun-IO/qqq/issues/739) track delivery. RabbitMQ and broker management behavior are covered by the backend conformance suite; these browser tests use Artemis without an HTTP management endpoint, so unavailable broker actions remain hidden.
|
||||
@@ -0,0 +1,444 @@
|
||||
# Next UI feature acceptance matrix
|
||||
|
||||
> **Current status lives in GitHub:** [Next UI 1.0 roadmap](https://github.com/QRun-IO/qqq/issues/713). This document records scoped evidence and historical checkpoints; older open/closed wording and summary counts do not override current issue status. See [tracking policy](../ISSUE-TRACKING.md). Final 1.0 remains held for owner testing and explicit approval.
|
||||
|
||||
Generated by `node scripts/acceptance-matrix-md.mjs` from `tests/acceptance/matrix/*.json` and the
|
||||
acceptance gate results. Each row is a supported QQQ UI feature or use case with executable
|
||||
real-backend acceptance coverage (QRun-IO/qqq#649). See `tests/acceptance/README.md` to run it.
|
||||
|
||||
Rows: 394. Required rows passing in the recorded runs: 391.
|
||||
|
||||
Recorded runs: `/Users/james.maes/Git.Local/QRunIO/qqq-frontend-next/test-results/visual-review/rc8-corrected-full-chromium/gate.json` (2026-10-01T01:03:05.420Z), `/Users/james.maes/Git.Local/QRunIO/qqq-frontend-next/test-results/visual-review/rc8-corrected-ci-chromium/gate.json` (2026-10-01T01:23:18.171Z).
|
||||
|
||||
Corrected RC8 source cd3d473: 602 local and 602 hosted Chromium cases passed, with zero failed/skipped/flaky cases or report errors. Hosted run 36798799502. Three approved external-service exclusions remain. Earlier failed runs are retained and described in docs/releases/1.0.0-RC.8.md; this matrix records the corrected primary runs only. Other browser profiles and whole-product compatibility/visual approval remain separate.
|
||||
|
||||
The additional ESB integration scenarios and their focused three-browser results are recorded in [ESB acceptance](esb.md). The historical full-suite counts above predate those ten new matrix rows.
|
||||
|
||||
## Navigation, application structure, branding and icons
|
||||
|
||||
| ID | Feature | Scenarios | Negative | Fixture | Result | Issues |
|
||||
|---|---|---|---|---|---|---|
|
||||
| NAV-001 | Direct link to a table list renders seeded rows | success | | prime-test-database.sql | pass (chromium) | |
|
||||
| NAV-002 | Sidebar lists every permitted top-level app once, by label, in backend order, with nested apps only under their parent | admin sees Miscellaneous, Nav Empty App, Nav Level One, People App, Sample Widgets Dashboard, Sharing Demo in backend order; Greetings App appears only under People App (regression: it was duplicated at top level); group children use metadata labels | noPets persona: Pet and Pet Note are absent from the sidebar; backend returns 404 for their metadata and 403 for a query | stock sample apps + NavigationFixtures (Nav Level One, Nav Empty App); persona noPets | pass (chromium) | [#652](https://github.com/QRun-IO/qqq/issues/652) |
|
||||
| NAV-003 | Apps nested deeper than two levels render as nested sidebar groups; deep links expand every enclosing group | expand three nested groups and open Nav Deep Item; grid rows match nav_deep_item; deep link to /app/navDeepItem expands all three groups and marks the item current | | NavigationFixtures: navLevelOne > navLevelTwo > navLevelThree > navDeepItem | pass (chromium) | [#652](https://github.com/QRun-IO/qqq/issues/652) |
|
||||
| NAV-004 | Sidebar click navigation to app homes, tables and processes with active-entry highlighting and collapse/expand | Dashboard is current on /app; click app, table, process; URL, content and aria-current follow; collapse hides children, expand shows them | | stock sample (Miscellaneous app) | pass (chromium) | |
|
||||
| NAV-005 | Declared icons (QIcon name, color, path) render on navigation surfaces | sidebar glyph per declared Material name, color applied, image path loads; app home entries and section headings show declared icons; command palette entries show declared icons | no declared icon renders the type fallback glyph | sample icons (stars, person, widgets, share, pets, local_shipping, badge) + NavigationFixtures (path icon, colored icon, inventory_2, inbox, layers) | pass (chromium) | [#538](https://github.com/QRun-IO/qqq/issues/538) |
|
||||
| NAV-006 | Hidden tables and processes never appear in navigation; direct links to hidden objects the user may not access show not-found and the backend denies them | sidebar, app home, landing page and command palette omit City, Greet People, Simple Sleep and Nav Hidden Note | /app/city and /app/greet show the not-found state; backend returns 404 and 403 | sample city (hidden table), greet and simpleSleep (hidden processes); NavigationFixtures navHiddenNote (hidden, NOT_PROTECTED) | pass (chromium) | |
|
||||
| NAV-007 | A hidden table the user may access opens by direct link (hidden is not denied) | /app/navHiddenNote lists nav_hidden_note rows; breadcrumbs show its enclosing apps | | NavigationFixtures navHiddenNote (isHidden, PermissionLevel.NOT_PROTECTED) | pass (chromium) | |
|
||||
| NAV-008 | App home sections list processes (Actions), reports (Reports) and tables (Data) by label with icons, and each entry navigates | Miscellaneous: Actions Sleep Interactive, Simple Throw; Data Carrier, Field Lab, Pet Species (labels, not names); section heading shows its label and declared icon; clicking an entry opens the table or process | hidden or unpermitted section entries are omitted (navHiddenNote, noPets persona) | sample greetingsApp and miscellaneous sections | pass (chromium) | [#651](https://github.com/QRun-IO/qqq/issues/651) |
|
||||
| NAV-009 | App home table entries show the backend record count | each Data entry reads 'N total records' equal to the SQL count | | sample carrier, fieldLab, petSpecies, person, pet, petNote rows | pass (chromium) | [#651](https://github.com/QRun-IO/qqq/issues/651) |
|
||||
| NAV-010 | App homes of apps with child apps, only processes, or nothing render without errors | People App shows Clone People and the Greetings App child app (regression: crashed on missing widgets); Nav Level One lists its child app and navigates to it; Nav Empty App shows the empty state | viewer persona: People App shows only the Greetings App child app (no denied process) | sample peopleApp; NavigationFixtures navLevelOne, navEmptyApp; persona viewer | pass (chromium) | [#651](https://github.com/QRun-IO/qqq/issues/651) |
|
||||
| NAV-011 | A dashboard app (widgets only) opens from navigation and shows one labelled container per permitted declared widget | sidebar click opens the dashboard; one widget region per declared widget label, in order | | sample SampleWidgetsDashboard (11 widgets) | pass (chromium) | |
|
||||
| NAV-012 | Branding: sidebar logo (Material parity), app name, favicon from the icon, accent color, and partial branding accepted without warnings | sidebar shows /samples-logo.png with alt QQQ Sample, loaded, linking to the dashboard; favicon link points to /kr-icon.png; primary color variables equal the branding accent color; no 'schema validation' console warning for branding without companyName/companyUrl | | sample branding (appName, logo /samples-logo.png, icon /kr-icon.png) + NavigationFixtures accentColor #1d4ed8 | pass (chromium) | [#539](https://github.com/QRun-IO/qqq/issues/539) |
|
||||
| NAV-013 | Branding banners render in their slots (top of site, top of body, side nav under logo) with text or sanitized HTML, severity and colors | each slot shows its message in the right place with severity and colors from metadata | unknown slots are not rendered | NavigationFixtures banners: QFMD_TOP_OF_SITE (INFO text), QFMD_TOP_OF_BODY (WARNING HTML), QFMD_SIDE_NAV_UNDER_LOGO (SUCCESS, colors, additional styles) | pass (chromium) | [#539](https://github.com/QRun-IO/qqq/issues/539) |
|
||||
| NAV-014 | Breadcrumbs show the full app hierarchy for tables, processes, records, create and edit pages, and each crumb navigates | /app/person: People App / Greetings App / Person; /app/navDeepItem/2/edit: Nav Level One / Nav Level Two / Nav Level Three / Nav Deep Item / 2 / Edit; process and create pages; clicking a crumb opens that app home | no breadcrumbs on the dashboard root | sample person/greetInteractive; NavigationFixtures navDeepItem | pass (chromium) | [#652](https://github.com/QRun-IO/qqq/issues/652) |
|
||||
| NAV-015 | Document title names the page, its enclosing apps and the application (Material parity) | Dashboard \| QQQ Sample; Person \| Greetings App \| People App \| QQQ Sample; record label title on the record page | unknown route title is Not Found \| QQQ Sample | stock sample | pass (chromium) | [#652](https://github.com/QRun-IO/qqq/issues/652) |
|
||||
| NAV-016 | The site root redirects to the dashboard | / replaces itself with /app (back does not return to /) | | stock sample | pass (chromium) | |
|
||||
| NAV-017 | Dashboard landing page: counts, quick actions and application cards derived from navigable, permitted metadata by label | counts equal the navigable tables, processes, apps and their widgets from backend metadata; quick actions create only navigable insertable tables and run only navigable processes, by label (regression: listed internal and table-bulk processes, crashed on apps without widgets); application cards open app homes | viewer persona: no create or process quick actions | stock sample + NavigationFixtures; persona viewer | pass (chromium) | [#651](https://github.com/QRun-IO/qqq/issues/651) |
|
||||
| NAV-018 | Direct (hard-loaded) links open every route shape with the shell, sidebar state and breadcrumbs | app home, nested app home, table, process, record view, record edit, create, saved view | | stock sample + NavigationFixtures | pass (chromium) | |
|
||||
| NAV-019 | Browser refresh keeps the route, its query string and the rendered content | refresh on a table with a query string, a record view, and an app home | | stock sample | pass (chromium) | |
|
||||
| NAV-020 | Browser back and forward traverse navigation history with the right content | dashboard > app home > table > record, then back three times and forward twice | | stock sample | pass (chromium) | |
|
||||
| NAV-021 | An unknown /app/{name} shows an in-shell not-found state with a link to the dashboard | /app/noSuchThing shows 'Page not found' naming the segment; the link opens the dashboard; no failed requests | | stock sample | pass (chromium) | [#654](https://github.com/QRun-IO/qqq/issues/654) |
|
||||
| NAV-022 | Unknown deeper or top-level paths return the 404 page with a working home link | /app/person/1/no/such and /no-such-page respond 404 and render the 404 page; Go Home reaches the dashboard | only the document 404 is reported | stock sample | pass (chromium) | |
|
||||
| NAV-023 | Record lookup by key: /app/{table}/key?{field}={value} opens the single matching record | ?code=NAV-B2 opens record 2 (Tide Chart); ?email=... opens that person | no match, several matches (?shelf=North), unknown field, no parameters: error message, no navigation | NavigationFixtures navDeepItem (unique key code); sample person emails | pass (chromium) | [#654](https://github.com/QRun-IO/qqq/issues/654) |
|
||||
| NAV-024 | Command palette (Ctrl+K and '.') lists navigable apps, tables and processes by label with type and app context, filters, and navigates by keyboard | filter 'pet' shows Pet, Pet Note, Pet Species with type and app; Enter opens the first; '.' opens the palette; Escape closes it | hidden objects are not listed | stock sample + NavigationFixtures | pass (chromium) | [#652](https://github.com/QRun-IO/qqq/issues/652) |
|
||||
| NAV-025 | Header search, '/' search dialog and search page jump to navigation targets and recently viewed records by label (matched locally, alongside record search when the backend offers it: NAV-030) | typing a label lists matching pages with app context; choosing one navigates; a viewed record is offered as a recent record; Enter without a selection opens /app/search?q= with the matches, surviving refresh; search dialog contains Tab and Shift+Tab with empty or populated results; Clear returns to the input; Escape and backdrop dismissal restore keyboard and pointer openers | no search request fails (regression: every keystroke produced a 404 from a missing /search endpoint); with no searchable tables no request is made at all (NAV-032) | stock sample | pass (chromium) | [#653](https://github.com/QRun-IO/qqq/issues/653) |
|
||||
| NAV-026 | Mobile navigation drawer opens from the header, navigates and closes | hamburger opens the drawer; choosing a table navigates and closes it; close button and a backdrop tap close it; drawer controls are 44 px touch targets | | stock sample, 412x839 touch viewport | pass (chromium) | |
|
||||
| NAV-027 | Developer page reports the backend metadata counts and the running framework versions | metadata counts equal the backend's; framework shows the bundled Next.js version (regression: hardcoded 'Next.js 15') | | stock sample | pass (chromium) | [#654](https://github.com/QRun-IO/qqq/issues/654) |
|
||||
| NAV-028 | Reports in the app tree are listed under their app (sidebar and app home Reports group) and open the report page | sidebar entry with its icon opens /app/navDeepItemReport with the report heading, title and breadcrumbs; app home Reports group lists and opens it | | NavigationFixtures navDeepItemReport (child of Nav Level Three, icon assessment) | pass (chromium) | [#654](https://github.com/QRun-IO/qqq/issues/654) |
|
||||
| NAV-029 | A user who may access no apps sees the Dashboard entry and the no-apps message (Material NoApps parity) | sidebar shows only Dashboard; landing shows You do not have permission to access any apps. | app home links show the not-found state | harness persona noApps (every permission except App HAS_ACCESS) - requested: not yet in AcceptanceSampleServer | pass (chromium) | [#652](https://github.com/QRun-IO/qqq/issues/652) |
|
||||
| NAV-030 | Global search (header box, '/' dialog, search page) lists the records a backend record search (POST /qqq/v1/search over each table's searchFields) finds, after the matching pages, by record label with the table label; results open by keyboard and the search page survives refresh | '/' dialog: 'COCO' finds the pet Coco (label and table label), case-insensitively; ArrowDown and Enter open /app/pet/{id}; the request searches the readable searchable tables (person, pet, savedView) with a per-table limit of 5; header box: 'Sample' lists the matching people (database order, limit 5) after pages; Enter opens /app/search?q=Sample whose Records region lists every match (limit 25) before and after refresh; a link opens the record | a one-character term is not sent (unit), and a recent record already found is not listed twice (unit) | stock sample + NavigationFixtures (search fields on person, pet, savedView) | pass (chromium) | [#701](https://github.com/QRun-IO/qqq/issues/701) |
|
||||
| NAV-031 | Record search respects table read permissions and record security locks, in the UI and the API | noPets: 'ey' lists the matching people only; the UI never asks for pets and the metadata advertises no pet search fields; admin: the backend returns the pets matching 'EY' for tableNames [pet]; alice finds her saved view 'Alice People View' with the saved view table label, in the UI and from the API; bob, with whom the view is shared read-only, finds it from the API (the lock admits shared readers) | noPets: POST /qqq/v1/search with tableNames [pet] returns no results; an unscoped search returns no pets; casey (neither owner nor shared reader): the locked saved view is not found in the UI or from the API | stock sample + NavigationFixtures + QueryFixtures (alice's saved view shared read-only with bob); noPets persona; sharing demo users | pass (chromium) | [#701](https://github.com/QRun-IO/qqq/issues/701) |
|
||||
| NAV-032 | Without searchable tables (the backend declares no searchFields) global search stays local and never calls a search endpoint | the variant's v1 metadata lists no table with searchFields; header box, '/' dialog and search page keep the local labels and matches, with no Records group or region | no request to /qqq/v1/search or /search is made while typing, opening the dialog or loading the search page | security variant (stock sample + SecurityFixtures) | pass (chromium) | [#701](https://github.com/QRun-IO/qqq/issues/701) [#653](https://github.com/QRun-IO/qqq/issues/653) |
|
||||
| NAV-033 | Login branding before sign-in: logo, app name, accent color, favicon and title from GET /qqq/v1/metaData/authentication (no session), never the banners | unauthenticated authentication metadata carries exactly appName, logo, icon and accentColor; after logout and a fresh load the login card shows the loaded logo and QQQ Sample; primary color variables and the Sign in button use the accent; favicon /kr-icon.png; title Sign in \| QQQ Sample | no banner text in the unauthenticated response or on the login page | sample branding (appName, logo /samples-logo.png, icon /kr-icon.png) + NavigationFixtures accentColor #1d4ed8 and three banners | pass (chromium) | [#703](https://github.com/QRun-IO/qqq/issues/703) [#539](https://github.com/QRun-IO/qqq/issues/539) |
|
||||
| NAV-034 | Material table-scoped URLs: /app/{table}/{process} opens the table process (or an instance process); /app/{table}/{id}/{process} runs it for that record and returns to it | /app/person/person.bulkEdit?recordsParam=recordIds&recordIds=1,2 replaces itself with the run (init sends 1,2; Back returns to the table); /app/person/prcQuickTask opens the instance process with returnTo the table; /app/person/1/clonePeople runs for record 1 (init and SQL clone) and Return lands on the record; /app/person/2/bulkEdit resolves the table process by suffix; Cancel returns to the record | /app/person/1/noSuchProcess shows the in-shell not-found state and starts no process; report URLs (/app/{table}/{report}) are not covered: the backend frontend report metadata (QFrontendReportMetaData) has no tableName, so no report can match (#732) | sample person.bulkEdit, clonePeople (table processes), prcQuickTask (ProcessesFixtures, instance process) | pass (chromium) | [#714](https://github.com/QRun-IO/qqq/issues/714) [#732](https://github.com/QRun-IO/qqq/issues/732) |
|
||||
| NAV-035 | Process runs launched from a record view or a filtered query return there (Return and Cancel honor returnTo) | record Actions > Clone People runs for the record and Return lands on the record; Bulk Edit from /app/person?filter=isEmployed: returnTo is the launching path and query; after the edit (SQL) Return restores the same filter and the filtered rows | the unmatched record (Morgan) stays out of the returned grid | sample person, clonePeople, person.bulkEdit | pass (chromium) | [#714](https://github.com/QRun-IO/qqq/issues/714) |
|
||||
| NAV-036 | Material app settings (MaterialDashboardAppMetaData): showAppLabelOnHomeScreen and includeTableCountsOnHomeScreen turned off hide the home-screen title and the table counts | v1 metaData carries supplementalAppMetaData.materialDashboard for the app; The app home has no visible title (a screen-reader-only h1 remains) and the table entry shows no count; No count request is made for the table | Apps without the settings keep their title and counts (Material defaults) | NavigationFixtures.java navQuietHome app (navQuietItem table) with MaterialDashboardAppMetaData(false, false) | pass (chromium) | [#714](https://github.com/QRun-IO/qqq/issues/714) |
|
||||
| NAV-037 | Mobile navigation drawer is a keyboard-usable modal dialog: focus moves in on open, Tab stays inside, and Escape, the close button or a chosen link close it and return focus to the menu button | Enter on the menu button opens the Navigation dialog with focus inside; Tab and Shift+Tab visit every drawer control and never reach the page behind; Escape, the close button and choosing Carrier close it; focus returns to the menu button; Carrier opens | Tab never leaves the open drawer (QRun-IO/qqq#708: focus stayed on the menu button; Firefox could not reach the drawer) | stock sample (Miscellaneous app), 412x839 touch viewport | pass (chromium) | [#708](https://github.com/QRun-IO/qqq/issues/708) |
|
||||
| NAV-038 | Shell on phones and tablets: skip link, header controls, breadcrumbs, navigation entries, user menu, app homes and not-found links are 44 px touch targets, and the page never scrolls sideways | skip link is the first Tab stop and 44 px tall when shown; header, breadcrumbs, sidebar or drawer entries and the user menu meet the 44 px target; no sideways page scroll with a four-level breadcrumb trail | under a mouse (pointer: fine) the desktop sizes are unchanged | stock sample + NavigationFixtures (navDeepItem three apps deep); mobile (Pixel 7) and tablet (iPad, desktop layout with touch) projects | pass (chromium) | [#708](https://github.com/QRun-IO/qqq/issues/708) |
|
||||
| NAV-055 | Material Dashboard CSS and test hooks remain available on navigation, banners, app homes, forms, record views and query controls | Material data-qqq-id and class selectors identify corresponding Next UI controls; application CSS keyed to those selectors changes the rendered style; record and query actions retain the expected behavior and button variants | | stock sample with NavigationFixtures and RecordsFixtures; application CSS applied in css-hooks.spec.ts | pass (chromium) | [QRun-IO/qqq-frontend-next#731](https://github.com/QRun-IO/qqq-frontend-next/issues/731) |
|
||||
| NAV-056 | The Material Dashboard theme from versioned QQQ metadata changes Next UI tokens, application CSS and the branded header | v1 metaData publishes the selected Material theme settings; the rendered dashboard applies its primary token, branded header colors and tagline, and custom CSS; Every theme scenario resets instance metadata before navigation; Preferences exposes accessible Light and Dark choices; keyboard changes, real reload persistence, reset and light/dark dialog accessibility are verified; Application theme override is explained in Preferences without erasing the saved browser appearance choice; Pivot validation meets text contrast in default light, default dark and a configured orange error theme; API playground visible text and hovered controls meet contrast in light/dark themes; Preferences changes update the rendered palette without reload | | AcceptanceSampleServer /acceptance/theme; reset removes the theme before each test | pass (chromium) | [QRun-IO/qqq-frontend-next#719](https://github.com/QRun-IO/qqq-frontend-next/issues/719) |
|
||||
|
||||
## Performance budgets on the production export: large tables, heavy dashboards and long processes (QRun-IO/qqq#710). Bundle budgets are checked separately by scripts/perf-budget.mjs.
|
||||
|
||||
| ID | Feature | Scenarios | Negative | Fixture | Result | Issues |
|
||||
|---|---|---|---|---|---|---|
|
||||
| PRF-001 | large table: a 10,000-row, 40-column table opens with every column and the exact first page within budget | all 40 columns have a sort header; the first 25 rows carry the exact computed values; the total reads 10,000; time from navigation to the first page is within PERFORMANCE_BUDGET.firstPageMs | only one page of rows is in the DOM (25 rows, 1,000 cells), not the table | tests/acceptance/fixture/PerformanceFixtures.java prfWide | pass (chromium) | [#710](https://github.com/QRun-IO/qqq/issues/710) |
|
||||
| PRF-002 | large table: 250-row pages of 40 columns render, page and reach the last page within budget | page size 250 shows ids 10000..9751 with 10,000 cells within PERFORMANCE_BUDGET.pageMs; next page shows 9750..9501 and last page 250..1 (page 40), each within PERFORMANCE_BUDGET.pageMs; the backend is asked for skip/limit 250 | the DOM never holds more than one page (250 rows) | tests/acceptance/fixture/PerformanceFixtures.java prfWide | pass (chromium) | [#710](https://github.com/QRun-IO/qqq/issues/710) |
|
||||
| PRF-003 | large table: sorting and filtering 10,000 rows return exact results within budget | sorting by Name ascending shows Row 00001..Row 00025 within PERFORMANCE_BUDGET.queryMs; Count 04 equals 0 narrows to the 40 matching rows (the SQL answer) within PERFORMANCE_BUDGET.queryMs | | tests/acceptance/fixture/PerformanceFixtures.java prfWide | pass (chromium) | [#710](https://github.com/QRun-IO/qqq/issues/710) |
|
||||
| PRF-004 | heavy dashboard: 24 widgets (statistics, bar, line and pie charts, 100-row tables) load once each within budget | every widget finishes loading within PERFORMANCE_BUDGET.dashboardMs of navigation; statistics show their exact counts, chart data tables their exact points, tables their 100 rows | each widget's data is requested exactly once | tests/acceptance/fixture/PerformanceFixtures.java prfDashboard | pass (chromium) | [#710](https://github.com/QRun-IO/qqq/issues/710) |
|
||||
| PRF-005 | long process: a 10-second job over 10,000 rows reports progress, polls at a bounded rate and shows its result promptly | progress messages advance while the job runs; the result shows Row Count 10000 and the SQL total; the result appears within PERFORMANCE_BUDGET.processResultMs of the job finishing | status is polled no more than once per POLL_INITIAL_MILLIS (1.5 s) and never concurrently | tests/acceptance/fixture/PerformanceFixtures.java prfLongRun | pass (chromium) | [#710](https://github.com/QRun-IO/qqq/issues/710) |
|
||||
|
||||
## Processes: steps, components, lifecycle, bulk load/edit/delete
|
||||
|
||||
| ID | Feature | Scenarios | Negative | Fixture | Result | Issues |
|
||||
|---|---|---|---|---|---|---|
|
||||
| PRC-001 | Selected-record run: record ids reach the backend, screen values are submitted, mixed view and record-list results render | recordIds 1,2 -> setup prefix/suffix -> results view values and both greeted rows | | sample greetInteractive | pass (chromium) | [#645](https://github.com/QRun-IO/qqq/issues/645) [#656](https://github.com/QRun-IO/qqq/issues/656) |
|
||||
| PRC-002 | All-records run from a filter selection with no criteria | filterJSON {criteria:[]} greets every seeded person (count checked against SQL) | | sample greetInteractive | pass (chromium) | |
|
||||
| PRC-003 | Zero-record runs from a filter that matches nothing | clonePeople input 0, validation on 0 with no lines, 0 processed | greetInteractive record-list step with zero records is refused ("Missing input records.") | sample greetInteractive, clonePeople | pass (chromium) | [#659](https://github.com/QRun-IO/qqq/issues/659) |
|
||||
| PRC-004 | Processes launched from an app (no records) | app shortcut opens Component Lab without records | greetInteractive without records is refused at init ("Missing input records.") | prcLab app, sample greetInteractive | pass (chromium) | |
|
||||
| PRC-005 | Process launched from selected rows of a table query | select Avery and Casey, Actions -> Greet Interactive -> both greeted | | sample person + greetInteractive | pass (chromium) | |
|
||||
| PRC-006 | Minimum and maximum input records | 1-2 selected records run and are listed | 0 or 3 explicit records show a message without init; filter selecting 5 shows the backend refusal; API filter selecting 0 is refused | prcBounds (min 1, max 2) | pass (chromium) | [#662](https://github.com/QRun-IO/qqq/issues/662) |
|
||||
| PRC-007 | Every declared component renders on one screen, in declared order, with one action bar | HELP_TEXT, HELP_TEXT, VIEW_FORM, EDIT_FORM, HTML, EDIT_FORM in order | | prcComponents mixed screen | pass (chromium) | [#645](https://github.com/QRun-IO/qqq/issues/645) |
|
||||
| PRC-008 | HELP_TEXT with line breaks and previewText toggle | plain lines; Show/Hide preview text | | prcComponents | pass (chromium) | [#645](https://github.com/QRun-IO/qqq/issues/645) |
|
||||
| PRC-009 | EDIT_FORM field subsets (includeFieldNames), section labels and persisted input values | Lab Inputs section holds only its fields; values saved (SQL prc_lab_run) | | prcComponents | pass (chromium) | [#645](https://github.com/QRun-IO/qqq/issues/645) |
|
||||
| PRC-010 | Required input validation on screens and in the backend | filled required field proceeds | empty required field blocks without a request; API step without the required input is refused | prcComponents | pass (chromium) | |
|
||||
| PRC-011 | Process possible-value fields | options from the process route; chosen id saved, label shown on review; search filters options | | prcComponents labColor (prcColor enum) | pass (chromium) | [#667](https://github.com/QRun-IO/qqq/issues/667) |
|
||||
| PRC-012 | VIEW_FORM labels and backend values | Introduction and Status values | | prcComponents | pass (chromium) | [#645](https://github.com/QRun-IO/qqq/issues/645) |
|
||||
| PRC-013 | HTML component renders the step html value, sanitized | markup rendered | script and onerror removed, never executed | prcComponents mixed.html | pass (chromium) | [#645](https://github.com/QRun-IO/qqq/issues/645) |
|
||||
| PRC-014 | DOWNLOAD_FORM delivers the generated file | download bytes equal the generated file | unregistered file path refused (403) | prcComponents compute step | pass (chromium) | |
|
||||
| PRC-015 | Step help content for process screens | help text shown | | prcComponents helpContent (PROCESS_SCREEN) | pass (chromium) | |
|
||||
| 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-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) |
|
||||
| PRC-023 | Completion: Next/Submit labels, Return on the last screen or when noMoreSteps | Submit on the step before the last screen; Return only on the last screen; noMoreSteps ends early | | prcComponents, prcWizard, prcEarlyFinish | pass (chromium) | |
|
||||
| PRC-024 | Linear stepper and step position | all step labels; current step marked; Step i of n | | prcComponents | pass (chromium) | |
|
||||
| PRC-025 | RECORD_LIST paging and page size over process records | 1-10, 11-20, 21-23; page size 25 | next disabled on the last page | prcManyRows (23 generated records) | pass (chromium) | |
|
||||
| PRC-026 | Validation review: input count, validate-or-skip choice, Next/Submit label, record preview | Input: 2 Person records.; radio switches the button label; preview 1 of 2 / 2 of 2 | | sample clonePeople | pass (chromium) | [#659](https://github.com/QRun-IO/qqq/issues/659) |
|
||||
| PRC-027 | Full validation summary lines (OK, WARNING, ERROR) with record links | four lines with counts and messages; link filters to the refused record | nothing persisted by validation | sample clonePeople + inserted clone persons | pass (chromium) | [#659](https://github.com/QRun-IO/qqq/issues/659) |
|
||||
| PRC-028 | Process summary results after execution and persisted effects | result lines and processed count; clones persisted (SQL); skip validation executes immediately | refused and nested records not cloned | sample clonePeople | pass (chromium) | [#659](https://github.com/QRun-IO/qqq/issues/659) |
|
||||
| PRC-029 | Bulk edit: switched-on fields only, review and result | edited fields persisted for selected records only (SQL); result lines | no switched-on field blocks without a request | enricher person.bulkEdit | pass (chromium) | [#656](https://github.com/QRun-IO/qqq/issues/656) [#659](https://github.com/QRun-IO/qqq/issues/659) |
|
||||
| PRC-030 | Bulk delete: review, validation and result | selected records removed (SQL) | validation deletes nothing | enricher person.bulkDelete | pass (chromium) | [#659](https://github.com/QRun-IO/qqq/issues/659) |
|
||||
| PRC-031 | Bulk insert: upload, column mapping, value mapping, profile, review preview and result | uploaded CSV mapped and inserted (SQL) | missing required file blocks | enricher person.bulkInsert + prcUploadArchive | pass (chromium) | [#656](https://github.com/QRun-IO/qqq/issues/656) [#660](https://github.com/QRun-IO/qqq/issues/660) [#543](https://github.com/QRun-IO/qqq/issues/543) |
|
||||
| PRC-032 | Bulk load mapping validation and Back through mapping and upload | Back from review keeps the mapping; Back from mapping to upload | unmapped required column blocks without a request | enricher person.bulkInsert | pass (chromium) | [#655](https://github.com/QRun-IO/qqq/issues/655) [#660](https://github.com/QRun-IO/qqq/issues/660) |
|
||||
| PRC-033 | Bulk edit with a file matched by key fields | rows matched by Id updated (SQL) | | enricher person.bulkEditWithFile | pass (chromium) | [#656](https://github.com/QRun-IO/qqq/issues/656) [#660](https://github.com/QRun-IO/qqq/issues/660) |
|
||||
| PRC-034 | Upload instructions HTML with downloadable template | template CSV downloads with the header row | | enricher person.bulkInsert upload.html | pass (chromium) | |
|
||||
| PRC-035 | Named WIDGET fetched with the process UUID and values | widget text built from process values | | prcWidgets prcHtmlWidget | pass (chromium) | [#661](https://github.com/QRun-IO/qqq/issues/661) |
|
||||
| PRC-036 | Named composite WIDGET seeded from process values | seeded blocks rendered, no fetch | | prcWidgets prcCompositeWidget | pass (chromium) | [#661](https://github.com/QRun-IO/qqq/issues/661) |
|
||||
| PRC-037 | Ad hoc composite WIDGET: interpolation, conditional blocks, action buttons, submit-on-enter inputs | Approve submits actionCode (SQL); Enter submits the scan code (SQL) | conditional block hidden | prcWidgets ad hoc blocks | pass (chromium) | [#661](https://github.com/QRun-IO/qqq/issues/661) |
|
||||
| PRC-038 | GOOGLE_DRIVE_SELECT_FOLDER screen keeps its other components working | help text and edit form submit (SQL); picker explains it is not configured | required export note blocks | prcDrive | pass (chromium) | [#645](https://github.com/QRun-IO/qqq/issues/645) |
|
||||
| PRC-039 | Google Drive folder selection through the Google Picker | pick a folder and submit its id | | needs GOOGLE_APP_CLIENT_ID, GOOGLE_APP_API_KEY and a Google account | excluded (Approved by James 2026-09-25 (QRun-IO/qqq#649): Google Drive folder picker needs a real Google OAuth client and account; the rest of the step is covered by PRC-038; real-service testing is follow-up work.) | |
|
||||
| PRC-040 | Reload mid-process starts a fresh run | new process UUID, first screen, empty inputs; earlier effects kept (SQL) | | prcComponents | pass (chromium) | |
|
||||
| PRC-041 | Expired session mid-process returns to sign-in | 401 redirects to login | nothing saved | prcComponents + expired persona | pass (chromium) | [#668](https://github.com/QRun-IO/qqq/issues/668) |
|
||||
| PRC-042 | Process permission enforcement | revoked permission shows the permission message | step refused, nothing saved; direct link offers no process; API init refused (403) | prcComponents + noProcesses persona | pass (chromium) | |
|
||||
| PRC-043 | Default process values from the link (defaultProcessValues) | sleepMillis preset drives an async sleep to screen1; labName prefilled; normal navigation shows a required sleep duration with a 1000ms default and completes to screen1 | clearing sleep duration blocks submission on screen0 | sample sleepInteractive, prcComponents | pass (chromium) | |
|
||||
| PRC-044 | Scanner-format screens: no heading or action bar, input blocks submit | auto-focused input submits on Enter (SQL) | no Next/Cancel buttons rendered | prcScanner (format scanner) | pass (chromium) | [#661](https://github.com/QRun-IO/qqq/issues/661) |
|
||||
| PRC-045 | State-machine step flow: repeated screens, no linear stepper | screen repeats with fresh inputs and counted rounds; noMoreSteps ends with Return | no stepper or Step i of n | prcLoop (STATE_MACHINE) | pass (chromium) | |
|
||||
| PRC-046 | Saved bulk load profiles: save, choose for a later load, delete | Save As stores the mapping (SQL); choosing the profile restores the mapping and is named on review; Delete removes it (SQL) | a duplicate name is refused | SavedBulkLoadProfileMetaDataProvider on owned saved_bulk_load_profile tables | pass (chromium) | [#660](https://github.com/QRun-IO/qqq/issues/660) |
|
||||
| PRC-047 | A run with no frontend screens completes with a completion screen | completion heading and message; backend effect persisted (SQL) | | prcQuickTask (backend step only) | pass (chromium) | |
|
||||
| PRC-048 | Process-field possible values load and search through the v1 API | the process combobox loads its options from the v1 process route; the chosen value reaches the process and persists; a v1 search returns the exact match | an unknown process field is 404 | prcComponents | pass (chromium) | [#699](https://github.com/QRun-IO/qqq/issues/699) [#406](https://github.com/QRun-IO/qqq/issues/406) |
|
||||
| PRC-049 | Processes run on the v1 process routes (upload, step, back step, records, metadata) | a bulk load uploads its file, maps, reviews, steps back (isStepBack=true) and inserts through /qqq/v1/processes/... and /qqq/v1/metaData/process/...; the inserted row is in the database; this session can cancel its own run | an unsafe upload filename is 400 before anything is stored; an unknown run is refused; (another session's run is refused 403 by records and cancel - backend unit tests) | person.bulkInsert, prcComponents | pass (chromium) | [#699](https://github.com/QRun-IO/qqq/issues/699) [#406](https://github.com/QRun-IO/qqq/issues/406) [#543](https://github.com/QRun-IO/qqq/issues/543) |
|
||||
| PRC-050 | Processes the instance adds to every query and record screen (Material processNamesToAddToAllQueryAndViewScreens) | v1 /metaData names the process in supplementalInstanceMetaData.materialDashboard (the only supplemental entry v1 publishes; the unversioned route's javalin server configuration stays off v1); casey receives it with no table and in no app (API); the person query Actions menu lists it last, after a divider, and runs it on the selected rows with tableName=person (SQL); the record actions menu of qryBin (no processes of its own) runs it on that record (SQL); the phone record action sheet runs it on that record (SQL) | a user whose metadata omits it (alice): not offered on query or record screens and its run page is not found (no SQL rows); noProcesses persona: absent from the metadata, not offered on query or record screens, and the backend refuses it (403, no SQL rows); a user whose metadata omits the process gets an empty v1 processNamesToAddToAllQueryAndViewScreens list (no hidden process names); the v1 and unversioned init routes refuse the process for a user without permission | ProcessesFixtures.java prcTagRecords (no table, in no app) named by MaterialDashboardInstanceMetaData; a metadata customizer lists it only for the sample user casey, so other areas keep their menus; person, qryBin | pass (chromium) | [#714](https://github.com/QRun-IO/qqq/issues/714) |
|
||||
| PRC-051 | Dependent possible values on a process screen: the choices follow the screen's other values | every search posts the screen values as the values map of the v1 JSON body; choices narrow to the chosen Category and change when it changes; the chosen id is recorded (SQL) and its label shows on the result screen | Category empty: the unset variable makes no matches for a form, shown as No options found; the backend refuses an out-of-filter id lookup and returns only the filtered choices | prcSpecimenPick specimenId (prcSpecimen source filtered on category = ${input.category}) | pass (chromium) | [#714](https://github.com/QRun-IO/qqq/issues/714) |
|
||||
| PRC-058 | Saved bulk load profile menu (Material SavedBulkLoadProfiles): new profile, update after the confirm, rename, save as a copy, delete, unsaved-change count with its list, Reset All Changes, New Bulk Load Profile, Reset to Empty or Suggested Mapping | a new profile is named in the Save New Bulk Load Profile dialog and Enter saves it (SQL row owned by the user); the heading names it; a change shows 1 Unsaved Change with the list of changes; Save asks Update Existing Bulk Load Profile and Enter confirms; the same row holds the new mapping (SQL); Rename keeps the row id under the new name; Save As stores a separate copy, which is then in use; a saved profile's changes are listed (header row, file column) and Reset All Changes restores its mapping; New Bulk Load Profile leaves the profile for an empty mapping; Reset to Suggested Mapping and Empty Mapping replace the mapping, and the suggested one is what the review screen shows | Enter in the delete dialog does not delete (SQL count unchanged); only the Delete button does, and the screen starts a new, empty mapping | enricher person.bulkInsert; SavedBulkLoadProfileMetaDataProvider on owned saved_bulk_load_profile tables | pass (chromium) | [#726](https://github.com/QRun-IO/qqq/issues/726) |
|
||||
| PRC-059 | Saved bulk load profile ownership: yours vs shared with you, and only the owner may save, rename or delete a profile | the query process returns both profiles with their owners; the menu lists alice's under Your Saved Bulk Load Profiles and bob's under Bulk Load Profiles Shared with you; on bob's profile Save, Rename and Delete are disabled with the not-owner reason, the changes list says why, and no Save link is offered; Save As is enabled; alice saves her own copy (SQL row owned by sample:alice) | bob's row is unchanged (label, owner and mapping, SQL) and no store or delete request named it; the backend has no ownership lock on saved bulk load profiles, so the UI is the only guard, as in Material | sample users alice and bob; bob's profile stored through storeSavedBulkLoadProfile and shared READ_ONLY with alice in shared_saved_bulk_load_profile | pass (chromium) | [#726](https://github.com/QRun-IO/qqq/issues/726) |
|
||||
| PRC-060 | Bulk load file mapping form fidelity: field help, column tooltips, repeated-header warning, header-row toggle re-mapping by header name, scroll to the first error, bulk-edit Key Fields / Fields To Update and the unmapped key field error | help for the header row, layout and table key fields shows under each and describes its control; a mapped column's tooltip names its fields; a repeated header is flagged and not offered as a column; without a header row columns are letters; a mapping to the repeated header is cleared with a warning when headers come back; off and on again the step posts header names (First Name, Last Name, Email) and the first Email column is inserted (SQL); an unmappable submit brings the first field error into view; bulk edit shows Key Fields (Select table key fields to continue.) and Fields To Update (Clear if empty only there); choosing Id names it as an unmapped key field until a column is chosen; the matched records update (SQL) | no step request is sent while a field has no column | enricher person.bulkInsert and person.bulkEditWithFile; ProcessesFixtures.primeBulkLoadHelp help content keyed process:person.bulkInsert;field:hasHeaderRow\|layout and process:person.bulkEditWithFile;field:tableKeyFields | pass (chromium) | [#726](https://github.com/QRun-IO/qqq/issues/726) |
|
||||
| PRC-061 | Typed bulk load default values, searchable value mapping and the grouped, searchable Add Fields menu (Material QHierarchyAutoComplete) | the Person default value is chosen by searching person possible values and shows its label; Birth Date takes a date; Is Employed takes Yes or No; the Add Fields menu groups Pet and Pet Note, searches, disables mapped fields with a tooltip and adds the WIDE note field twice (Pet Note: Note (1) and (2)); species values are mapped by searching the species possible values; the pets, their person, species and birth date and both notes each are inserted (SQL) | a default value left empty is refused with A value is required. before any step request | enricher pet.bulkInsert (pet with its notes association, WIDE layout) and person.bulkInsert | pass (chromium) | [#726](https://github.com/QRun-IO/qqq/issues/726) |
|
||||
| PRC-062 | Composite INPUT_FIELD metadata editors share process validation and multipart submission | Choose a labeled possible value, edit currency and multiline code, update a widget filter without changing columns, upload BLOB and FILE_UPLOAD data and verify backend-read content; Desktop wheel scrolling over a plain numeric block preserves its value; intentional keyboard edits and touch input changes are returned by the backend process review | Required uploads block submission; code-editor Enter does not submit the process | prcBlockEditors | pass (chromium) | [#728](https://github.com/QRun-IO/qqq/issues/728) |
|
||||
| PRC-063 | INPUT_FIELD autofocus and trimmed Enter values on subsequent process screens | Plain and metadata editors receive configured focus, placeholders and previous process values; Enter saves trimmed text verified in SQL; ordinary result screens receive heading focus | First blur does not show premature validation; required whitespace cannot advance either input screen | prcInputFocus | pass (chromium) | [#728](https://github.com/QRun-IO/qqq/issues/728) |
|
||||
| PRC-064 | DATE_TIME process inputs display local time and submit UTC instants | Unchanged seeded timestamps preserve seconds and instant across plain/shared blocks and regular form fields, including the repeated autumn DST hour; Edited timestamps crossing daylight saving submit UTC values verified from backend SQL; TIME inputs in plain/shared blocks and forms retain seconds for unchanged and edited submissions, verified through SQL readback. | Zoned values must not disappear from datetime-local controls or lose their timezone on submission | prcInputDateTimes with browser timezone America/New_York | pass (chromium) | [#728](https://github.com/QRun-IO/qqq/issues/728) |
|
||||
| PRC-065 | Configured Google Drive shared-folder selection and process submission | Select a shared-drive folder and submit ID, name, token and other required input through the real process; independently verify database outcome | Reject a file and retry; cancel a replacement selection clears the destination and token; missing required note cannot advance | prcDrive with owned Google SDK script responses and real configured backend CSP | pass (chromium) | [#704](https://github.com/QRun-IO/qqq/issues/704) |
|
||||
| PRC-066 | INPUT_FIELD case behaviors preserve caret and submitted process values | Backend uppercase/lowercase metadata transforms a middle-of-value edit without moving the caret; Enter and Submit persist exact values verified through independent SQL; Multiline TEXT case conversion preserves the middle-edit caret and inserted newline; Submit persists the exact transformed text through independent SQL; Unicode case expansion preserves continued typing in STRING and TEXT fields; submitted values are verified through independent SQL; Explicitly configured PASSWORD case behaviors preserve masking and show/hide values; Enter and Submit persist Unicode-expanded values verified through independent SQL | Typing alone does not submit or persist the process; Multiline Enter does not submit or navigate away | prcInputCase / prcTextCase | pass (chromium) | [#728](https://github.com/QRun-IO/qqq/issues/728) |
|
||||
|
||||
## Querying, relationships, saved views, export and bulk selection
|
||||
|
||||
| ID | Feature | Scenarios | Negative | Fixture | Result | Issues |
|
||||
|---|---|---|---|---|---|---|
|
||||
| QRY-090 | Column menu sorts, pins, opens a field filter and hides a column | Sort matches SQL; Pin right is reflected in the grid; Filter opens the field editor; Hide removes the column | | QueryFixtures.java qryItem | pass (chromium) | [#716](https://github.com/QRun-IO/qqq/issues/716) |
|
||||
| QRY-091 | Column menu copies page or full query values in backend sort order | Page copy contains the first ten ids; Full copy contains all eleven ids | | sample carrier (11 rows) | pass (chromium) | [#716](https://github.com/QRun-IO/qqq/issues/716) |
|
||||
| QRY-092 | Column chooser searches, groups, counts and marks changed layouts | Search narrows fields; Groups collapse and expand; Hiding a field marks the columns dirty; Keyboard opening focuses the popup and Tab reaches search; Explicit close and Escape restore Columns focus; outside click keeps the clicked control focused | Unknown search shows an empty message | QueryFixtures.java qryItem | pass (chromium) | [#716](https://github.com/QRun-IO/qqq/issues/716) |
|
||||
| QRY-093 | Last saved view and unsaved edits survive reopening and reload | Dirty filter survives reload; Table route opens the last saved view; Reset restores the backend view; Displayed filter updates the URL before a delayed page navigation completes; immediate reload preserves it, along with unrelated parameters, hash and history length | | sample Alice People View | pass (chromium) | [#717](https://github.com/QRun-IO/qqq/issues/717) |
|
||||
| QRY-094 | Counted quick views render and open their saved-view route | Count reflects the backend filter; Quick-view button opens the filtered view | | sample Alice People View marked as a quick view in H2 | pass (chromium) | [#717](https://github.com/QRun-IO/qqq/issues/717) |
|
||||
| QRY-095 | A new query defaults to fifty rows per page | Fifty visible rows; Pagination reports fifty of the full count | | QueryFixtures.java qryManyRow (1,234 rows) | pass (chromium) | [#717](https://github.com/QRun-IO/qqq/issues/717) |
|
||||
| QRY-096 | A saved view reconciles removed fields and warns the user | Obsolete criteria and sort are removed before querying; Warning names the removed field; Heading names the active saved view | | sample Alice People View with retiredField references in H2 | pass (chromium) | [#717](https://github.com/QRun-IO/qqq/issues/717) |
|
||||
| QRY-001 | Record list renders metadata labels and all records in the default primary-key-descending order | Headers use field labels; Rows equal SQL select ordered by id desc; Possible-value cells show labels, booleans Yes/No, money/decimals formatted; Original Next uniform row backgrounds in light and dark mode, with opaque pinned cells and selection highlighting | Null boolean renders empty, not No; Hidden and heavy (TEXT) fields are not columns | QueryFixtures.java qryItem (8 rows, every field type) | pass (chromium) | [#686](https://github.com/QRun-IO/qqq/issues/686) [#691](https://github.com/QRun-IO/qqq/issues/691) |
|
||||
| QRY-002 | Pagination with Material page sizes and URL state | Page 2 shows the remaining rows; Page sizes 10/25/50/100/250; Page and size survive reload via URL | Next is disabled on the last page | sample (person, pet, petNote, carrier, petSpecies) carrier (11 rows) | pass (chromium) | [#686](https://github.com/QRun-IO/qqq/issues/686) |
|
||||
| QRY-003 | Column header sort cycles ascending, descending and default; sort kept in the URL | Ascending and descending match SQL order by; Sort persists across reload | Third click returns to the PK-descending default | QueryFixtures.java qryItem | pass (chromium) | [#686](https://github.com/QRun-IO/qqq/issues/686) |
|
||||
| QRY-004 | Column visibility and order persist per table | Hide/show and reorder columns; Order and visibility survive reload; Show/hide all per table group | Another table keeps its own columns | QueryFixtures.java qryItem | pass (chromium) | |
|
||||
| QRY-005 | Column widths, density and card view | Resized width persists; Density changes row height; Card view shows the same records | Keyboard resize separator exposes its value | QueryFixtures.java qryItem | pass (chromium) | |
|
||||
| QRY-006 | Opening a record and returning keeps the query; refresh shows backend changes | Filter, sort and page are restored after Back; Refresh shows a row changed through the API | No stale cached rows after refresh | QueryFixtures.java qryItem | pass (chromium) | |
|
||||
| QRY-007 | A failed query reports the backend error with a retry | Backend error text shown; Retry reloads after the failure clears; Query error text and the front error notification meet text contrast in light, dark and configured application themes; Retry repeats the backend request; hover remains readable; Dismiss hides the error; resetting the invalid filter restores SQL-verified records | No rows from the failed request are shown | QueryFixtures.java qryItem | pass (chromium) | |
|
||||
| QRY-010 | Text operators match Material labels and backend semantics | equals, does not equal, contains, starts/ends with, is any of, is like, is empty/not empty; results equal SQL | does not equal includes empty values (NOT_EQUALS_OR_IS_NULL); Incomplete criteria are not sent | QueryFixtures.java qryItem.name/code | pass (chromium) | [#684](https://github.com/QRun-IO/qqq/issues/684) |
|
||||
| QRY-011 | Number operators including ranges and lists | equals, greater/less (or equal), is between, is any of; results equal SQL; Values sent as numbers | not between and none of exclude the bounds/list | QueryFixtures.java qryItem.quantity/price | pass (chromium) | [#684](https://github.com/QRun-IO/qqq/issues/684) |
|
||||
| QRY-012 | Date operators and relative date expressions | is/after/before/between with dates; today, N days ago, start of this/last month send backend expression JSON and match SQL | Clearing an expression restores a plain value input | QueryFixtures.java qryItem.receivedDate (relative to today) | pass (chromium) | [#684](https://github.com/QRun-IO/qqq/issues/684) |
|
||||
| QRY-013 | Date-time operators convert local input to UTC and support relative values | Local input sent as UTC instant; now and N hours ago expressions match SQL | Grid shows formatted date-times, not raw ISO | QueryFixtures.java qryItem.checkedAt | pass (chromium) | [#684](https://github.com/QRun-IO/qqq/issues/684) [#679](https://github.com/QRun-IO/qqq/issues/679) |
|
||||
| QRY-014 | Boolean, blob and long-text operators | equals yes / equals no / is empty for booleans; blob is empty / not empty; long text contains | Blob offers no value operators | QueryFixtures.java qryItem.isActive/photo/notes | pass (chromium) | [#684](https://github.com/QRun-IO/qqq/issues/684) |
|
||||
| QRY-015 | Possible-value selectors for table and enum sources show labels and send ids | is / is any of / is none of with label chips; Link-loaded ids render labels; Results equal SQL | Search with no match shows no options | QueryFixtures.java qryItem.ownerId (person), speciesId (petSpecies enum) | pass (chromium) | [#684](https://github.com/QRun-IO/qqq/issues/684) [#667](https://github.com/QRun-IO/qqq/issues/667) |
|
||||
| QRY-016 | AND/OR and nested groups combine as declared | AND group, OR group and nested subfilter match SQL | Removing the last criterion clears the filter | QueryFixtures.java qryItem | pass (chromium) | [#684](https://github.com/QRun-IO/qqq/issues/684) |
|
||||
| QRY-017 | Filters live in the URL: reload, back/forward and Material-style JSON links | Reload keeps the filter; Back/forward switches filters; Plain JSON filter link opens filtered | Malformed filter param falls back to no filter | QueryFixtures.java qryItem | pass (chromium) | [#684](https://github.com/QRun-IO/qqq/issues/684) |
|
||||
| QRY-018 | Backend-only operators from links render and filter correctly | Each operator from a link renders its label and returns the SQL rows | FALSE matches no records; builder does not crash | QueryFixtures.java qryItem | pass (chromium) | [#684](https://github.com/QRun-IO/qqq/issues/684) |
|
||||
| QRY-019 | Quick search combines with the advanced filter | Search term ANDs with Is Active = yes | Clearing search restores the filtered rows | QueryFixtures.java qryItem | pass (chromium) | [#686](https://github.com/QRun-IO/qqq/issues/686) |
|
||||
| QRY-020 | One-side exposed join: labelled columns, filters and sorts match SQL | Join column 'Owner: First Name' hidden by default, shown on demand; Join filter and sort match SQL LEFT JOIN | Joins only sent when used; rows without a match are kept | QueryFixtures.java qryItem -> person (MANY_TO_ONE) | pass (chromium) | [#686](https://github.com/QRun-IO/qqq/issues/686) [#684](https://github.com/QRun-IO/qqq/issues/684) |
|
||||
| QRY-021 | Many-side exposed join repeats rows and reports the distinct count | Repeated rows per note; Count shows (N distinct) | Row selection dedupes repeated ids | QueryFixtures.java qryItem -> qryItemNote (ONE_TO_MANY) | pass (chromium) | [#686](https://github.com/QRun-IO/qqq/issues/686) |
|
||||
| QRY-022 | Column statistics aggregate the filtered rows | Value distribution and count/distinct/sum/avg/min/max equal SQL under the filter | Sort by value/count | QueryFixtures.java qryItem (QUERY_STATS) | pass (chromium) | [#691](https://github.com/QRun-IO/qqq/issues/691) |
|
||||
| QRY-023 | Tables without QUERY_STATS offer no column statistics | No statistics button on headers | Process not launched | sample (person, pet, petNote, carrier, petSpecies) person | pass (chromium) | [#691](https://github.com/QRun-IO/qqq/issues/691) |
|
||||
| QRY-030 | Selection menu: this page, full query result, first N, clear | Banners and counts for page, all and subset; Touch opens and toggles the density menu without synthetic trigger focus dismissing it; Enter, Escape and outside search focus retain their normal behavior; Pinned selection targets remain exposed and respond at both edges for every density | Subset dialog rejects invalid N | sample (person, pet, petNote, carrier, petSpecies) carrier | pass (chromium) | [#689](https://github.com/QRun-IO/qqq/issues/689) |
|
||||
| QRY-031 | Bulk Edit launches with the checked record ids | recordIds param equals checked rows | No selection shows 'No records were selected to Bulk Edit.' | sample (person, pet, petNote, carrier, petSpecies) carrier | pass (chromium) | [#689](https://github.com/QRun-IO/qqq/issues/689) |
|
||||
| QRY-032 | Bulk Delete of the full query result sends the whole filter | filterJSON has no skip/limit; backend recordCount equals SQL count | Current page paging is not sent | sample (person, pet, petNote, carrier, petSpecies) carrier (page 2) | pass (chromium) | [#689](https://github.com/QRun-IO/qqq/issues/689) |
|
||||
| QRY-033 | First-N selection under a filter launches with that limit | filterJSON limit N with the filter and sort | N larger than the result is capped | QueryFixtures.java qryItem | pass (chromium) | [#689](https://github.com/QRun-IO/qqq/issues/689) |
|
||||
| QRY-034 | Actions menu offers bulk load, bulk processes and table processes | Bulk Load/Edit/Edit With File/Delete then table processes; Launch carries the selection | Process min/max record checks block launch with a message | sample (person, pet, petNote, carrier, petSpecies) person | pass (chromium) | [#689](https://github.com/QRun-IO/qqq/issues/689) [#662](https://github.com/QRun-IO/qqq/issues/662) |
|
||||
| QRY-035 | Viewers get no create, bulk or process actions and the backend refuses them | No Create, bulk or write process entries | Direct insert/delete API calls return 403 | viewer persona on sample (person, pet, petNote, carrier, petSpecies) | pass (chromium) | [#689](https://github.com/QRun-IO/qqq/issues/689) [#671](https://github.com/QRun-IO/qqq/issues/671) |
|
||||
| QRY-040 | CSV export has visible columns in order and exactly the filtered, sorted rows | File name, headers and rows equal SQL; Join columns exported | Hidden columns excluded | QueryFixtures.java qryItem | pass (chromium) | [#688](https://github.com/QRun-IO/qqq/issues/688) |
|
||||
| QRY-041 | XLSX and JSON exports carry the same rows | XLSX sheet and JSON array equal SQL rows | JSON keys limited to exported fields | QueryFixtures.java qryItem | pass (chromium) | [#688](https://github.com/QRun-IO/qqq/issues/688) |
|
||||
| QRY-042 | Nothing to export disables the formats | Empty filter result disables CSV/XLSX/JSON | No export request sent | QueryFixtures.java qryItem | pass (chromium) | [#688](https://github.com/QRun-IO/qqq/issues/688) |
|
||||
| QRY-043 | Backend refuses exports of unreadable tables | Export of a readable table succeeds | Export of pet returns 403 without data | noPets persona | pass (chromium) | [#688](https://github.com/QRun-IO/qqq/issues/688) |
|
||||
| QRY-050 | Your saved views are listed and open on their own route | 'Your Saved Views' lists backend views; /savedView/{id} applies filter/columns | Empty group message | sample sharing demo (Alice People View) | pass (chromium) | [#687](https://github.com/QRun-IO/qqq/issues/687) |
|
||||
| QRY-051 | Saving a new view stores filter, sort, columns and page size | saved_view row viewJson matches Material RecordQueryView | Unsaved-changes indicator clears after save | sample (person, pet, petNote, carrier, petSpecies) person | pass (chromium) | [#687](https://github.com/QRun-IO/qqq/issues/687) |
|
||||
| QRY-052 | Update, rename and delete your view | Update keeps unrelated view JSON; Rename and delete change the saved_view row | Reset All Changes restores the stored view | sample (person, pet, petNote, carrier, petSpecies) person | pass (chromium) | [#687](https://github.com/QRun-IO/qqq/issues/687) |
|
||||
| QRY-053 | Duplicate names and unknown views are reported without changes | Duplicate name error shown in the dialog | Unknown view id shows an error; saved_view unchanged | sample (person, pet, petNote, carrier, petSpecies) person | pass (chromium) | [#687](https://github.com/QRun-IO/qqq/issues/687) |
|
||||
| QRY-054 | A view shared with you opens read-only and the backend refuses changes | Listed under 'Views Shared with you' for bob; Opens with its filter | Owner-only actions disabled; storeSavedView by bob refused | QueryFixtures.java (Alice People View shared with sample:bob READ_ONLY) | pass (chromium) | [#687](https://github.com/QRun-IO/qqq/issues/687) |
|
||||
| QRY-060 | Variant table asks for a variant, queries with it and remembers it | Picker lists /variants; Rows equal the chosen store; Choice remembered and changeable; export uses it | Filter narrows options | QueryFixtures.java qryStock on qryVariantMemory (qryStore) | pass (chromium) | [#690](https://github.com/QRun-IO/qqq/issues/690) |
|
||||
| QRY-061 | Dismissing the variant prompt queries nothing; backend requires a variant | Prompt shown with a choose button | No query sent; API query without a variant fails | QueryFixtures.java qryStock | pass (chromium) | [#690](https://github.com/QRun-IO/qqq/issues/690) |
|
||||
| QRY-062 | A table without count, export or write capabilities offers none of them | Rows listed without a total; Export disabled with explanation; No create or bulk actions | Backend insert refused | QueryFixtures.java qryLedger (no TABLE_INSERT/UPDATE/DELETE/EXPORT/COUNT) | pass (chromium) | [#671](https://github.com/QRun-IO/qqq/issues/671) [#688](https://github.com/QRun-IO/qqq/issues/688) |
|
||||
| QRY-063 | Enum-backed Pet Species table is read-only | Metadata omits write capabilities; no Create or bulk writes | Insert returns 403, not 500 | sample (person, pet, petNote, carrier, petSpecies) petSpecies | pass (chromium) | [#692](https://github.com/QRun-IO/qqq/issues/692) |
|
||||
| QRY-064 | Backend refuses count and export for a table without those capabilities | Query still succeeds | v1 count, legacy count and export return 403 | QueryFixtures.java qryLedger | pass (chromium) | [#693](https://github.com/QRun-IO/qqq/issues/693) |
|
||||
| QRY-065 | Export streams from the v1 export route | the export menu posts {format, filename, fieldNames, filter} to /qqq/v1/table/{table}/export and downloads every row; the v1 route streams the same rows as CSV | a table without the export capability is refused (403) by the v1 route | qryItem, qryLedger | pass (chromium) | [#699](https://github.com/QRun-IO/qqq/issues/699) [#406](https://github.com/QRun-IO/qqq/issues/406) |
|
||||
| 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-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) |
|
||||
| QRY-081 | Add quick filter accepts metadata fields and remembers a chosen field | A field offered by metadata appears as a chip after selection and reload; Joined and virtual fields query SQL-backed records | Field choice is scoped to this table | sample person and QueryFixtures.java qryItem/qryCaseItem | pass (chromium) | [#715](https://github.com/QRun-IO/qqq/issues/715) |
|
||||
| QRY-082 | Basic mode cannot simplify repeated or nested advanced conditions and explains why | Repeated Name conditions disable Basic with a focusable reason tooltip; URL criteria become chips; OR criteria open Advanced with a reason | Clicking Basic does not discard either condition | QueryFixtures.java qryItem | pass (chromium) | [#715](https://github.com/QRun-IO/qqq/issues/715) |
|
||||
| QRY-083 | Advanced query shows and removes individual conditions (Next inline editor) | Name and Quantity conditions filter SQL rows; deleting Name in the inline editor preserves Quantity | Removing one condition does not reset the whole query | QueryFixtures.java qryItem | pass (chromium) | [#715](https://github.com/QRun-IO/qqq/issues/715) |
|
||||
| QRY-084 | Sort picker changes field and direction; clearing filters requires confirmation and keeps the sort | Name ascending order matches SQL before and after confirmed clear; Condition status explains missing and complete values on keyboard focus | Cancel leaves conditions in place | QueryFixtures.java qryItem | pass (chromium) | [#715](https://github.com/QRun-IO/qqq/issues/715) |
|
||||
| QRY-085 | Weekday criteria use the metadata-enabled date function and ISO weekday values | Selecting a weekday sends WeekdayOfDate and returns the SQL-matching rows; Date-time weekday sends WeekdayOfDateTime with configured UTC arguments and returns matching rows | The weekday list uses names rather than bare numbers | QueryFixtures.java qryItem with dated rows and Material weekday settings; AcceptanceSampleServer.java H2 date functions | pass (chromium) | [#718](https://github.com/QRun-IO/qqq/issues/718) |
|
||||
| QRY-086 | Relative date presets expose their evaluated date and filter records | Seven days ago renders a live date and returns the recent SQL-backed rows; A date-time preset retains its relative expression, evaluates on focus and returns recent rows | A relative value remains an expression instead of fixed text | QueryFixtures.java qryItem with relative dates | pass (chromium) | [#718](https://github.com/QRun-IO/qqq/issues/718) |
|
||||
| QRY-087 | Bulk pasted values become a multi-value filter | Two pasted record names return the matching SQL-backed rows | The dialog previews each value before adding it | QueryFixtures.java qryItem | pass (chromium) | [#718](https://github.com/QRun-IO/qqq/issues/718) |
|
||||
| QRY-088 | Instance query help accompanies the bulk paste dialog | The dialog displays the configured help text | Help text comes from instance metadata | QueryFixtures.java instance-level QUERY_SCREEN help | pass (chromium) | [#718](https://github.com/QRun-IO/qqq/issues/718) |
|
||||
| QRY-089 | String filter input honors field case behavior from v1 metadata | Uppercase typing is normalized and queries the lowercase stored code | The normalized value remains visible in the input | QueryFixtures.java qryCaseItem code with TO_LOWER_CASE | pass (chromium) | [#718](https://github.com/QRun-IO/qqq/issues/718) |
|
||||
| QRY-008 | Phone card list: readable records, touch-sized selection, query selections and paging | One card per record with the SQL ids and names, values wrap instead of being cut off; Tapping the 44 px label around a card checkbox selects it without opening the record; A full-query selection checks every card; paging and rows per page work on cards; Tapping a card opens its record | No horizontal page scroll and no control below 44 x 44 px | sample (person, pet, petNote, carrier, petSpecies) carrier (11 rows), 412 px touch viewport | pass (chromium) | [#708](https://github.com/QRun-IO/qqq/issues/708) |
|
||||
| QRY-009 | Phone filter sheet fits the viewport, scrolls inside and manages focus | Filter opens a modal sheet that takes focus; Many conditions scroll inside the sheet, which stays inside the viewport; Cards behind the sheet show the filtered records (SQL); Escape or Close closes the sheet and returns focus to Filter, which counts the conditions | No horizontal page scroll and no control below 44 x 44 px in the sheet | QueryFixtures.java qryItem, 412 px touch viewport | pass (chromium) | [#708](https://github.com/QRun-IO/qqq/issues/708) |
|
||||
| QRY-024 | Column configuration is reachable and usable by touch on phones and tablets | The panel opens inside the viewport with touch-sized controls; Hiding a column removes it from the cards (phone) or the grid (tablet) and persists; Escape closes the panel and returns focus to its button | The panel never runs off the left edge of a phone | QueryFixtures.java qryItem | pass (chromium) | [#708](https://github.com/QRun-IO/qqq/issues/708) |
|
||||
| QRY-036 | Bulk actions launch from a phone card selection | Checked cards show the selection banner; The Actions menu fits the phone; Bulk Edit launches with exactly the checked ids | No horizontal page scroll while the menu is open | QueryFixtures.java qryItem, 412 px touch viewport | pass (chromium) | [#708](https://github.com/QRun-IO/qqq/issues/708) |
|
||||
| QRY-055 | Saved views and export menus fit the phone and are touch-sized | The saved views menu lists your view inside the viewport; Escape closes it; The export menu fits the phone and downloads a CSV | No horizontal page scroll and no control below 44 x 44 px in either menu | sample sharing demo (Alice People View), 412 px touch viewport | pass (chromium) | [#708](https://github.com/QRun-IO/qqq/issues/708) |
|
||||
| REL-001 | Possible-value cells show the related record label and link to it | Owner cell shows the person label; Link opens /app/person/{id} | Enum values are not links | sample (person, pet, petNote, carrier, petSpecies) pet.personId | pass (chromium) | [#691](https://github.com/QRun-IO/qqq/issues/691) |
|
||||
| REL-002 | Parent lists exactly its own children; View All opens the filtered child list | Panel rows equal SQL children; View All filters /app/pet and Back returns | Parent without children shows an empty panel | sample (person, pet, petNote, carrier, petSpecies) person -> pets | pass (chromium) | [#686](https://github.com/QRun-IO/qqq/issues/686) |
|
||||
| REL-003 | Adding a child from its parent links it and shows it immediately | New pet stored with person_id of the parent; Panel refreshes | Join field not editable; no other rows changed | sample (person, pet, petNote, carrier, petSpecies) person -> pets | pass (chromium) | |
|
||||
| REL-004 | Three-level associations: each pet shows only its own notes | Notes equal SQL per pet | Other pets' notes absent | sample (person, pet, petNote, carrier, petSpecies) person -> pet -> petNote | pass (chromium) | |
|
||||
| REL-005 | Editing and deleting a child leaves no stale parent, list or unexpected rows | Renamed pet shown in parent panel and list; Deleting a pet removes its notes | Other rows unchanged | sample (person, pet, petNote, carrier, petSpecies) pet, petNote | pass (chromium) | |
|
||||
| REL-006 | Denied child reads show an unavailable panel without data or actions | Panel says related records are unavailable | No data, View All or Create; API query 403 | noPets persona; qryShelter whose associated qryShelterPet is denied with DenyBehavior.DISABLED | pass (chromium) | [#671](https://github.com/QRun-IO/qqq/issues/671) |
|
||||
| REL-007 | Aliased and composite associations bind to their widgets and list the right members | childRecordList and rowBuilder show SQL-matching members; Composite join on code + review date | Household without members shows empty | QueryFixtures.java qryHousehold/qryMember (members, care group / primary, scheduled reviews) | pass (chromium) | |
|
||||
| REL-008 | Adding through a composite association stores every join value | New member stored with household_code and review_date from the parent | Aliased association unaffected; other rows unchanged | QueryFixtures.java qryHousehold/qryMember | pass (chromium) | |
|
||||
| REL-009 | Deleting a parent removes associated children and leaves other parents intact | Person and its pets removed | Other pets and notes unchanged; list reflects SQL | sample (person, pet, petNote, carrier, petSpecies) person -> pets | pass (chromium) | |
|
||||
|
||||
## Records, fields, editors, adornments, validation and persistence
|
||||
|
||||
| ID | Feature | Scenarios | Negative | Fixture | Result | Issues |
|
||||
|---|---|---|---|---|---|---|
|
||||
| REC-001 | Record view shows the record label, tiered sections and backend-formatted values | label heading; T1 header fields; T2 section cards; currency, grouped integer, Yes/No, date | values match independent SQL readback | prime-test-database.sql (person 1) | pass (chromium) | |
|
||||
| REC-002 | Empty values render a placeholder; hidden fields and hidden sections are never shown | NULL date shows an em dash; hidden field absent from view and from the GET payload; hidden section not rendered | hidden value stays stored (SQL) | person 4 (NULL birth date); recordLab hiddenCode + hidden legacy section | pass (chromium) | [#677](https://github.com/QRun-IO/qqq/issues/677) [#680](https://github.com/QRun-IO/qqq/issues/680) |
|
||||
| REC-003 | Record view of a missing record shows Record Not Found and a way back | not-found panel; back to the table list | 404 read is the only failed request; no generic error toast | person 999 (absent) | pass (chromium) | [#673](https://github.com/QRun-IO/qqq/issues/673) |
|
||||
| REC-004 | Record view tabs and list view expose every visible section | Overview shows all T2 sections; Overview field rows retain original Next typography and spacing in light and dark themes; a section tab shows only that section; list view shows every section; Arrow/Home/End keys move tab focus; Enter/Space activates a named panel with one tab stop; phone accordion supports keyboard activation | | fieldLab record created in the test | pass (chromium) | |
|
||||
| REC-005 | Create form validates required fields in the browser before any request | required markers and messages for each required field | no insert request, no row | person | pass (chromium) | |
|
||||
| REC-006 | Create persists the entered values and opens the new record | success toast; navigates to /app/person/<new id>; view shows the entered values | non-editable fields are not offered on create; row matches SQL readback | person | pass (chromium) | |
|
||||
| REC-007 | Backend enforces required fields when the UI is bypassed | direct insert without Name is rejected with the backend message | no row written | fieldLab | pass (chromium) | |
|
||||
| REC-008 | Cancel from create and edit, with the unsaved-changes guard | clean cancel returns immediately; dirty cancel asks Stay/Leave; Stay keeps typed values; Leave discards | no insert or update request; no row created or changed | person | pass (chromium) | |
|
||||
| REC-009 | Edit loads stored values, saves only what changed, and shows the updated record | fields prefilled from the record; Save disabled until a change; view shows the new value without reload | only the changed field is submitted; other columns unchanged in SQL | person 1 | pass (chromium) | [#672](https://github.com/QRun-IO/qqq/issues/672) [#676](https://github.com/QRun-IO/qqq/issues/676) |
|
||||
| REC-010 | Edit shows non-editable fields read-only and never submits them | read-only controls with the stored values | controls are disabled; create date unchanged in SQL after save | person 1 (id, createDate, modifyDate) | pass (chromium) | [#680](https://github.com/QRun-IO/qqq/issues/680) [#537](https://github.com/QRun-IO/qqq/issues/537) |
|
||||
| REC-011 | Edit of a missing record shows the load failure instead of a form | error message, no form fields | 404 is the only failed request | person 999 | pass (chromium) | [#673](https://github.com/QRun-IO/qqq/issues/673) |
|
||||
| REC-012 | Delete asks for confirmation, removes the record and returns to the list without errors | dialog names the record; Cancel keeps it; Delete removes it and returns to the list; success toast | no Resource not found toast; no 404 read of the deleted record; row gone in SQL | person 5 | pass (chromium) | [#541](https://github.com/QRun-IO/qqq/issues/541) |
|
||||
| REC-013 | Copy prefills a new record from the source and saves it as a separate record | source values prefilled without the key; Full Copy loads the descendant draft on selection; returning to Base Copy preserves the base form; save creates a new row and opens it; source unchanged | unique-key conflict on copy shows the backend message and keeps the form; download-URL files are not copied | person 1; recordLab 1 | pass (chromium) | [#672](https://github.com/QRun-IO/qqq/issues/672) |
|
||||
| REC-014 | STRING and TEXT editors round-trip single and multi-line text | single-line input; multi-line textarea keeps line breaks in view and SQL | | fieldLab | pass (chromium) | |
|
||||
| REC-015 | INTEGER, LONG and DECIMAL editors keep exact values | LONG beyond 2^53 saved and shown exactly; DECIMAL with 4 places | fractional LONG rejected in the browser; backend rejects a fractional LONG sent directly | fieldLab | pass (chromium) | [#679](https://github.com/QRun-IO/qqq/issues/679) |
|
||||
| REC-016 | BOOLEAN editor applies the metadata default and saves true/false | create form starts at the default (true); switching to false persists false; view shows Yes/No | unchanged boolean is not rewritten on edit | fieldLab (booleanValue default true) | pass (chromium) | [#680](https://github.com/QRun-IO/qqq/issues/680) |
|
||||
| REC-017 | DATE and TIME editors round-trip values and clearing | date input value and view; time input value and backend 12-hour display; clearing a date stores NULL | | fieldLab | pass (chromium) | |
|
||||
| REC-018 | DATE_TIME is entered and shown in the viewer's time zone and stored in UTC | typed local time stored as the UTC instant; view shows yyyy-MM-dd hh:mm:ss AM EDT; edit form shows the local time | unchanged date-times are not rewritten; non-editable date-times read-only | fieldLab; browser zone America/New_York | pass (chromium) | [#679](https://github.com/QRun-IO/qqq/issues/679) |
|
||||
| REC-019 | Zoned DATE_TIME display behaviors (fixed zone and per-record zone) | fixed America/Chicago display; display in the record's own timeZone; daylight-saving boundary | invalid zone falls back to UTC | fieldLab | pass (chromium) | [#679](https://github.com/QRun-IO/qqq/issues/679) |
|
||||
| REC-020 | HTML field edits in the rich-text editor and renders sanitized | formatted text rendered on the view | script content is not executed | fieldLab htmlValue | pass (chromium) | [#676](https://github.com/QRun-IO/qqq/issues/676) |
|
||||
| REC-021 | PASSWORD is masked on read and never overwritten by the mask | view shows the mask; typing a new password saves it | editing another field keeps the stored password (SQL) | fieldLab passwordValue | pass (chromium) | [#672](https://github.com/QRun-IO/qqq/issues/672) |
|
||||
| REC-022 | BLOB upload and download without a download adornment | file chosen on create is stored byte-for-byte; view offers the bytes as a download | | fieldLab blobValue | pass (chromium) | [#677](https://github.com/QRun-IO/qqq/issues/677) |
|
||||
| REC-023 | Dynamic default values (user id, create day, modify day, explicit NONE) | user id and dates filled on create; modify day updated on edit, create day kept | NONE leaves the value empty | fieldLab | pass (chromium) | |
|
||||
| REC-024 | Max-length policies: truncate, ellipsis, pass-through and reject | truncate, ellipsis and pass-through stored as declared | reject shows the backend message, keeps the form and writes nothing | fieldLab (maxLength 8) | pass (chromium) | [#680](https://github.com/QRun-IO/qqq/issues/680) |
|
||||
| REC-025 | Case and whitespace normalization on save | upper, lower, trim variants, remove-all-whitespace and unchanged stored as declared | | fieldLab | pass (chromium) | |
|
||||
| REC-026 | Numeric range policies: reject, exclusive reject, inclusive clip and exclusive clip | clip to 0/100 and 0.01/99.99 | out-of-range and exclusive-boundary values show the backend message and write nothing | fieldLab | pass (chromium) | [#673](https://github.com/QRun-IO/qqq/issues/673) |
|
||||
| REC-027 | Unique keys are enforced with the backend message on create and edit | distinct values save | duplicate Name on create; duplicate normalized key on edit; form keeps values, row unchanged | fieldLab (name, normalizedKey) | pass (chromium) | [#673](https://github.com/QRun-IO/qqq/issues/673) |
|
||||
| REC-028 | LINK adornment: record links and URL links | toRecordFromTable opens the referenced record; URL opens in a new tab (target _blank) | a non-http value is not turned into a link | recordLab (ownerId, website) | pass (chromium) | [#677](https://github.com/QRun-IO/qqq/issues/677) |
|
||||
| REC-029 | CHIP adornment colors and icons by value | label is the display value; color from color.<value>; icon from icon.<value> | undeclared value uses the default color | recordLab status | pass (chromium) | [#677](https://github.com/QRun-IO/qqq/issues/677) |
|
||||
| REC-030 | SIZE adornment sets the query grid column width | column is 100px wide | undeclared columns keep the default width | recordLab shortCode (width small) | pass (chromium) | [#677](https://github.com/QRun-IO/qqq/issues/677) |
|
||||
| REC-031 | CODE_EDITOR adornment: code view with formatting and a code editor on forms | view shows the code and its language; Format JSON / Reset Format; editor saves edited code | invalid JSON reports a formatting error | recordLab config (json) | pass (chromium) | [#677](https://github.com/QRun-IO/qqq/issues/677) |
|
||||
| REC-032 | RENDER_HTML adornment renders sanitized HTML | markup rendered | script removed and not executed | recordLab htmlNote | pass (chromium) | [#677](https://github.com/QRun-IO/qqq/issues/677) |
|
||||
| REC-033 | REVEAL adornment masks, reveals and copies the value | masked by default; reveal shows the exact stored value; hide masks again | | recordLab apiToken (PASSWORD + REVEAL) | pass (chromium) | [#677](https://github.com/QRun-IO/qqq/issues/677) |
|
||||
| REC-034 | FILE_DOWNLOAD adornment links to the stored file with its name | file name from the companion field; file name from the format; open and download return the stored bytes | record without a file shows no link | recordLab attachment (heavy, fileNameField) and notesFile (fileNameFormat) | pass (chromium) | [#677](https://github.com/QRun-IO/qqq/issues/677) |
|
||||
| REC-035 | FILE_UPLOAD adornment formats, replacing and removing a stored file | drag-and-drop zone and button formats; current file shown on edit; new file replaces bytes and file name | removing the file clears bytes and name | recordLab attachment (dragAndDrop), notesFile (button) | pass (chromium) | [#677](https://github.com/QRun-IO/qqq/issues/677) [#680](https://github.com/QRun-IO/qqq/issues/680) |
|
||||
| REC-036 | TOOLTIP adornment shows its text on the value | tooltip on hover and keyboard focus | | recordLab hint (staticText) | pass (chromium) | [#677](https://github.com/QRun-IO/qqq/issues/677) [#685](https://github.com/QRun-IO/qqq/issues/685) |
|
||||
| REC-037 | WIDGET adornment renders the field value as the named widget | widget content from the record | the value is not offered as an editable field | recordLab summaryWidget | pass (chromium) | [#677](https://github.com/QRun-IO/qqq/issues/677) |
|
||||
| REC-038 | ERROR adornment highlights the value | value shown with error styling | | recordLab problem | pass (chromium) | [#677](https://github.com/QRun-IO/qqq/issues/677) |
|
||||
| REC-039 | Field help content follows screen roles and formats | view: READ_SCREENS text and VIEW_SCREEN HTML; edit: WRITE_SCREENS markdown rendered and EDIT_SCREEN text; create: INSERT_SCREEN text | help for other screens is not shown | recordLab title/website/ownerId help | pass (chromium) | [#681](https://github.com/QRun-IO/qqq/issues/681) [#406](https://github.com/QRun-IO/qqq/issues/406) |
|
||||
| REC-040 | Section help content on the record view | HTML help under the section heading | | recordLab presentation section | pass (chromium) | [#681](https://github.com/QRun-IO/qqq/issues/681) |
|
||||
| REC-041 | Table help content slots are delivered in table metadata | v1 table metadata carries the slot help | | recordLab summary slot | pass (chromium) | |
|
||||
| REC-042 | Audit history lists who changed what, newest first | edit then open Audit: message, user and per-field sentences; audit rows independently verified in SQL | unaudited record shows no audits | recordLab (FIELD audits) | pass (chromium) | [#678](https://github.com/QRun-IO/qqq/issues/678) |
|
||||
| REC-043 | Audit history without process permission reads the audit table | same entries via the audit table query | no process request is made | recordLab; persona noProcesses | pass (chromium) | [#678](https://github.com/QRun-IO/qqq/issues/678) |
|
||||
| REC-044 | Record label format is used for the record's title and dialogs | heading; delete dialog; audit title | | recordLab (Lab: %s) | pass (chromium) | [#678](https://github.com/QRun-IO/qqq/issues/678) |
|
||||
| REC-045 | Possible-value editor shows stored labels, looks up defaults and selects by keyboard | edit shows the stored label, not the id; create default shows its label; keyboard search and select saves the id | | recordLab status/ownerId | pass (chromium) | [#667](https://github.com/QRun-IO/qqq/issues/667) |
|
||||
| REC-046 | Read-only persona sees no write actions and the backend refuses writes | no Edit, Copy or Delete; create and edit URLs show the permission message | direct insert, update and delete are 403 and change nothing | persona viewer | pass (chromium) | |
|
||||
| REC-047 | Forms are labelled and keyboard operable | every control has a label; keyboard-only create saves the record | invalid fields expose aria-invalid and a described error | person | pass (chromium) | [#676](https://github.com/QRun-IO/qqq/issues/676) |
|
||||
| REC-048 | Server validation errors show the backend message once and keep the form | alert and toast show the backend message; typed values remain | no generic Something went wrong toast | fieldLab | pass (chromium) | [#673](https://github.com/QRun-IO/qqq/issues/673) |
|
||||
| REC-049 | pages of a table outside the app tree use the table label in the document title and breadcrumbs | create page title is "Create Scheduled Report \| Scheduled Report \| QQQ Sample"; record view title is the record label, then Scheduled Report | the table name (scheduledReport) is not shown in the breadcrumbs | scheduledReport (memory, not in the app tree) for savedReport 1 | pass (chromium) | [#707](https://github.com/QRun-IO/qqq/issues/707) |
|
||||
| REC-051 | Table-field possible values search and resolve through the v1 API | owner search sends a v1 POST with the search term and lists the exact match; ids and labels resolve to the exact option; the chosen owner persists | a source table the persona cannot read is refused (403) by the v1 route; an unknown field is 404 | recordLab | pass (chromium) | [#699](https://github.com/QRun-IO/qqq/issues/699) [#406](https://github.com/QRun-IO/qqq/issues/406) |
|
||||
| REC-050 | Record create, view, edit and delete run on the v1 record routes | the create/edit/delete flow sends POST, GET, PATCH and DELETE to /qqq/v1/table/... and persists each step; record-v1 associations are written and read back with includeAssociations | a missing required field is a 400 with the backend message and nothing is written; a read-only persona is refused (403) insert, update and delete on v1 and the row is unchanged | person, fieldLab, qryHousehold | pass (chromium) | [#699](https://github.com/QRun-IO/qqq/issues/699) [#406](https://github.com/QRun-IO/qqq/issues/406) |
|
||||
| REC-056 | Record view hash links: #audit, #/launchProcess={process} and #{section} anchors | #audit opens the audit dialog; closing it clears the hash and a reload does not reopen it; #/launchProcess=person.bulkEdit replaces the URL with the run for the record (init recordIds=2); Cancel returns to the record; #employmentInfo selects the Employment Info tab and scrolls it into view; a later hash change selects Dates | | sample person, person.bulkEdit | pass (chromium) | [#714](https://github.com/QRun-IO/qqq/issues/714) |
|
||||
| REC-057 | Create presets from links: #/createChild= opens a child create dialog over the record; /create#/defaultValues=/disabledFields= prefills and locks | the backend-format #/createChild=pet link on person 3 opens Add Pet with Person preset and disabled; Create closes the dialog, clears the hash, submits personId=3 and SQL shows the pet linked to person 3; /app/pet/create#/defaultValues={name, personId}/disabledFields={personId} prefills both, locks Person and saves them (SQL); /app/pet/create#defaultValues={speciesId} (no slash) prefills Species without locking | locked fields are disabled; unlocked preset fields stay editable | sample person -> pet (personId), petSpecies | pass (chromium) | [#714](https://github.com/QRun-IO/qqq/issues/714) |
|
||||
| REC-052 | Dependent possible values: a field whose filter uses ${input.otherField} offers only the choices matching the form's current values | every search posts the form values as the values map of the v1 JSON body; create: choices narrow to the chosen Category and change when it changes; edit: the stored Item label shows, a new Category changes the choices; the chosen id persists (SQL) and the record view shows its label | Category empty: the unset variable makes no matches for a form, shown as No options found; an item outside the filter is not offered, even when searched by name; the backend refuses an out-of-filter id lookup and returns only the filtered choices | recPvPick itemId (recPvItem source filtered on categoryId = ${input.categoryId}) | pass (chromium) | [#714](https://github.com/QRun-IO/qqq/issues/714) |
|
||||
| REC-053 | Table developer view shows the API Docs & Playground section for the table's application APIs | developer page for a table keeps its metadata sections and shows the API Docs & Playground section; with no application APIs it shows "This table is not available in any APIs." and no API or Version selectors; OpenAPI download and new-tab buttons remain fully visible before interaction at desktop, phone and 320px widths in light/dark modes; pointer/touch downloads yield the real specification and keyboard navigation opens its URL; specification labels fit wider application font metrics at desktop and phone widths without content clipping | a viewer persona sees the same empty state and cannot fetch any table spec | recordLab has no application APIs; person is exposed through the owned Acceptance API | pass (chromium) | [#714](https://github.com/QRun-IO/qqq/issues/714) |
|
||||
| REC-054 | Record developer view manages associated scripts: versions, new versions, test runs, logs, docs and script creation | versions list with CURRENT on the current revision and per-file code for the selected version; an older version offers Edit and Activate; save a new version with a commit message (SQL: new script_revision row, its file and script.current_script_revision_id; persists after reload); test run output and log lines equal the backend testScript response; a failing run shows the error the backend returns; logs list the script_log rows of the selected version with their lines; docs show the script type help text and sample code; create a script for an empty associated script field; local QQQ helper and keyword suggestions support keyboard and pointer/touch insertion, native undo/redo, both-theme accessibility, and SQL-verified revision persistence | a viewer cannot save or test a revision (no Edit or Test offered; the backend refuses storeScriptRevision and testScript); a viewer cannot create a script (no Create Script offered; the backend refuses the create route); a table without associated scripts shows none | scriptLab (RecordsFixtures: Alpha with script 101, revisions 101-102, logs 101-102; Beta without a script) on the scripts tables from WidgetsFixtures | pass (chromium) | [#714](https://github.com/QRun-IO/qqq/issues/714) |
|
||||
| REC-055 | Record view keyboard shortcuts n, e, c, d, a (Material) and their help text | e, c and n open edit, copy and create with the right values; a opens the audit history; d opens the delete confirmation, Escape keeps the row (SQL), confirming deletes it (SQL); ? lists Create a New Record, Edit/Copy/Delete/Audit the current Record with their keys | no shortcut fires while the audit or keyboard help dialog is open; keys typed in the Jump to input are text; viewer persona: n, e, c, d do nothing (a still works); the backend refuses insert and delete (403, SQL unchanged) | sample person | pass (chromium) | [#714](https://github.com/QRun-IO/qqq/issues/714) |
|
||||
| REC-058 | Record view on phones and tablets: the title stays readable, the header controls wrap onto their own row, and the phone action sheet fits, scrolls and keeps focus | The title stays on one line and never overlaps the view toggle, Audit or record actions; neither the page nor its main region scrolls sideways; The back link, value links and header controls are touch-sized (44 x 44 px) under a coarse pointer; The phone Actions sheet fits the screen, takes focus, keeps Tab and Shift+Tab inside, closes by Escape or a backdrop tap with focus back on its trigger, and runs Edit | A read-only user gets no phone Actions trigger (asserted in REC-046) | recordLab 1 (Lab: Alpha); phone (412 px touch), tablet (810 px touch) and desktop viewports | pass (chromium) | [#708](https://github.com/QRun-IO/qqq/issues/708) |
|
||||
| REC-059 | Record forms on phones and tablets: every editor type fits without sideways scrolling, controls are touch-sized, possible-value lists open on screen and the phone action bar stays reachable | Create Field Lab: no sideways scroll, every control at least 44 x 44 px by touch; on a phone Save stays on screen; boolean, date, time, text and rich text values persist; Edit Record Lab: the owner list opens inside the screen with touch-sized options; the picked owner persists | | fieldLab create (every field type), recordLab 1 edit (possible values, code editor, password reveal, file uploads) | pass (chromium) | [#708](https://github.com/QRun-IO/qqq/issues/708) |
|
||||
| REC-061 | An editable dynamicForm widget on a record form validates its fields and persists them in the declared JSON record field | the region field starts from stored JSON and appears in the record edit form; changing the region persists the new JSON in inputValues and survives reload | clearing the required region blocks the write; the transient region field is absent from the table multipart payload | accWidgetHost 1 with accHostRecordVariables and hidden inputValues field | pass (chromium) | [QRun-IO/qqq-frontend-next#722](https://github.com/QRun-IO/qqq-frontend-next/issues/722) |
|
||||
| REC-062 | Inline enum possible values filter by label prefix without a lookup request; CHIP adornments style options and chosen IDs persist | v1 metadata exposes the inline enum and the edit form shows the held label; a label-prefix search filters options and the CHIP adornment colors and icons them; choosing High stores HIGH in SQL and shows High after reload | a substring that is not a prefix offers no options; the picker makes no possible-values request | recordLab priority field with an inline enum and CHIP adornment | pass (chromium) | [QRun-IO/qqq-frontend-next#721](https://github.com/QRun-IO/qqq-frontend-next/issues/721) |
|
||||
| REC-060 | Material table and field form adjusters and field rules operate against the real QQQ sample server | v1 table and field metadata expose the Material supplemental adjuster settings; the table onLoad route supplies a note before render; the name onChange route changes a field label and clears its value; the field rule clears status | a backend lock message disables Save for a preset name | FormAdjusterFixtures.java adjusterLab table with a table onLoad adjuster, name onChange adjuster and clear-target field rule | pass (chromium) | [QRun-IO/qqq-frontend-next#720](https://github.com/QRun-IO/qqq-frontend-next/issues/720) |
|
||||
| REC-064 | Record sections render qualified fields from readable exposed joins | record GET sends a selected LEFT join with qryItemJoinPerson; the joined person's first name appears in the Details section | unreadable join paths are removed from sections and GET descriptors by the same permission filter as query pages | QueryFixtures.java qryItem joins person and shows person.firstName in Details | pass (chromium) | [QRun-IO/qqq-frontend-next#722](https://github.com/QRun-IO/qqq-frontend-next/issues/722) |
|
||||
| REC-063 | Record view, edit and copy load from the table variant selected on the query screen | Selecting South Store shows S-KIWI on the record view and edit/copy forms; Every South record GET includes tableVariant {type: qryStore, id: 2}; Switching to North Store shows N-APPLE for the same record ID with variant id 1 | The duplicate ID must not show North Store's N-APPLE value | QueryFixtures.java qryStock (same record ID in North and South variants) | pass (chromium) | [QRun-IO/qqq-frontend-next#722](https://github.com/QRun-IO/qqq-frontend-next/issues/722) |
|
||||
| REC-065 | Unchanged DATE_TIME instants and precision survive edit and copy | Unrelated edit omits unchanged timestamp; Copy sends the original zoned instant and preserves source/copy SQL timestamps | Copy must not select the first occurrence of a repeated hour or truncate fractional seconds | fieldLab; browser America/New_York; repeated DST hour with subsecond precision | pass (chromium) | [#679](https://github.com/QRun-IO/qqq/issues/679) |
|
||||
|
||||
## Permissions, sessions, authentication and interaction quality
|
||||
|
||||
| ID | Feature | Scenarios | Negative | Fixture | Result | Issues |
|
||||
|---|---|---|---|---|---|---|
|
||||
| SEC-001 | Table hidden from a user (DenyBehavior.HIDDEN): absent from navigation, direct link loads nothing, backend refuses | noPets sees Person but no Pet / Pet Note in navigation; person record neither shows nor requests the hidden pet association | direct link /app/pet shows unknown resource with no data request; API query/get of pet returns 403, table metadata 404 | tests/acceptance/fixture/AcceptanceSampleServer.java personas (noPets) | pass (chromium) | [#671](https://github.com/QRun-IO/qqq/issues/671) |
|
||||
| SEC-002 | Table disabled for a user (DenyBehavior.DISABLED): listed, but list/record/create/edit links explain the denial without loading data | admin lists the ledger rows; noPets sees the Security Ledger link | noPets list, record, create and edit links show the permission message and request no records; API query/get/insert/update return 403 and rows are unchanged | tests/acceptance/fixture/SecurityFixtures.java via SecurityAcceptanceServer (security variant) (securityLedger, base name pet, noPets) | pass (chromium) | [#671](https://github.com/QRun-IO/qqq/issues/671) |
|
||||
| SEC-003 | Insert permission denied: create not offered, create link refused, backend writes nothing | viewer list has no Create button and the dashboard has no Create quick action | /app/person/create shows the permission message with no form; POST /data/person returns 403 and the person count is unchanged | tests/acceptance/fixture/AcceptanceSampleServer.java personas (viewer) | pass (chromium) | |
|
||||
| SEC-004 | Edit permission denied: edit not offered, edit link refused, backend changes nothing | viewer record view has no Edit action | /app/person/1/edit shows the permission message without loading the record; PUT /data/person/1 returns 403 and the name is unchanged | tests/acceptance/fixture/AcceptanceSampleServer.java personas (viewer) | pass (chromium) | |
|
||||
| SEC-005 | Delete permission denied: delete not offered on record or bulk bar, bulk delete refused, backend deletes nothing | record view and bulk action bar have no Delete | /app/person.bulkDelete is unknown to the viewer; DELETE /data/person/1 and bulkDelete init return 403; the row remains | tests/acceptance/fixture/AcceptanceSampleServer.java personas (viewer) | pass (chromium) | |
|
||||
| SEC-006 | Copy requires insert permission: copy not offered and copy link refused | viewer record view has no Copy action | /app/person/1/copy shows the permission message; nothing is inserted | tests/acceptance/fixture/AcceptanceSampleServer.java personas (viewer) | pass (chromium) | |
|
||||
| SEC-007 | Process permission denied (hidden): processes not offered in navigation or launcher, links do not start them, backend refuses | viewer has no Actions menu; noProcesses' Actions menu offers only the table's bulk actions (table rights) and no standalone process; no process links; noProcesses keeps table rights (Create offered) | /app/clonePeople and /app/greetInteractive are unknown and make no process request; legacy and v1 process init return 403; no person is cloned | tests/acceptance/fixture/AcceptanceSampleServer.java personas (viewer, noProcesses) | pass (chromium) | |
|
||||
| SEC-008 | Process disabled for a user (DenyBehavior.DISABLED): shown disabled, link explains the denial, backend refuses with no side effect | admin runs Security Audit and one audit row is written; viewer sees the audit tile disabled and no dashboard quick action for it | viewer direct link shows the permission message and never calls init; process init returns 403 and the audit log stays empty | tests/acceptance/fixture/SecurityFixtures.java via SecurityAcceptanceServer (security variant) (securityAudit process) | pass (chromium) | [#671](https://github.com/QRun-IO/qqq/issues/671) |
|
||||
| SEC-009 | Widget restrictions: hidden widgets absent, disabled widgets show a permission state without loading data, backend refuses | admin sees both pet widgets with their content; noPets sees the open bulletin widget | noPets: hidden widget absent, disabled widget shows "You do not have permission to view this data." and no /widget request; GET /widget/<denied> returns 403 | tests/acceptance/fixture/SecurityFixtures.java via SecurityAcceptanceServer (security variant) (securityPetWidget, securityPetDisabledWidget) | pass (chromium) | [#671](https://github.com/QRun-IO/qqq/issues/671) |
|
||||
| SEC-010 | App restrictions: hidden apps absent from navigation and unknown by link; disabled apps keep their children gated | admin sees both pet apps | noPets: Pet Vault App absent and /app/securityPetApp unknown; Pet Disabled App children stay permission-gated | tests/acceptance/fixture/SecurityFixtures.java via SecurityAcceptanceServer (security variant) (securityPetApp hidden, securityPetDisabledApp disabled) | pass (chromium) | [#651](https://github.com/QRun-IO/qqq/issues/651) [#671](https://github.com/QRun-IO/qqq/issues/671) |
|
||||
| SEC-011 | Record-level security (sharing demo): users see only their own or shared saved views/reports; direct links and writes to others are refused | alice lists and opens her own saved view | bob's list omits alice's view; bob's direct link shows not found; bob's GET/PUT/DELETE of alice's view are refused and the row is unchanged | tests/acceptance/fixture/AcceptanceSampleServer.java personas (alice, bob) | pass (chromium) | |
|
||||
| SEC-012 | Record security lock on a data table: list filtered to the key, other users' records unreachable, forged owner refused | alice sees and edits only her notes (persisted) | bob's direct link to alice's note shows not found; bob's update/delete of alice's note and insert with alice as owner are refused; SQL unchanged | tests/acceptance/fixture/SecurityFixtures.java via SecurityAcceptanceServer (security variant) (securityNote locked by sample user) | pass (chromium) | |
|
||||
| SEC-013 | Field protection: hidden fields never shown or returned, password fields masked and never overwritten by the mask, heavy fields not loaded in lists | REVEAL password can be revealed on request; editing another field keeps the stored password | hidden value absent from list, record, edit form and API responses; masked password shows only the mask; the list query does not carry the heavy BLOB | tests/acceptance/fixture/SecurityFixtures.java via SecurityAcceptanceServer (security variant) (securityVault) | pass (chromium) | [#672](https://github.com/QRun-IO/qqq/issues/672) |
|
||||
| SEC-014 | Capability restrictions: tables without insert/update/delete capabilities offer none of them and refuse direct links | admin can list and view the archive | no Create/Edit/Delete offered; create and edit links explain the table cannot be changed; backend insert/update/delete are refused; SQL unchanged | tests/acceptance/fixture/SecurityFixtures.java via SecurityAcceptanceServer (security variant) (securityArchive) | pass (chromium) | [#671](https://github.com/QRun-IO/qqq/issues/671) |
|
||||
| SEC-015 | Permission revoked mid-session: stale UI actions are refused by the backend with a clear message, nothing is written, reload reflects the new rights | after reload the viewer rights are shown | saving an open edit form after revocation shows a permission error and the row is unchanged | tests/acceptance/fixture/AcceptanceSampleServer.java personas (admin then viewer) | pass (chromium) | [#673](https://github.com/QRun-IO/qqq/issues/673) |
|
||||
| SEC-020 | MOCK sign-in: a deep link establishes the session and shows the session user from manageSession values | lands on the requested page with rows; user menu shows Alice (sample) / sample:alice | | tests/acceptance/fixture/AcceptanceSampleServer.java personas | pass (chromium) | [#332](https://github.com/QRun-IO/qqq/issues/332) [#669](https://github.com/QRun-IO/qqq/issues/669) |
|
||||
| SEC-021 | Logout: POST /qqq/v1/logout ends the session, protected pages stay on login, Sign in returns to the page, per-user client data is cleared | Sign in returns to the original page | after logout a protected page (full load) stays on login and reads no data; a different user signing in on the same browser never sees the previous user's cached pages or recent records | tests/acceptance/fixture/AcceptanceSampleServer.java personas | pass (chromium) | [#669](https://github.com/QRun-IO/qqq/issues/669) |
|
||||
| SEC-022 | Session expiry: 401 redirects to login with returnTo and re-authenticates back to the page (initial load and mid-use) | expired on first load returns to the page; expired mid-use returns to the clicked record | no hang on "Setting up anonymous session" | tests/acceptance/fixture/AcceptanceSampleServer.java personas (expired) | pass (chromium) | [#669](https://github.com/QRun-IO/qqq/issues/669) |
|
||||
| SEC-023 | Denied sign-in: a 401 from manageSession keeps the user out with an explanation (no silent anonymous fallback) | | login shows "Sign-in was denied", no navigation or data is rendered | tests/acceptance/fixture/AcceptanceSampleServer.java personas | pass (chromium) | [#669](https://github.com/QRun-IO/qqq/issues/669) |
|
||||
| SEC-024 | Post-login returnTo is limited to in-app paths (open-redirect guard) | in-app returnTo with query string is honoured | absolute, protocol-relative, backslash and javascript: targets land on /app; paths that normalize to another origin (/.//host, /app/..//host, /app/../\host) land on /app | tests/acceptance/fixture/AcceptanceSampleServer.java personas | pass (chromium) | [#669](https://github.com/QRun-IO/qqq/issues/669) [#696](https://github.com/QRun-IO/qqq/issues/696) |
|
||||
| SEC-025 | OAUTH2 authorization code + PKCE sign-in with an owned OIDC provider (discovery, authorize, callback, backend token exchange, session, reload resume) | authorize request carries S256 challenge, state, client id, scopes and /token redirect; backend exchanges code + verifier with client secret; user_session row stored; provider identity is displayed; reload resumes without the provider | | tests/acceptance/support/fake-oidc.ts + SecurityAcceptanceServer -Dqqq.security.auth=OAUTH2 | pass (chromium) | [#670](https://github.com/QRun-IO/qqq/issues/670) [#406](https://github.com/QRun-IO/qqq/issues/406) |
|
||||
| SEC-026 | OAUTH2 sign-in failures: provider denial, tampered state and failed code exchange are reported and create no session | | a state-matched access_denied shows the provider description once; a state mismatch shows a generic error and is rejected before any token exchange; a rejected code exchange shows the failure; no user_session row | tests/acceptance/support/fake-oidc.ts + SecurityAcceptanceServer -Dqqq.security.auth=OAUTH2 | pass (chromium) | [#670](https://github.com/QRun-IO/qqq/issues/670) |
|
||||
| SEC-027 | OAUTH2 logout: backend session deleted, provider end-session called, next visit requires provider sign-in | user_session row deleted and provider session ended | protected page after logout returns to the provider login | tests/acceptance/support/fake-oidc.ts + SecurityAcceptanceServer -Dqqq.security.auth=OAUTH2 | pass (chromium) | [#670](https://github.com/QRun-IO/qqq/issues/670) [#674](https://github.com/QRun-IO/qqq/issues/674) |
|
||||
| SEC-028 | OAUTH2 session revoked server-side mid-use: 401 sends the user through the provider and back to the page | returns to the same page after provider sign-in | revoked session gets 401, not stale data | tests/acceptance/support/fake-oidc.ts + SecurityAcceptanceServer -Dqqq.security.auth=OAUTH2 | pass (chromium) | [#670](https://github.com/QRun-IO/qqq/issues/670) |
|
||||
| SEC-029 | AUTH_0 authorization code + PKCE with an owned Auth0-compatible provider (browser token exchange, backend JWT validation via JWKS) | sign-in and data access through the Auth0 module; logout returns through the provider | provider denial is reported | tests/acceptance/support/fake-oidc.ts + SecurityAcceptanceServer -Dqqq.security.auth=AUTH_0 | pass (chromium) | [#670](https://github.com/QRun-IO/qqq/issues/670) |
|
||||
| SEC-030 | AUTH_0 against a real Auth0 tenant | real tenant sign-in/logout | | Requires an Auth0 tenant QRun does not own in CI | excluded (Approved by James 2026-09-25 (QRun-IO/qqq#649): real Auth0 tenant; the AUTH_0 flow is covered against an owned Auth0-compatible provider by SEC-029/SEC-031; real-service testing is follow-up work.) | |
|
||||
| SEC-031 | FULLY_ANONYMOUS authentication: session without an identity provider, anonymous identity, logout and sign-in again | deep link loads data as Anonymous | after logout the page stays signed out until Sign in | SecurityAcceptanceServer -Dqqq.security.auth=FULLY_ANONYMOUS | pass (chromium) | [#669](https://github.com/QRun-IO/qqq/issues/669) |
|
||||
| SEC-032 | Unsupported authentication type (a type this UI does not know, as a newer backend could declare) is reported, never silently treated as another type | | login shows "Unsupported authentication type: ACCEPTANCE_UNKNOWN" and no data or session is requested | SecurityAcceptanceServer -Dqqq.security.auth=UNSUPPORTED (v1 authentication metadata names type ACCEPTANCE_UNKNOWN; TABLE_BASED is supported since QRun-IO/qqq#700, see SEC-034..SEC-037) | pass (chromium) | [#669](https://github.com/QRun-IO/qqq/issues/669) [#700](https://github.com/QRun-IO/qqq/issues/700) |
|
||||
| SEC-033 | Server-side logout invalidation: v1 logout expires every session cookie it issued and an ended Auth0 session cannot be replayed | logout response expires sessionUUID and sessionId | a logged-out Auth0 sessionUUID replayed against the API gets 401 | tests/acceptance/support/fake-oidc.ts + SecurityAcceptanceServer -Dqqq.security.auth=OAUTH2 | pass (chromium) | [#674](https://github.com/QRun-IO/qqq/issues/674) [#675](https://github.com/QRun-IO/qqq/issues/675) |
|
||||
| SEC-034 | TABLE_BASED sign-in: a deep link asks for username and password under the app branding, POST /qqq/v1/manageSession with Authorization: Basic creates a stored session, the session user is shown and a reload resumes it | login form shows the logo and app name before any session exists and reads no data; credentials travel once as Basic credentials (password with colons), never in the body or browser storage; lands on the requested page with rows; user menu shows Tess Table (table-based) / tess.table; one session row for the user, matching the sessionUUID cookie; reload keeps it | without the session cookie the backend refuses a read (401) | SecurityAcceptanceServer -Dqqq.security.auth=TABLE_BASED + SecurityFixtures.defineTableBased/primeTableBased (users tess.table, ravi.rows) | pass (chromium) | [#700](https://github.com/QRun-IO/qqq/issues/700) |
|
||||
| SEC-035 | TABLE_BASED sign-in failure: a wrong password or unknown user is refused with one message, stores no session, and the form recovers | correct credentials after a refusal sign in to the requested page | empty fields are named and nothing is sent; wrong password and unknown user both show "Incorrect username or password."; no session row is stored and no data is read; the password field is cleared | SecurityAcceptanceServer -Dqqq.security.auth=TABLE_BASED + SecurityFixtures.defineTableBased/primeTableBased (users tess.table, ravi.rows) | pass (chromium) | [#700](https://github.com/QRun-IO/qqq/issues/700) |
|
||||
| SEC-036 | TABLE_BASED session expiry: a session idle past the inactivity timeout gets a 401, the user signs in again and returns to the record they opened | expired mid-use returns to the clicked record after signing in again | the expired session is refused by the backend ("Session is expired.") | SecurityAcceptanceServer -Dqqq.security.auth=TABLE_BASED + SecurityFixtures.defineTableBased/primeTableBased (users tess.table, ravi.rows) + /acceptance/expire-table-sessions | pass (chromium) | [#700](https://github.com/QRun-IO/qqq/issues/700) |
|
||||
| SEC-037 | TABLE_BASED logout: POST /qqq/v1/logout deletes the stored session and clears both cookies, protected pages stay on the form, and the next user signs in as themselves | a different user signs in on the same browser and is shown as themselves | the ended session cannot be replayed with either cookie (401); after logout a protected page (full load) stays on the form and reads no data | SecurityAcceptanceServer -Dqqq.security.auth=TABLE_BASED + SecurityFixtures.defineTableBased/primeTableBased (users tess.table, ravi.rows) | pass (chromium) | [#700](https://github.com/QRun-IO/qqq/issues/700) |
|
||||
| SEC-038 | Dashboard documents carry a strict Content-Security-Policy (script-src self plus the hash of each inline script, object-src none, base-uri/form-action self, frame-ancestors none), X-Frame-Options DENY, Referrer-Policy and Permissions-Policy | login, token, app, list, record, edit and 404 documents send the policy and headers; the script-src hashes equal the hashes of the inline scripts in each served document; pages hydrate and navigate under the policy with no violation | another web site framing the dashboard gets no content; an injected inline script and inline event handler do not run and are reported as violations | tests/acceptance/fixture/AcceptanceSampleServer.java (javalin mode); with QQQ_ACCEPTANCE_MODE=standalone the container image server (standalone/qqq-server.mjs) | pass (chromium) | [#695](https://github.com/QRun-IO/qqq/issues/695) [#734](https://github.com/QRun-IO/qqq/issues/734) |
|
||||
| SEC-039 | The policy allows the configured identity provider origin (OAUTH2 discovery, AUTH_0 browser token exchange) and no other connection target | OAUTH2: connect-src is self plus the provider origin; sign-in completes with no violation; AUTH_0: connect-src is self plus the Auth0 domain; the browser token exchange completes with no violation | a fetch to any other origin is refused and reported (connect-src) | tests/acceptance/support/fake-oidc.ts + SecurityAcceptanceServer -Dqqq.security.auth=OAUTH2\|AUTH_0 | pass (chromium) | [#695](https://github.com/QRun-IO/qqq/issues/695) |
|
||||
| SEC-040 | Application override hook and metadata-derived origins: the application adds its own origins (QApplicationJavalinServer.withNextDashboardSecurityHeadersCustomizer); QuickSight and custom component bundle origins come from widget metadata | the QuickSight embed, block image and audio from the loopback service and the custom component bundle load with no violation; the policy lists exactly the configured origins | a frame from an origin the application did not configure is refused and reported (frame-src) | tests/acceptance/fixture/WidgetsFixtures.java loopback service | pass (chromium) | [#695](https://github.com/QRun-IO/qqq/issues/695) [#734](https://github.com/QRun-IO/qqq/issues/734) |
|
||||
| SEC-041 | The login page explains known sign-in error codes and never displays text taken from the link | direct links with access_denied and other known codes show their safe message; other well-formed codes show the code; Try again signs in through the provider | free text in ?error= is replaced by a generic message | tests/acceptance/support/fake-oidc.ts + SecurityAcceptanceServer -Dqqq.security.auth=OAUTH2 | pass (chromium) | [#696](https://github.com/QRun-IO/qqq/issues/696) |
|
||||
| SEC-042 | Rendered HTML (field values, widgets, process steps, help, banners) cannot add forms, form controls, style sheets or page-covering positioning | formatting in a stored HTML value is kept; the stored value is unchanged in the database | a stored form with a password input, a style element and a position:fixed overlay render as plain content | tests/acceptance/fixture/RecordsFixtures.java fieldLab.htmlValue | pass (chromium) | [#696](https://github.com/QRun-IO/qqq/issues/696) |
|
||||
| SEC-043 | Per-user browser data (recently viewed records, stored identity) is bound to the signed-in identity | the same user keeps their recently viewed records across reloads | a different user signing in on the same browser without a logout does not see the previous user's recent records | tests/acceptance/fixture/AcceptanceSampleServer.java personas and sample users | pass (chromium) | [#696](https://github.com/QRun-IO/qqq/issues/696) |
|
||||
| SEC-044 | Session cookies (sessionUUID, sessionId) are SameSite=Lax for the whole site, and Secure when the request arrived over HTTPS | manageSession and API responses set the cookies with Path=/ and SameSite=Lax; the browser stores them as Lax | over plain HTTP the cookies are not Secure (sign-in keeps working); behind X-Forwarded-Proto https they are | tests/acceptance/fixture/AcceptanceSampleServer.java | pass (chromium) | [#696](https://github.com/QRun-IO/qqq/issues/696) |
|
||||
| SEC-048 | Session cookies (sessionUUID, sessionId) are HttpOnly: script in the page cannot read them, and resuming a session from the cookie never hands its token to script | after sign-in (MOCK, TABLE_BASED, OAUTH2) the browser stores both cookies HttpOnly, naming the current session; a reload resumes the session through POST /qqq/v1/manageSession with an empty body; the response carries the session values and no uuid | document.cookie never shows sessionUUID or sessionId; an in-page POST /qqq/v1/manageSession {} (what injected script could send) answers without the session token | tests/acceptance/fixture/AcceptanceSampleServer.java (MOCK); SecurityAcceptanceServer -Dqqq.security.auth=TABLE_BASED and OAUTH2 with tests/acceptance/support/fake-oidc.ts | pass (chromium) | [#733](https://github.com/QRun-IO/qqq/issues/733) |
|
||||
| SEC-049 | With HttpOnly session cookies the backend ends and replaces sessions: logout expires both cookies, an idle-expired or revoked session sends the user to sign in again, and a new password or OAuth2 sign-in points both cookies at the new session | TABLE_BASED: a first visit shows the form without a session request; an idle-expired session is refused, the form returns without a resume attempt, and signing in again returns to the record; OAUTH2: a session revoked on the server sends the user through the provider and back, with new HttpOnly cookies; logout: the server deletes the session and expires both cookies; a protected page stays signed out without a session request | a logout that never reached the server leaves the earlier user's cookies; the next user's sign-in replaces both, and a reload keeps the new user | SecurityAcceptanceServer -Dqqq.security.auth=TABLE_BASED and OAUTH2 with tests/acceptance/support/fake-oidc.ts | pass (chromium) | [#733](https://github.com/QRun-IO/qqq/issues/733) |
|
||||
| INT-001 | Keyboard: navigate from the sidebar to a list, open a record, edit and save without a pointer | edit saved by keyboard persists (SQL) | | qqq-sample-project (stock sample, mock auth, sharing demo) | pass (chromium) | [#676](https://github.com/QRun-IO/qqq/issues/676) [#682](https://github.com/QRun-IO/qqq/issues/682) |
|
||||
| INT-002 | Keyboard: run a process through its steps without a pointer | greetInteractive completes and shows results | | qqq-sample-project (stock sample, mock auth, sharing demo) | pass (chromium) | |
|
||||
| INT-003 | Dialogs trap focus, close on Escape and restore focus to the trigger; confirm by keyboard | keyboard delete confirmation deletes the row (SQL) | Escape cancels without deleting; focus returns to Delete | qqq-sample-project (stock sample, mock auth, sharing demo) | pass (chromium) | [#682](https://github.com/QRun-IO/qqq/issues/682) [#683](https://github.com/QRun-IO/qqq/issues/683) [#694](https://github.com/QRun-IO/qqq/issues/694) |
|
||||
| INT-004 | Command menu, search and shortcut dialogs: keyboard open, focus inside, Escape restores focus | command menu navigates to a table | Escape closes and returns focus | qqq-sample-project (stock sample, mock auth, sharing demo) | pass (chromium) | [#682](https://github.com/QRun-IO/qqq/issues/682) |
|
||||
| INT-005 | Automated accessibility scan (axe WCAG 2.1 AA) of login, dashboard, list, record, edit form and process step | no serious or critical violations on each page | | qqq-sample-project (stock sample, mock auth, sharing demo) | pass (chromium) | [#682](https://github.com/QRun-IO/qqq/issues/682) |
|
||||
| INT-006 | Form labels and ARIA: every field has an accessible label, required fields are marked, invalid fields announce their error | labels match metadata labels | submitting empty required fields sets aria-invalid and a described error | qqq-sample-project (stock sample, mock auth, sharing demo) | pass (chromium) | |
|
||||
| INT-007 | Validation and error messages: client and backend validation name the field and nothing is saved | | missing required value is named and focused; backend rejection is shown verbatim; SQL unchanged | qqq-sample-project (stock sample, mock auth, sharing demo) | pass (chromium) | |
|
||||
| INT-008 | Responsive layout at phone width (Pixel 7): navigation drawer, list, record and form usable without horizontal scrolling | drawer navigation to a list, open and edit a record on a phone viewport | no horizontal page overflow | qqq-sample-project (stock sample, mock auth, sharing demo) | pass (chromium) | [#676](https://github.com/QRun-IO/qqq/issues/676) |
|
||||
| INT-009 | Loading states: a delayed list or record request shows a busy state, then the real data | busy indicator while the real request is held, then rows | | qqq-sample-project (stock sample, mock auth, sharing demo) | pass (chromium) | [#694](https://github.com/QRun-IO/qqq/issues/694) |
|
||||
| INT-010 | Failed request and recovery: a failed list request shows an error with retry; retry loads the data | retry succeeds | the failure is explained, not a blank page | qqq-sample-project (stock sample, mock auth, sharing demo) | pass (chromium) | |
|
||||
| INT-011 | Delayed save: the submit control is disabled while saving and double submission creates one record | exactly one row inserted (SQL) | second click while pending is ignored | qqq-sample-project (stock sample, mock auth, sharing demo) | pass (chromium) | [#673](https://github.com/QRun-IO/qqq/issues/673) |
|
||||
| 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) |
|
||||
|
||||
## Dashboards, widgets, reports, saved and scheduled reports, and sharing
|
||||
|
||||
| ID | Feature | Scenarios | Negative | Fixture | Result | Issues |
|
||||
|---|---|---|---|---|---|---|
|
||||
| WID-001 | alert widget: severity from alertType, sanitized HTML and bullet list | WARNING alert renders strong HTML and each bullet as sanitized HTML | hideWidget and empty alerts render nothing (WID-063) | tests/acceptance/fixture/WidgetsFixtures.java widgetGallery/accAlert | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
|
||||
| WID-002 | barChart widget: one bar per label in backend colors | bars carry per-point backgroundColors; accessible data table has exact values | | sample SampleWidgetsDashboard/SampleBarChartWidget | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
|
||||
| WID-003 | chart output from an untyped widget renders as a bar chart | metadata type absent, payload type chart, values rendered | zero-value month has no drawn bar | sample greetingsApp/PersonsByCreateDateBarChart | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
|
||||
| WID-004 | divider widget renders a rule without card chrome | hr rendered, no label or card | | tests/acceptance/fixture/WidgetsFixtures.java widgetGallery/accDivider | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
|
||||
| WID-005 | fieldValueList widget: labels, display values, prefix icons, indentation | display value wins, zero shown, icon color, indent | empty field list shows empty state (WID-052) | tests/acceptance/fixture/WidgetsFixtures.java widgetGallery/accFieldValueList | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
|
||||
| WID-006 | generic widget renders its common payload fields (sublabel, footer) | sublabel and sanitized footer HTML shown | | tests/acceptance/fixture/WidgetsFixtures.java widgetGallery/accGeneric | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
|
||||
| WID-007 | horizontalBarChart widget: horizontal bars with negative values left of zero | negative bar extends left of the zero line; height from payload | | tests/acceptance/fixture/WidgetsFixtures.java widgetGallery/accHorizontalBarChart | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
|
||||
| WID-008 | html widget (no-code) renders sanitized user HTML | velocity output with i/b/u and centered wrapper | empty html renders an empty body (WID-052) | sample SampleWidgetsDashboard/SampleHTMLWidget | pass (chromium) | |
|
||||
| WID-009 | lineChart widget: line with a point per label and description | points carry exact values | | sample SampleWidgetsDashboard/SampleLineChartWidget | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
|
||||
| WID-010 | smallLineChart widget: compact line with title and HTML description | title, strong description text, 5 points | | sample SampleWidgetsDashboard/SampleSmallLineChartWidget | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
|
||||
| WID-011 | location widget: image, title, description, address and footer | owned image loads from the owned service | | tests/acceptance/fixture/WidgetsFixtures.java widgetGallery/accLocation | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
|
||||
| WID-012 | multiStatistics widget: groups with icon, header, subheader and statistics | every group and value matches the payload | empty group list shows empty state (WID-052) | sample SampleWidgetsDashboard/SampleMultiStatisticsWidget | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
|
||||
| WID-013 | multiTable widget renders each table with its label and rows | two labelled tables with their rows | empty table list shows empty state (WID-052) | tests/acceptance/fixture/WidgetsFixtures.java widgetGallery/accMultiTable | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
|
||||
| WID-014 | pieChart widget: colored slices, legend values and subheader | slice fills, legend label: value, main number, up/good change | | sample SampleWidgetsDashboard/SamplePieChartWidget | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
|
||||
| WID-015 | quickSightChart widget embeds the provider URL (real renderer, owned local provider endpoint) | iframe src is the owned embed URL and its content renders | metadata never exposes the secret key | tests/acceptance/fixture/WidgetsFixtures.java repointSampleQuickSight (greetingsApp/QuickSightChartRenderer against the owned loopback endpoint) | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
|
||||
| WID-016 | statistics widget: count, context, colored percentage change and links | decrease with increaseIsGood is bad; increase with increaseIsGood=false is bad; count links to countURL | | sample SampleStatisticsWidget; tests/acceptance/fixture/WidgetsFixtures.java widgetGallery/accStatisticsGood | pass (chromium) | |
|
||||
| WID-017 | stackedBarChart widget stacks datasets with colors and legend | bars of one label share x; dataset colors | | sample SampleStackedBarChartWidget; tests/acceptance/fixture/WidgetsFixtures.java widgetGallery/accStackedBars | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
|
||||
| WID-018 | stepper widget: completed, current (with link) and upcoming steps | states and current link | empty steps show empty state (WID-052) | sample SampleWidgetsDashboard/SampleStepperWidget | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
|
||||
| WID-019 | table widget: headers, rows, alignment and fixed totals row | rows and sticky last row match payload | no rows shows noRowsFoundHTML or default (WID-052) | sample SampleWidgetsDashboard/SampleTableWidget | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
|
||||
| WID-020 | usaMap widget plots each marker with name and coordinates | 3 markers placed west to east by longitude; marker list | empty marker list shows empty state (WID-052) | tests/acceptance/fixture/WidgetsFixtures.java widgetGallery/accUsaMap | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
|
||||
| WID-021 | process widget runs the sample interactive process inline | form fields render, submit, result greeting shows | viewer persona: process denied contained (WID-061) | tests/acceptance/fixture/WidgetsFixtures.java widgetProcess/accProcess (greetInteractive) | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
|
||||
| WID-022 | parentWidget renders its children in a 12-column grid | children render with their own data | tabs (WID-055), denied/missing children (WID-056) | tests/acceptance/fixture/WidgetsFixtures.java widgetParents/accParentGrid | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) [#666](https://github.com/QRun-IO/qqq/issues/666) |
|
||||
| WID-023 | composite widget renders QQQ blocks (big numbers, up/down, links, tooltips) | every block value, direction, link and tooltip | no blocks renders an empty composite (WID-052) | sample SampleWidgetsDashboard/SampleBigNumberBlocksWidget | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
|
||||
| WID-024 | childRecordList widget without association binding lists joined child records | rows link to child records, Showing 2 of 3, View All filter link | host with no children shows empty state | tests/acceptance/fixture/WidgetsFixtures.java accWidgetHost section accWidgetHostJoinChild (maxRows 2) | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) [#663](https://github.com/QRun-IO/qqq/issues/663) |
|
||||
| WID-025 | customComponent widget loads its bundle and renders the named component | component receives widget metadata and data | missing bundle shows contained error (WID-062) | tests/acceptance/fixture/WidgetsFixtures.java widgetGallery/accCustomComponent (owned bundle) | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
|
||||
| WID-026 | cronUI widget shows the record's schedule expression, description and time zone | expression, backend description and time zone | record without expression shows No schedule set | tests/acceptance/fixture/WidgetsFixtures.java accWidgetHost section accHostCron | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
|
||||
| WID-027 | dynamicForm widget shows labeled values from the payload | labels and values incl. 0 | | tests/acceptance/fixture/WidgetsFixtures.java accWidgetHost section accHostDynamicForm | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
|
||||
| WID-028 | dataBagViewer widget lists versions and shows the selected version contents | newest version current and shown; selecting older version shows its JSON | bag without versions shows empty message | tests/acceptance/fixture/WidgetsFixtures.java dataBag/1 section accDataBagViewer (owned dataBag tables) | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
|
||||
| WID-029 | pivotTableSetup widget shows, edits and clears the saved report's pivot definition | rows/values labels from the source table; modal OK updates only the host draft; host Save persists rows, columns and aggregate values with SQL readback and reopen; keyboard row reorder and value move preserve saved configuration; turning the pivot off clears the stored definition on host Save | report without pivot shows message; missing row fields and aggregate functions keep the editor open; Cancel discards added fields without changing the database; used group/value fields are excluded from choices; changing a numeric value to text clears an incompatible aggregate | prime-sharing savedReport 1; tests/acceptance/fixture/WidgetsFixtures.java owned pivot saved report | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
|
||||
| WID-030 | filterAndColumnsSetup widget shows the saved report's filter, sort and columns | criteria with field labels and operator phrases, columns by label | report without filter shows No filters | prime-sharing savedReport 1; tests/acceptance/fixture/WidgetsFixtures.java owned filtered saved report | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
|
||||
| WID-070 | filterAndColumnsSetup edits a saved report's filter, sort, columns and variable values through the host form | Edit Filters and Columns opens the report setup dialog; OK writes queryFilterJson and columnsJson to the host form, and Save persists a changed sort to the backend; assigning a filter variable persists FilterVariableExpression in the backend JSON and shows ${VARIABLE} in the editor; import a saved view, reset changes to that view or reset to a new default draft without updating the saved view | Cancel discards the draft; OK without host Save leaves the backend row unchanged; preview and Open in new window block a query while a filter variable lacks a value; resetting and cancelling leave both the report and all saved views unchanged | prime-sharing savedReport 1 (Pet Species Report, no pivot table); saved-reports-sharing.spec.ts | pass (chromium) | [#722](https://github.com/QRun-IO/qqq/issues/722) |
|
||||
| WID-071 | a WIDGET-adorned process form field renders filter setup and writes its own value | the widget request names the adorned field, the editor updates its filter, and the next process screen receives the JSON | other widget output fields are not written to the adorned field's host form | ProcessesFixtures.java prcFieldWidget with a queryFilterJson WIDGET adornment | pass (chromium) | [#722](https://github.com/QRun-IO/qqq/issues/722) |
|
||||
| WID-072 | API-versioned report setup loads table metadata and previews through the application API | the report editor loads person metadata and its preview rows and count through the real API-aware v1 routes | an unsupported API version returns 404 | ApiVersionFixtures.java acceptanceApi 2026.Q3; savedReport 102; saved-reports-sharing.spec.ts | pass (chromium) | [#722](https://github.com/QRun-IO/qqq/issues/722) |
|
||||
| WID-073 | report setup editor uses the full server-backed grid for paging, sorting and column sizing | grid preview hides selection, changing page size changes the real backend query, header sorting changes the real backend query, and keyboard column resize persists in saved columns JSON; create a report from a query, switch to Audit, save and reopen, verify stored columns and download the CSV; filtered column statistics totals and exported value counts match SQL; closing statistics and cancelling the editor leave saved data unchanged; a table without COUNT still shows real preview rows and page copy matches SQL; variant-backed report previews choose, switch and remember the backend variant for query/count, copy and statistics on ordinary and application-API routes; complete nested criteria mark preview column headers; opening the indicator edits the existing condition and real preview rows match SQL; configured omitted joins disappear from new preview column/filter choices while saved joined values, editable criteria and column positions survive; changed results and reload match SQL; editor Cancel, Basic/Advanced, OK, host Save and reopen preserve joined criteria, sorts, order and width; hidePreview hides only the read-only summary; the editor retains real query rows, sorting and Refresh; Cancel, OK, host Save and reopen preserve the intended sort with the flag enabled and disabled; column menus fit 320px and 720px windows; keyboard End/Escape restores focus and pointer Copy/full-query and sorting remain reachable | selection pseudo-column and incompatible previous-table fields are excluded from saved reports; full-column copy refuses results over the configured limit; an empty preview offers no column copy action; cancellation preserves the saved report; no query before selecting a required variant; an old variant copy response cannot overwrite a later copy; variant changes do not persist report edits; incomplete criteria do not mark columns; opening an indicator adds no condition; read-only preview edits and editor Cancel leave stored report JSON unchanged | savedReport 1 targeting petSpecies without a pivot; saved-reports-sharing.spec.ts | pass (chromium) | [#722](https://github.com/QRun-IO/qqq/issues/722) |
|
||||
| WID-031 | rowBuilder widget without association binding shows its rows read-only | column labels from widget fields, row values incl. 0 | | tests/acceptance/fixture/WidgetsFixtures.java accWidgetHost section accHostRows | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) [#663](https://github.com/QRun-IO/qqq/issues/663) |
|
||||
| WID-032 | scriptViewer widget lists script revisions and shows the selected revision's files | current revision marked and its file contents shown; selecting older revision | | tests/acceptance/fixture/WidgetsFixtures.java script/1 (owned memory script with 2 revisions) | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
|
||||
| WID-033 | quickSightChart against a real AWS QuickSight account and dashboard | real embed URL | | requires AWS credentials and a QuickSight dashboard (external service) | excluded (Approved by James 2026-09-25 (QRun-IO/qqq#649): real AWS QuickSight dashboard; the widget and renderer are covered against an owned loopback provider by WID-015; real-service testing is follow-up work.) | |
|
||||
| WID-040 | dashboard renders app widgets in declared order sized by gridColumns (twelfths) | order matches metadata; 12 spans the grid; 4 is a third | | sample SampleWidgetsDashboard | pass (chromium) | [#666](https://github.com/QRun-IO/qqq/issues/666) |
|
||||
| WID-041 | widget chrome: card vs plain (isCard=false), sublabel, metadata and payload footer HTML | plain widget has no card border; footers render | | tests/acceptance/fixture/WidgetsFixtures.java widgetControls/accPlain, widgetGallery/accGeneric | pass (chromium) | [#666](https://github.com/QRun-IO/qqq/issues/666) |
|
||||
| WID-042 | header icons (topLeftInsideCard/topRightInsideCard) with metadata colors | icon names, colors and placement | | sample SampleWidgetsDashboard | pass (chromium) | [#538](https://github.com/QRun-IO/qqq/issues/538) [#666](https://github.com/QRun-IO/qqq/issues/666) |
|
||||
| WID-043 | widget label tooltip from metadata on hover and keyboard focus | hover and focus show, Escape hides | | sample SampleWidgetsDashboard/SampleBarChartWidget | pass (chromium) | [#666](https://github.com/QRun-IO/qqq/issues/666) |
|
||||
| WID-044 | widget help content (HTML) on the label | help icon shows sanitized HTML help | | tests/acceptance/fixture/WidgetsFixtures.java widgetControls/accHelp | pass (chromium) | [#666](https://github.com/QRun-IO/qqq/issues/666) |
|
||||
| WID-045 | reload button re-renders the widget from the backend | render count increments per reload | no reload button without showReloadButton | tests/acceptance/fixture/WidgetsFixtures.java widgetControls/accReload | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) [#666](https://github.com/QRun-IO/qqq/issues/666) |
|
||||
| WID-046 | export button downloads the widget's csvData as CSV | file bytes match Material quoting rules | no csvData: no-data message and no download | tests/acceptance/fixture/WidgetsFixtures.java widgetControls/accExport, accExportEmpty, accControls | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) [#666](https://github.com/QRun-IO/qqq/issues/666) |
|
||||
| WID-047 | PVS dropdown from the widget payload: selection is sent as a parameter and re-renders | options from payload, choice reaches renderer, clearing removes it | non-stored selection is not persisted | tests/acceptance/fixture/WidgetsFixtures.java widgetControls/accDropdownHtml | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) [#666](https://github.com/QRun-IO/qqq/issues/666) |
|
||||
| WID-048 | DATE_PICKER dropdown sends the chosen date | date reaches the child renderer | | tests/acceptance/fixture/WidgetsFixtures.java widgetControls/accControls | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) [#666](https://github.com/QRun-IO/qqq/issues/666) |
|
||||
| WID-049 | required dropdowns: the parent waits with the backend message until selections are made | message then children render with selections | | tests/acceptance/fixture/WidgetsFixtures.java widgetControls/accControls | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) [#666](https://github.com/QRun-IO/qqq/issues/666) |
|
||||
| WID-050 | storeDropdownSelections persists selections across reload; stale stored options are dropped | localStorage qqq.widgets.dropdownData.<widget>.<param>; reload keeps selections | removed option in storage is not selected | tests/acceptance/fixture/WidgetsFixtures.java widgetControls/accControls | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) [#666](https://github.com/QRun-IO/qqq/issues/666) |
|
||||
| WID-051 | failing widget renderer shows a contained error with the backend message and retry | error message, retry re-requests, neighbor content | no error toast, no console error | tests/acceptance/fixture/WidgetsFixtures.java widgetStates/accError + accHealthy | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) [#666](https://github.com/QRun-IO/qqq/issues/666) |
|
||||
| WID-052 | empty payloads show descriptive empty states for every display type | table, multiStatistics, charts, stepper, statistics 0, composite, multiTable, field values, map, html | | tests/acceptance/fixture/WidgetsFixtures.java widgetStates/accEmpty* | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
|
||||
| WID-053 | malformed payloads are contained to their own widget | chart, table, composite, stepper, multiStatistics show a format notice | no crash, no console error, neighbors render | tests/acceptance/fixture/WidgetsFixtures.java widgetStates/accMalformed* | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
|
||||
| WID-054 | widget permissions: a denied widget is absent from metadata, not rendered and not requested; the API denies it | admin sees it | noPets: no card, no request, GET /widget 403, renderer not called | tests/acceptance/fixture/WidgetsFixtures.java widgetPermissions/accDenied (pet.accDenied.hasAccess) with the noPets persona | pass (chromium) | |
|
||||
| WID-055 | parent widget TABS layout switches tabs and remembers the selected tab | tab click, arrow keys, stored tab restored after reload | | tests/acceptance/fixture/WidgetsFixtures.java widgetParents/accParentTabs | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) [#666](https://github.com/QRun-IO/qqq/issues/666) |
|
||||
| WID-056 | parent widget skips denied and unknown children without requests | allowed children render | noPets: denied child absent and not requested; unknown child skipped | tests/acceptance/fixture/WidgetsFixtures.java widgetParents/accParentDenied, accParentMissing | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
|
||||
| WID-057 | composite renders every block type with its values and styles | TEXT, BIG_NUMBER, UP_OR_DOWN_NUMBER, NUMBER_ICON_BADGE, ICON, TABLE_SUB_ROW_DETAIL_ROW, PROGRESS_BAR, DIVIDER, IMAGE, AUDIO, INPUT_FIELD, BUTTON; Unmapped legacy icon names render local glyphs in light/dark desktop/mobile modes; metadata navigation and main tiles preserve color, sizing, existing Lucide mappings and same-origin font delivery | | tests/acceptance/fixture/WidgetsFixtures.java widgetBlocks/accBlocks and accLegacyIcons | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
|
||||
| WID-058 | composite layouts and slot links/tooltips | FLEX_COLUMN, FLEX_ROW, BADGES_WRAPPER, TABLE_SUB_ROW_DETAILS, FLEX_ROW_SPACE_BETWEEN, FLEX_ROW_CENTER, FLEX_ROW_WRAPPED; link to /app/person with tooltip | | tests/acceptance/fixture/WidgetsFixtures.java widgetBlocks/accBlocks | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
|
||||
| WID-059 | unsupported block type shows a contained warning | Unsupported block type: OWNED_UNKNOWN; neighbor healthy | | tests/acceptance/fixture/WidgetsFixtures.java widgetBlocks/accBlocksUnknown | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
|
||||
| WID-060 | record-view section widgets receive the record context (id, tableName) | record 1 and 2 show their own values | renderer enforces the user's table read | tests/acceptance/fixture/WidgetsFixtures.java accWidgetHost sections accHostHtml, accHostFieldValues | pass (chromium) | |
|
||||
| WID-061 | process widget for a persona without process permission is contained | error shown in the widget; neighbor healthy | process metadata 403, no process run | tests/acceptance/fixture/WidgetsFixtures.java widgetProcess/accProcess with the noProcesses persona | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
|
||||
| WID-062 | custom component whose bundle cannot load shows a contained error | Error loading MissingOwnedComponent | | tests/acceptance/fixture/WidgetsFixtures.java widgetGallery/accCustomComponentMissing | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
|
||||
| WID-063 | alerts with hideWidget or without HTML render nothing | no card rendered | | tests/acceptance/fixture/WidgetsFixtures.java widgetGallery/accAlertHidden, widgetStates/accEmptyAlert | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) [#666](https://github.com/QRun-IO/qqq/issues/666) |
|
||||
| RPT-001 | TABLE report runs through its process and downloads CSV matching the database | CSV header labels and every person row from SQL | | tests/acceptance/fixture/WidgetsFixtures.java acceptanceReports/accPersonReport | pass (chromium) | [#664](https://github.com/QRun-IO/qqq/issues/664) |
|
||||
| RPT-002 | TABLE report downloads XLSX with the same rows | sheet rows match SQL | | tests/acceptance/fixture/WidgetsFixtures.java acceptanceReports/accPersonReport | pass (chromium) | [#664](https://github.com/QRun-IO/qqq/issues/664) |
|
||||
| RPT-003 | TABLE report downloads JSON with the same rows | JSON objects match SQL | | tests/acceptance/fixture/WidgetsFixtures.java acceptanceReports/accPersonReport | pass (chromium) | [#664](https://github.com/QRun-IO/qqq/issues/664) |
|
||||
| RPT-004 | SUMMARY report groups rows with counts and a totals row | pets per owner match SQL group by; Totals row | | tests/acceptance/fixture/WidgetsFixtures.java acceptanceReports/accPetSummaryReport | pass (chromium) | [#664](https://github.com/QRun-IO/qqq/issues/664) |
|
||||
| RPT-005 | PIVOT report produces a native pivot table in XLSX | xlsx contains the pivot table and cache over the rows sheet | CSV omits the pivot view (no native pivots) | tests/acceptance/fixture/WidgetsFixtures.java acceptanceReports/accPetPivotReport | pass (chromium) | [#664](https://github.com/QRun-IO/qqq/issues/664) [#484](https://github.com/QRun-IO/qqq/issues/484) |
|
||||
| RPT-006 | report input fields: required validation and the value filters the rows | minimumId filters rows >= value; Standard report inputs retain compact widths and an inline Generate Report action without a process header | empty required input blocks submit | tests/acceptance/fixture/WidgetsFixtures.java acceptanceReports/accPersonInputReport | pass (chromium) | [#664](https://github.com/QRun-IO/qqq/issues/664) |
|
||||
| RPT-007 | report permissions: restricted report denied in UI and by the server | admin can run it | noPets: no-permission message, legacy route 403 | tests/acceptance/fixture/WidgetsFixtures.java acceptanceReports/accRestrictedReport with the noPets persona | pass (chromium) | [#664](https://github.com/QRun-IO/qqq/issues/664) |
|
||||
| RPT-008 | reports appear in app navigation and open their run page | sidebar REPORT nodes navigate to the report page | | tests/acceptance/fixture/WidgetsFixtures.java acceptanceReports app | pass (chromium) | [#664](https://github.com/QRun-IO/qqq/issues/664) [#651](https://github.com/QRun-IO/qqq/issues/651) |
|
||||
| RPT-009 | saved report view shows its filter/columns and pivot setup | filter criteria, columns, pivot rows/values labels | | prime-sharing savedReport 1; tests/acceptance/fixture/WidgetsFixtures.java owned saved reports | pass (chromium) | [#550](https://github.com/QRun-IO/qqq/issues/550) |
|
||||
| RPT-010 | saved report renders through renderSavedReport and downloads the saved columns and rows | format chosen, CSV contains the saved columns and every species | | prime-sharing savedReport 1 (Pet Species Report) | pass (chromium) | [#645](https://github.com/QRun-IO/qqq/issues/645) [#661](https://github.com/QRun-IO/qqq/issues/661) |
|
||||
| RPT-011 | streaming report route for reports without a process | CSV streamed from /reports/{name}?format=csv matches SQL | | tests/acceptance/fixture/WidgetsFixtures.java acceptanceReports/accStreamedReport | pass (chromium) | [#664](https://github.com/QRun-IO/qqq/issues/664) |
|
||||
| RPT-012 | scheduled report: create a schedule for a saved report and view its cron schedule | schedule persisted with cron description; record view shows the schedule widget; Add new on the saved report opens the Add Scheduled Report dialog over it with Saved Report preset and locked (Material #/createChild=); Create submits savedReportId=1, closes the dialog and reloads the child list | invalid cron expression rejected with message | scheduledReport (memory) for savedReport 1 | pass (chromium) | [#673](https://github.com/QRun-IO/qqq/issues/673) [#714](https://github.com/QRun-IO/qqq/issues/714) |
|
||||
| RPT-013 | sharing: the owner shares a saved report with a user (Read-Only) | dialog lists Bob; shared_saved_report row READ_ONLY | | prime-sharing savedReport 1 owned by alice | pass (chromium) | [#665](https://github.com/QRun-IO/qqq/issues/665) [#444](https://github.com/QRun-IO/qqq/issues/444) |
|
||||
| RPT-014 | sharing: change a share's scope to Read and Edit | row scope READ_WRITE | | prime-sharing savedReport 1 | pass (chromium) | [#665](https://github.com/QRun-IO/qqq/issues/665) |
|
||||
| RPT-015 | sharing: remove a share | row deleted, list empty | | prime-sharing savedReport 1 | pass (chromium) | [#665](https://github.com/QRun-IO/qqq/issues/665) |
|
||||
| RPT-016 | sharing record security: another user cannot see an unshared report until it is shared | bob sees it after sharing (UI list + API) | before sharing: not in bob's list, GET 404 | prime-sharing savedReport 1 (alice), user bob | pass (chromium) | [#665](https://github.com/QRun-IO/qqq/issues/665) |
|
||||
| RPT-017 | sharing: only the owner may share | Share disabled with reason for the non-owner | server rejects a non-owner share; no row | tests/acceptance/fixture/WidgetsFixtures.java savedReport owned by casey shared with alice | pass (chromium) | [#665](https://github.com/QRun-IO/qqq/issues/665) [#371](https://github.com/QRun-IO/qqq/issues/371) |
|
||||
| RPT-018 | sharing a saved view through the share dialog | shared_saved_view row for Casey | | prime-sharing savedView 1 owned by alice | pass (chromium) | [#665](https://github.com/QRun-IO/qqq/issues/665) |
|
||||
| WID-064 | cronUI schedule editor on record forms: Basic days/hours/minutes, Advanced expression, live description, validation, time zone | Basic mode builds weekdays, hours and minutes; the live description equals the cronDescription QQQ stores; Basic mode builds selected dates, Clear resets the schedule, and the Advanced hint names the part under the keyboard caret; Advanced mode edits the stored expression; the live description equals the backend widget description; expression and time zone persist (SQL); an expression Basic mode cannot show opens Advanced with Basic disabled and the reason | an out-of-range expression shows the error live and marks the input invalid; saving without a schedule shows the required error in the editor, focuses it, and saves nothing | scheduledReport (memory) for savedReport 1; tests/acceptance/fixture/WidgetsFixtures.java accWidgetHost section accHostCron with a time-zone enum possible-value source | pass (chromium) | [#702](https://github.com/QRun-IO/qqq/issues/702) |
|
||||
| WID-065 | record view mounts only the active layout, so each section widget mounts and loads once | desktop tabs: every section widget is in the DOM once and requested once; no accordion is mounted; phone accordion: only open sections mount; each widget is requested once, on opening | the desktop tab layout is not mounted at phone width | tests/acceptance/fixture/WidgetsFixtures.java accWidgetHost | pass (chromium) | [#706](https://github.com/QRun-IO/qqq/issues/706) |
|
||||
| WID-066 | Widget metadata and widget data come from the v1 API | v1 /metaData lists permitted widgets with hasPermission and the reports map; the dashboard renders its widgets from POST /qqq/v1/widget/{name} and makes no legacy metadata or widget request | a denied widget is absent from v1 metadata, never requested, and refused (403) by POST /qqq/v1/widget without rendering | widgetPermissions (accHealthy, accDenied), accPersonReport | pass (chromium) | [#699](https://github.com/QRun-IO/qqq/issues/699) [#406](https://github.com/QRun-IO/qqq/issues/406) |
|
||||
| WID-067 | a widget whose payload is a single leaf block (Material block widget) renders that block | The backend payload is the TEXT block itself (blockTypeName TEXT, no blocks list); The widget shows the block's text as a TEXT block | No empty COMPOSITE container is rendered in its place | WidgetsFixtures.java accLeafBlock (COMPOSITE widget whose renderer returns a TextBlockData) | pass (chromium) | [#714](https://github.com/QRun-IO/qqq/issues/714) |
|
||||
| WID-068 | table widget Material extras: typed cells (numbers with separators, html, htmlAndTooltip, image, composite), hidden helper columns, fr widths, sub-rows, sticky header, column-header help, paging (10 by default, numbered pages, jump-to-page, entries-per-page), export built from columns and rows, and a multi-table widget with an export button and footer per table | Default cells show numbers with thousands separators; html cells render sanitized markup; an htmlAndTooltip cell shows the row's tooltip HTML on hover, focus or tap; an image cell shows the row's image, label and total; a composite cell renders its blocks; Columns of type hidden (tooltip, image helpers) are not shown; fr widths share the width left by fixed widths (2fr is twice 1fr; a 180px column keeps 180px); A row with sub-rows expands and collapses them (nested rows are shaded; a sub-row expands its own sub-rows); The columnHeader=name help slot shows its HTML help on the Name header; Without rowsPerPage 10 of 40 rows show, with numbered page buttons; the entries-per-page select (5-25) changes the page size; more than six pages become a jump-to-page input; the header stays at the top while the fixed-height body scrolls; Export without csvData downloads the Material CSV of every column (hidden ones too), quoted, with link text and without icon glyph text; Each table of a multi-table widget has its own label, export button (csvData or columns and rows) and footer; the widget itself has no export button | Icon glyph text (open_in_new) never reaches the CSV; Collapsed sub-rows are not rendered; A sub-table's footer is not shown under another sub-table | WidgetTableChartFixtures.java widgetTableCharts/accTableCells, accTablePaging, accMultiTableExtras | pass (chromium) | [#728](https://github.com/QRun-IO/qqq/issues/728) |
|
||||
| WID-069 | chart widget Material extras: theme color names, legend toggles, single-series legends (horizontal bars, line badges), full pie with percent tooltip, stacked backgroundColor, stacked tooltip and right integer axis, rotating category labels, small line y ticks and grid, bar chart As of line | Pie slices named info and success fill with the Material theme colors; the pie is full (no inner radius); Hovering a slice shows its label, value and percent of the total; Clicking a legend entry hides that slice or series and clicking again shows it; Stacked datasets fill from their singular backgroundColor (a theme name or a CSS color); the tooltip names only the hovered dataset; the y axis sits on the right with whole-number ticks; long category labels turn; A single-series horizontal bar chart has a legend; a line chart names its series in badges above the chart; A small line chart shows y-axis values and a horizontal grid; A barChart widget ends with an As of line naming today's date | A hidden series draws no bars or points; No y-axis tick of the stacked chart is fractional | WidgetTableChartFixtures.java widgetTableCharts/accPieNamed, accStackedExtras, accLineBadges, accHorizontalLegend, accSmallLineTicks, accBarAsOf | pass (chromium) | [#728](https://github.com/QRun-IO/qqq/issues/728) |
|
||||
| RPT-019 | render report input step shows only its fields; a report without variables adds no no-fields message | Report Format, Email To and Email Subject are shown; the values widget renders nothing | no "No fields" text on the input step | savedReport 1 (Pet Species Report, no variables) | pass (chromium) | [#707](https://github.com/QRun-IO/qqq/issues/707) |
|
||||
| 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) |
|
||||
@@ -0,0 +1,554 @@
|
||||
# Next UI visual review
|
||||
|
||||
> **Current status lives in GitHub:** [Next UI 1.0 roadmap](https://github.com/QRun-IO/qqq/issues/713). This document records scoped evidence and historical checkpoints; older open/closed wording and summary counts do not override current issue status. See [tracking policy](../ISSUE-TRACKING.md). Final 1.0 remains held for owner testing and explicit approval.
|
||||
|
||||
Status: in progress, September 30, 2026. This review is a release requirement. Captured screenshots are not approvals or evidence that a surface has passed review.
|
||||
|
||||
The visual reference is the working Next UI before the September 26 parity integrations, commit `e42ad2b2bcdc76311e13002a22dd70e3ec437192`. Material Dashboard remains the reference for supported workflows and metadata contracts. The user's September 27–28 correction requires preserving the earlier Next design while completing those workflows.
|
||||
|
||||
## Latest checkpoint — September 30, phone process states
|
||||
|
||||
Compared original Next `e42ad2b2bcdc76311e13002a22dd70e3ec437192` with the packaged RC8 candidate `f0f0eae98e4b6c883475956a7c59e060d2e5f0a5`, using the same fresh fixture classes and backend. All 24 phone images were inspected: light/dark, original/current, bulk-load mapping at four scroll positions, process progress and expanded internal error. All requested scroll positions were reached, with no setup errors, page errors or document horizontal overflow. Evidence: `rc8-phone-process-review/` manifests and review.
|
||||
|
||||
Progress and expanded-error composition retain the original arrangement. Bulk-load field cards and footer retain their arrangement, but saved-profile/reset controls, metadata help and larger touch targets make the upper section taller. Grouped Add Fields and the red removal icon also differ from the original. These are recorded differences, not owner approval. Header breadcrumb crowding remains open. This checkpoint covers these phone states only; whole-product visual and interaction approval remain open.
|
||||
|
||||
## Earlier checkpoint — September 30, reachable column menus
|
||||
|
||||
VIS-031: column menus were not constrained to available viewport height. A browser probe on the report preview measured a 352px menu starting at y=-147 in a 320px window in both Chromium and WebKit. The shared menu now uses Radix's available-height limit, an 8px collision margin and vertical scrolling, matching existing query menus. Its labels, ordering, typography and normal-height presentation are preserved.
|
||||
|
||||
The corrected regression first failed at 320px and passed at 720px in both engines. All 28 final Chromium/WebKit checks pass, including bounded placement, End/Escape and focus return, pointer Copy, sorting, standalone column actions and report variant workflows. Four final menu screenshots were directly inspected. Seven grid component tests, production export, types/lint/license and bundle budgets pass. Evidence: `column-menu-{probe.json,corrected-red-report.json,green-report.json,green-artifacts,unit.log,build.log,budget.log}` under local visual-review evidence. The first regression incorrectly expected a statistics action absent from Person metadata; that test assumption was corrected before the valid red run. No product change addressed that test-only error.
|
||||
|
||||
Hosted WebKit run36739388053 failed WID-073 before copying: the trace repeatedly reports Copy full query values outside the viewport. Its report and trace are retained in `row-appearance-ci-webkit/`. The exact workflow passed locally before the change, so this reproduced short-window defect does not by itself prove that hosted failure resolved. A fresh hosted result is still required.
|
||||
|
||||
The prior combined table, record-spacing and report hidePreview source `a8e4d3b` separately passed **583 Chromium tests/all 391 required rows**, zero failures/skips/flaky results and three approved external-service exclusions. That full run predates this menu correction; evidence is `report-preview-full-chromium/`. RC4 remains the published version; final 1.0 and whole-product visual approval remain held.
|
||||
|
||||
## Earlier checkpoint — September 30, forms, processes and dark widgets
|
||||
|
||||
At post-RC4 source `9fb613d`, inspected all 88 images from 64 desktop entries for 16 form/process states in both themes. These include Create, validation, Edit, Copy, Field Lab, process components/help/validation/review, route details/validation/confirmation, cancellation and widget results. All entries have export fingerprints, no setup/page errors and no document overflow. Layouts retain the original Next composition. The broken image in Lab briefing is the deliberate script/onerror sanitization fixture, not a newly broken image feature. A focused axe scan of the default process stepper reports no color-contrast violations or incomplete checks in either theme; its colors were not changed. Evidence: `post-rc4-form-process-review/`. This does not cover all process interactions, bulk-load/progress/error variants or responsive states.
|
||||
|
||||
Also inspected 22 fresh dark desktop images (eight entries) covering the full scrolled Widget Gallery, Blocks, Parents, and Tables and Charts pages. Every requested scroll reached its target; no setup/page errors or document overflow occurred. The existing Next card composition remains. Changes include functional maps, typed table cells, chart colors/legends, supplied footer metadata and native dark audio controls. No new visual regression was found in this inventory. This closes the current dark desktop lower-page gap for these four pages only. Evidence: `post-rc4-widget-review/` (11 directly inspected paired sheets). Other themes, responsive layouts, interactive widget states and whole-product approval remain open.
|
||||
|
||||
## September 30 — report summary preview flag compatibility
|
||||
|
||||
Source comparison found that Material's `hidePreview` hides the read-only summary preview but leaves the editing query available (`FilterAndColumnsSetupWidget.tsx:483-526`). Next incorrectly applied the flag inside the editor too. The bounded correction retains the existing Next editor and scopes this flag to the summary. It is not in published RC4.
|
||||
|
||||
A real-backend regression failed for the hidden-summary case while the ordinary case passed. After correction, eight Chromium report scenarios pass with zero failures/skips/flaky results, covering hidden/shown summaries, editor sorting/Refresh, Cancel, OK, host Save/reopen, query permissions, filter indicators and saved omitted joins. All 1,884 units, production export, types, changed-file lint and license checks pass. Four fresh editor images were directly inspected (both themes, flag enabled/disabled); dialog and preview bounds are identical, with no page errors or document overflow. Evidence: `report-preview-{red,green}-report.json`, `report-preview-{unit,build}.log`, and `report-preview-fixed-review/`. Broader embedded-query equivalence is still open.
|
||||
|
||||
## Earlier checkpoint — September 30, record detail review
|
||||
|
||||
Captured and inspected six desktop record states in light and dark themes against the original Next: small record, Person, selected section with keyboard focus, the field/adornment lab (including its lower scroll position), empty Audit and Delete confirmation. The 24 manifest entries contain 28 images, with no setup/page errors or document overflow. Evidence: `post-rc4-record-review/` under the local visual-review directory. Card placement and dialog composition are retained; added audit sort controls, code expansion and widget chrome remain visible. These captures do not approve populated audits, every field interaction or responsive record states.
|
||||
|
||||
Found VIS-030: overview field values inherit 16px/24px typography after the metadata grid migration, increasing plain rows from the original 42px to 46px. Browser measurements on both exports establish the difference (`spacing-probe.json`). Restored the original overview line height while retaining metadata widths and the selected-section layout. Both new Chromium cases failed at 24px versus 20px before the change; all nine affected record workflows pass after it. Fresh measurement matches the original five Person field heights exactly (42px for plain values, 43px for the boolean badge). All 1,884 unit tests, production export/types and changed-file lint pass. Eight fresh after-fix entries (ten images including the lab scroll states) were inspected in both themes with no setup/page errors or document overflow; evidence is `record-spacing-fixed-review/`. The initial browser launch used an inactive default backend port and is retained separately from the valid regression run. The correction is not in published RC4.
|
||||
|
||||
## Earlier checkpoint — September 30, query rows and filter review
|
||||
|
||||
Compared 14 desktop query/filter states in both themes between the original Next export and the published RC4 JAR: 56 entries, no unexpected setup/page errors or document overflow. The retained baseline export fingerprint matches the earlier review. Inspected table, Basic/Advanced, populated/nested filter, quick editor, columns, column-menu, relative-date, density, selection, Views and Export captures. Three added controls have no equivalent original state and are marked accordingly. Evidence: `rc4-query-review/` under the local visual-review directory. This is a desktop subset, not whole-query or whole-product approval.
|
||||
|
||||
Found VIS-029: unconditional alternating row shading added by parity commit `4c7918f` changes the original Next table, especially in dark mode. Restored the original uniform default while retaining selected-row highlighting, opaque pinned cells, theme overrides and the `data-row-parity` customization hook. The correction is on the post-RC4 branch and is not in published RC4. Both new rendered-color tests failed against unchanged RC4, then passed on the corrected export. All 1,884 unit tests, production export/types, changed-file lint and 13 existing Chromium theme/density/pinning workflows passed.
|
||||
|
||||
The first browser attempt had a stopped server; a subsequent temporary symlink was not discoverable by the backend classpath scanner. Those setup failures are retained separately. The new pinned-cell test initially matched nested transparent text spans as well as table cells; correcting its selector produced two passing light/dark cases without further product changes. Corrected-build screenshots from that selector-failure run were inspected and show uniform rows with opaque pinned cells; their origin remains explicit. Fresh corrected-export captures are now complete and inspected for the small table, Person table, populated filters and selected rows in both themes (eight entries). Their manifests contain no setup or page errors. Evidence: `row-appearance-fixed-review/` under the local visual-review directory. Broader visual review remains open.
|
||||
|
||||
## Earlier checkpoint — September 29, report omitted-join preservation
|
||||
|
||||
Inspected the corrected report editor's Filters and sort and Columns tabs in desktop light and dark mode. The existing dialog, tabs, typed filter controls, sort controls, column picker, preview grid and footer remain in use. Saved joined fields remain labelled and editable even when omitted from new choices; the preview still shows their values. This inspection covers the affected state and does not approve the whole report family or the broader original-Next comparison.
|
||||
|
||||
Four captures and geometry/runtime results are retained in the local visual-review directory: `report-editor-omission-{light,dark}.png`, `report-editor-omission-columns-{light,dark}.png`, and `report-editor-omission-visual-result.json`. At 1440×1000 the dialog is 1376×936 with 32px margins in both themes, no document overflow and no page errors. The Columns tab keeps its existing internal scrolling. The isolated visual server used a separate sample database and was stopped after capture; owner previews were untouched.
|
||||
|
||||
The destructive open-and-OK regression was observed failing before the correction. Current verification: 1,884 unit tests, production export, types, affected lint and three real-backend Chromium report workflows pass; independent review found no further defects. The full current Chromium gate passed 575 tests and all 391 required rows, with zero failures, skips or flaky results. RC3 does not include this follow-up.
|
||||
|
||||
## Earlier checkpoint — September 29, script suggestions
|
||||
|
||||
Added caret-adjacent suggestions inside the existing script editor: QQQ API/logger helpers, language keywords and file identifiers. The parent dialog, textarea, split panes and syntax overlay are retained. Inspected desktop light/dark captures under `test-results/acceptance/script-completion-evidence`; list bounds and both-theme axe checks pass across all five profiles. This focused review does not approve the whole developer-tools family.
|
||||
|
||||
The new interaction passes 1,840 unit tests, types, affected lint, license, static export and bundle budgets; 15 Chromium developer/script workflows and 10 focused cases across all five profiles. Native WebKit groups consecutive edits differently from Chromium: the initial cross-browser assertion failed after a fill plus completion, and a plain-textarea probe reproduced it. Retained failures; no production undo workaround or retry was added. The replacement check starts from a loaded revision and verifies native undo and redo, then separately tests typed-prefix completion and SQL-backed save/reload. Source/caret validation and linear prefix scanning correct both independent review findings.
|
||||
|
||||
Baseline full Chromium at 95847c2 is now terminal: 556 passed, all 390 required rows, no failures/skips/flaky tests. Backend PR #913 is merged. The broad original-Next visual review and final release gates remain open.
|
||||
|
||||
## Earlier checkpoint — September 29, report filter indicators and API cache correction
|
||||
|
||||
Report previews now feed complete nested criteria into the shared grid’s existing filtered-column indicator. Clicking it opens the current filter without adding a condition. The read-only preview keeps edits local. Inspected the real-backend desktop light editor and read-only captures in `report-filter-indicator-final-artifacts`; the existing grid indicator and layouts are reused. The captured editor is scrolled to its active condition. This is a focused inspection, not whole-family approval.
|
||||
|
||||
The previous full local Chromium export (303ba61) passed **554 tests**,390 required rows,zero failed/skipped/flaky. The subsequent indicator change passes **1,828 unit tests**,types,affected lint,license,static production export,bundle budget and **30 real-backend Chromium report workflows**. The first focused browser run mistakenly served the previous static export after a standalone build; its missing-indicator failures are retained separately and are not evidence about the corrected artifact. The proper static export was then rebuilt and verified.
|
||||
|
||||
Remote eeab664 CI is terminal: Chromium549pass; Firefox WID-070 widget-loading timeout; WebKit NAV-023 internal navigation error; touch WID-072 application-API500,NAV-034 redirect timeout,REL-005 internal navigation error. Logs and traces are retained. The API500 was reproduced as a concurrent static metadata-cache race in QQQ; backend62461e163 fixes atomic initialization and endpoint-test fixture isolation, with110module tests and coverage passing. PR913 remains pending new CI/merge. All40 focused rechecks of the failed workflows pass across Firefox,WebKit,phone and tablet on the corrected export/backend. The original failures remain recorded; a focused recheck does not establish that all navigation and loading failures are resolved. Full final-source/browser-matrix verification and the original Next visual review remain open.
|
||||
|
||||
## Earlier checkpoint — September 29, report backend variants
|
||||
|
||||
Report previews now reuse the existing Next variant picker for tables that require a store/tenant selection. No query runs before selection; switching starts a fresh preview page and scopes query/count, copy and statistics. The picker works nested inside the report editor. Light/dark desktop captures show it above the parent dialog and the selected variant above the preview, with no horizontal overflow or page errors. No changes were needed to the shared picker's layout or stacking. Captures: `report-variant-picker-{light,dark}.png`, `report-variant-{light,dark}.png` and `report-variant-visual-result.json` in the local visual-review directory. This does not approve the full report family.
|
||||
|
||||
Depth verification passes 35 real-backend Chromium scenarios, all 1,826 units, production build, types, affected lint, license and bundle budget. Both ordinary and application-API previews are covered. A review-found delayed-copy race was reproduced then corrected: a response from the previous variant cannot overwrite a later clipboard copy. Independent frontend/backend reviews have no remaining findings in this scope.
|
||||
|
||||
The application-API browser run exposed a backend defect: its query/count executors ignored the received variant. QQQ PR #913 applies the ordinary endpoints' session handling. Two HTTP regressions failed then passed; all 109 API middleware tests and coverage pass. Backend build succeeds under its configured static-analysis gates, with warnings. The 35 browser checks used that corrected local middleware JAR; PR CI/merge is pending. Full embedded-query parity, original-Next visual review and release acceptance remain open.
|
||||
|
||||
## Earlier checkpoint — September 29, report saved-view resets
|
||||
|
||||
Report setup now exposes Reset Changes and Reset to New View beside the selected saved view. The first restores the imported view; the second starts a default draft. Both discard unapplied Basic quick-filter values, and neither updates the saved view or report. The stale quick-filter regression failed for both reset actions before the correction, then passed. Independent review confirmed the correction with no remaining findings in this scope.
|
||||
|
||||
The rebuilt export passes all 25 report/sharing Chromium scenarios (zero skips/flaky tests), 1,823 unit tests, types, affected lint, license headers and the bundle budget (1,035.8 KB / 1,050 KB). New depth cases verify copy limits, missing COUNT capability and empty preview behavior against the real backend, with persisted report and saved-view comparisons. This is targeted verification, not complete embedded-query equivalence.
|
||||
|
||||
Inspected desktop light/dark captures show the reset links beside the view selector, with readable controls, no dialog horizontal overflow and no page errors. Existing dialog, filter and preview composition is preserved. Evidence: `report-reset-{light,dark}.png`, `report-reset-visual-result.json`, `report-reset-chromium.log` and `report-reset-final-units.log` in the local visual-review directory. The broader original-Next comparison remains open; no complete screen family is approved.
|
||||
|
||||
## Earlier checkpoint — September 29, process details and RC1
|
||||
|
||||
Fresh Chromium desktop comparisons cover process widgets, results, collapsed internal errors and expanded error details in light and dark modes: 16 original/current captures, with no setup/runtime errors or document overflow. The inspected widget, result and expanded-error pairs preserve the original Next card and action composition. Reload controls remain compact; desktop widget content is slightly shorter than the reference. These checks do not approve the complete process family.
|
||||
|
||||
The expanded-detail review led to **VIS-028**, a reproduced accessibility defect in the application-theme configuration: its orange error color yielded 2.8:1 contrast for 12px text. Default light/dark passed. Process details now use the same derived dark/light error-text shades as the query error panel. Text size, position and disclosure behavior are unchanged. The three-theme regression first failed on the application theme, then passed; nine real-backend Chromium error/retry/completion cases and all 1,806 units pass. Build, types and affected lint pass. Evidence: `process-detail-contrast-{red,green,build,unit}.log` and `rc1-process-visual-{light,dark}.log` in the local visual-review directory.
|
||||
|
||||
RC1 preparation separately passed the full 544-case Chromium gate (before this one-line contrast correction), clean JAR verification, dependency audit and focused checks. RC1 was subsequently published from `e901df9` on September 29, with Maven Central and GHCR artifact verification recorded in the release notes. See the [RC1 notes](../releases/1.0.0-RC.1.md). The original Next visual review and all remaining Material behavior parity gaps remain final 1.0 work.
|
||||
|
||||
## Earlier checkpoint — September 29, appearance preferences
|
||||
|
||||
The user-facing Preferences dialog now exposes Light and Dark choices using native radio controls within its existing layout. The choice applies immediately and persists under the existing `qqq-dark-mode` browser/origin key; a fresh profile remains light. An application theme still requires light mode, now with a visible explanation and disabled choices. The saved preference is retained under that override; Reset to Defaults clears it to light along with table and record defaults. No system-following or account synchronization is claimed.
|
||||
|
||||
The initial two integration tests failed because the controls were absent. The completed change passes **1,803 unit tests** and **15 real-backend browser cases across all five profiles** (NAV-056 appearance and NAV-038 shell touch/focus). The browser audit initially caught transitional colors while switching palettes; it now waits for actual CSS animation completion before checking the whole visible dialog against WCAG A/AA rules. A subsequent tablet diagnostic caught a metadata request interrupted by the test's immediate reload; the scenario now waits for the application theme to load before reloading. Failure logs/traces are retained under `appearance-transition-*` and `appearance-reload-*`. Build, types, changed-file lint, license and bundle budget pass (1,034.5 KB / 1,050 KB).
|
||||
|
||||
Refreshed original/current Preferences captures in both themes at desktop, tablet and phone sizes, including the dialog's own scroll region. Inspected current light desktop/phone top and dark phone bottom: controls remain within the viewport, the footer remains available, and lower layout options are reachable by scrolling. The screenshot tool was corrected to target the Preferences scroll area rather than the phone navigation drawer. Current export fingerprint: `d87518fddea70bd830c3fb10ea3ebaae41410feca77f6b6df2daf56a24370c1a`. The gallery still has 106 light / 87 dark states per viewport; this refresh adds no new state IDs. No whole screen family is approved. Full remote acceptance on the previous pushed head is still running; this targeted result is not release acceptance.
|
||||
|
||||
## Earlier checkpoint — September 29, widget width and acceptance setup
|
||||
|
||||
The remote phone failure reproduced with a wider native font: the standalone choice stayed 224px while its neighboring scalar input measured 230px. The original width assertion is retained. The metadata-editor minimum is now `min(15rem, 100%)`: 240px where available, capped to 230px at the tested 320px phone viewport. A browser-only trial preceded the one-line source correction; the final export reproduces the trial without injected styles. System, Arial and monospace measurements and 320/393/810/1440px viewports show no document overflow. Plain scalar controls are unchanged.
|
||||
|
||||
The Firefox dashboard trace showed its page-opening hook ran before the owned backend session and diagnostics existed. A pre-navigation session assertion failed locally. The hook now initializes those fixtures first and verifies the owned session cookie; metadata and widget reads start after the reset. This corrects a definite fixture precondition. The original 17-second render delay is not claimed independently explained; full CI must verify that it does not recur.
|
||||
|
||||
All **105 targeted real-backend cases** pass across Chromium, Firefox, WebKit, phone and tablet: the complete sample-dashboard group and standalone input cases, including system/monospace center clicks and a 320px viewport. All **1,801 unit tests**, production export, types, lint, license and bundle checks pass. The original width and dashboard-heading assertions remain. Logs use `widget-ci-*`, `widget-width-*` and `widget-setup-red-browser.log`; the width red trace is preserved separately.
|
||||
|
||||
Refreshed two widget states in both themes at three sizes: 24 original/current entries. Inspected the current desktop editors and light/dark phone open-choice menu. Current captures match the final export with no setup/runtime error or document overflow. The original has no choice menu; its new state is explicitly marked non-equivalent. Recounting all six canonical manifests gives **106 light and 87 dark states per viewport**; this corrects the prior checkpoint's dark count after form captures were added. No whole family is approved.
|
||||
|
||||
## Earlier checkpoint — September 29, error readability and refreshed form review
|
||||
|
||||
**VIS-027 corrected:** the query error panel failed at 4.24:1 in default light mode, Retry hover at 1.8:1 in dark mode, and the configured orange error theme at 2.52:1. Error text now uses the existing theme-derived red-800 in light mode and red-400 in dark mode shades, with red-300 for dark Retry hover. The visible Sonner error notification separately failed at 4.34:1; its light surface now uses red-800. The configured orange pivot validation also failed at 4.25:1 and now uses red-800 in light mode. Panel, dialog and notification geometry, placement and timing are unchanged.
|
||||
|
||||
The broader browser run exposed lazy-fixture leakage: default report-theme tests did not request the backend reset and inherited the preceding configured theme. Its 34 passes / 16 failures are retained as diagnostic evidence. The report spec now requests the reset before every test; a deliberate configured-orange pivot case retains that contrast regression. Query and pivot checks assert the actual root theme. The final run passes **55 real-backend cases across all five profiles**: error panel/notification contrast, Retry hover and request, dismissal, SQL-verified recovery, report-link and pivot contrast, and theme metadata. All **1,801 unit tests**, production export, types, changed-file lint, license and bundle checks pass (1,034.1 KB / 1,050 KB). This is targeted acceptance, not final release acceptance. Logs use `query-error-{complete,pivot,final}-*`; confirmed earlier failures remain in their logs and `query-error-theme-red-artifacts`.
|
||||
|
||||
Refreshed original/current create, edit, copy, validation and field-lab forms in both themes at all three sizes:60 entries / 120 images, including scroll sequences. Inspected desktop edit, desktop field-lab middle/bottom, phone field-lab top and edit bottom, dark phone validation, tablet create, and dark desktop field-lab bottom pairs. Their form-body section spacing, field placement and footer positions match the original; the Person currency prefix is metadata-driven. This does not approve uninspected field interactions, modal forms or the whole forms family. Original/current error and Retry-hover captures were also refreshed. Final current-export refresh covers eight states per theme/size (48 entries): five forms, two query errors and pivot validation; no setup/runtime errors or document overflow, all with matching build fingerprints.
|
||||
|
||||
Inventory is **105 light / 84 dark states per viewport**. Other captures may carry older export fingerprints and must be checked before approval. No whole family is approved. Current preview on port 18769 serves the final contrast build; original on port 18768 remains the original Next design.
|
||||
|
||||
Full remote CI 36580709453 at 30db309 is now **terminal failure**: WebKit passed; phone WID-074 measured choice width 224px versus native scalar 226px, and Firefox WID-014 timed out waiting for the dashboard heading in beforeEach. The later failure snapshot contains that heading. Downloaded logs/traces are in `ci-36580709453-*`; neither failure is waived. The Firefox trace shows backend/diagnostics fixtures were never initialized before the failed page-opening hook. A phone font-loading probe did not reproduce the width failure: system-font scalar 201px and choice 224px stayed unchanged while the downloaded font was held/released. Investigate platform font sizing and hook setup next; do not change geometry or relax assertions without evidence. No merge or publication.
|
||||
|
||||
## Earlier checkpoint — September 29, query evidence and column-popup focus
|
||||
|
||||
**VIS-026 corrected:** opening Columns with Enter left focus on its toolbar trigger; activating the popup's Close button with Enter then left focus on the page body. The nonmodal popup now receives focus without scrolling or automatically opening a phone search keyboard. Explicit close returns to Columns; Escape keeps its existing focus return, and outside clicks retain focus on the clicked control. Embedded report column editors do not take focus when mounted. The popup layout and field controls are unchanged.
|
||||
|
||||
The component regression failed before the fix. A real-browser probe reproduced both focus failures; the corrected Playwright regression then failed at inactive popup focus. Its first attempt used a Testing Library matcher by mistake and is not counted as product evidence. After the fix, 48 affected component tests pass. The broader five-profile backend run passed 28/30; both failures assumed WebKit tabs to a button before search. A plain native-control probe showed Chromium/Firefox tab to Close while this WebKit runner tabs directly to search. The test now verifies those observed native paths while retaining popup focus, search, keyboard close, Escape and outside-click assertions. The final focus check passes all five profiles. Production export, types, lint, license and bundle checks pass. Logs use `column-focus-*`; the confirmed pre-fix artifacts are retained separately.
|
||||
|
||||
**Review evidence correction:** some older current captures still showed column widths that had already been fixed. The capture tool now stores a timestamp and SHA-256 fingerprint of the exported files in each new entry. The gallery displays them, flags untracked/older builds, and avoids resetting a newer scenario selection when an earlier fetch finishes. Six theme/viewport image-pair checks and both warning checks pass (`gallery-provenance-check.log`). Captures outside the refreshed query family still require freshness verification; they must not be treated as current approval evidence.
|
||||
|
||||
All 27 query states now have refreshed original/current entries in light/dark at desktop/tablet/phone sizes: 324 entries. Current entries have no setup/runtime errors or document overflow and match the final export fingerprint. Three added states cover column-popup keyboard focus, search and no matches. The original has no search control; those baseline captures explicitly state they are not equivalent interaction states. Inspected fresh desktop column configuration/menu/empty/error/loading states, desktop keyboard focus, tablet filtered columns and dark phone no-match presentation. Inventory is now 104 light and 83 dark states per viewport. No full family is approved.
|
||||
|
||||
**VIS-027 remains open:** the light query error panel fails a browser contrast check at 4.24:1 for its heading, message and Retry text (required 4.5:1). The dark panel passes. `query-error-contrast-probe.log` contains the measured foreground/background colors and affected nodes. Correct the text contrast while preserving configured status colors and the original panel layout, then verify both default and application themes.
|
||||
|
||||
Remote full CI36580709453 at30db309: WebKit passed; Chromium/phone/tablet and Firefox remain running. Keep local corrections unpushed until that run finishes. Full visual review, functional gaps and final release gates remain open.
|
||||
|
||||
## Earlier checkpoint — September 29, compact filter controls restored
|
||||
|
||||
**VIS-002 corrected for the reproduced desktop height regression:** the populated filter table started at y579, versus y535 in the original Next capture. The unsaved-view status forced a separate full-width row (24px), and the desktop filter header used 44px controls plus padding (20px above the original header height). A browser-only trial isolated both causes before the source change.
|
||||
|
||||
The status can now share available toolbar space at tablet/desktop widths; phone wrapping remains. Filter mode and sort controls use a compact 32px minimum on devices without coarse pointers and retain 44px minimums when any coarse pointer is available. The desktop panel header is41px, down from61px; the resulting panel and table positions match the original populated-filter capture. Basic/Advanced, sorting, ownership, save/reset actions and the single editable condition list remain intact. Tablet/phone measurements retain44px targets and no document overflow.
|
||||
|
||||
Verification: production export/typecheck, standalone typecheck, changed-file lint, license and bundle checks pass (1034KB/1050KB). All34 affected component tests and50 real-backend cases across Chromium/Firefox/WebKit/phone/tablet pass (QRY-080/083/084/051/093). These are targeted checks, not a whole-suite acceptance result. `filter-layout-red.log` preserves the pre-fix61px header failure; `filter-layout-trial.log` proves the browser-only correction; `filter-layout-green.log` checks the actual final export without injected styles. Other logs use `filter-layout-{unit,build,browser,types,license,budget}`.
|
||||
|
||||
Refreshed six current query states in both themes at three sizes:36 entries, no setup/runtime errors or document horizontal overflow. Inspected desktop light/dark populated filters, tablet populated filters and dark phone sheet. Original captures remain unchanged. Inventory stays101light/80dark states per viewport; no whole family is approved. Current preview includes this build. The full remote run36580709453 at30db309 is still running; general and image CI passed. Keep local corrections unpushed until the full run finishes. Full visual review and release gates remain open.
|
||||
|
||||
## Earlier checkpoint — September 29, copied-record timestamp persistence
|
||||
|
||||
The follow-up found ordinary record edits already protect unchanged timestamps by omitting them from PATCH. Base Copy and both child-row editor hosts submit complete values, however, and were discarding fractional seconds. The real-backend copy regression in America/New_York also reproduced a one-hour shift during the repeated autumn hour: `06:30:07.123456Z` became `05:30:07Z`. Copies and child edits now pass original record values through the existing wire converter; edited values retain normal local-to-UTC conversion.
|
||||
|
||||
Verification: **1,799 unit tests and 30 real-backend cases across five profiles pass** (REC-065/018/013/060 and both PRC-064 cases). REC-065 checks unchanged PATCH omission, the copy POST instant and source/copy SQL precision. Separate component regressions prove association and process child-row preservation; these are not claimed as new child-specific real-backend coverage. Production export, types, changed-file lint, license, whitespace and bundle checks pass (1033.9KB/1050KB). Logs use `record-datetime-*`; the confirmed pre-fix trace is retained in `record-datetime-red-artifacts`. The first browser attempt stopped at the existing unreadable-password copy guard; supplying a new fixture password allowed the timestamp defect itself to reproduce.
|
||||
|
||||
No layout or style changed in this correction, so existing visual captures were not repeated. Current preview includes the corrected export. Visual inventory remains101light/80dark states per size; no whole family approved. Full CI36580709453 at30db309 remains running across all browser jobs; general CI36580709451 and image36580709514 passed. Keep the new correction local until that full run finishes. Final visual review, complete release verification and publication remain open.
|
||||
|
||||
## Earlier checkpoint — September 29, date-time input review
|
||||
|
||||
**VIS-025 corrected:** zoned process values were assigned directly to datetime-local controls, which displayed blank fields. Plain standalone blocks sliced UTC digits into a local-time control and discarded seconds. Shared standalone editors displayed local values but sent unzoned text through their action callback. New component regressions and a fixed America/New_York browser context reproduced the failures.
|
||||
|
||||
Process fields and standalone blocks now reuse the existing date-time conversion helpers, retaining their native controls and seconds. Process payload conversion occurs after contributors. The converter can retain an original instant when its displayed wall time is unchanged, preserving the second occurrence of a repeated DST hour and hidden subsecond precision. Edited local times become UTC instants. Material's unadorned TEXT/HTML block mapping was inspected and confirmed to use ordinary text controls; no speculative editor redesign was made.
|
||||
|
||||
Verification: **1,796 unit tests and 30 real-backend cases across all five profiles pass** (PRC-064 unchanged/edited, WID-075, PRC-063/062 and REC-018). Backend SQL proves unchanged spring and autumn timestamps retain their instants; edits across the spring transition use the correct offset. A unit regression proves unchanged subsecond precision survives. Production export, standalone types, changed-file lint, license, whitespace and bundle checks pass (1033.9KB/1050KB). Logs: `block-datetime-{red-unit,callback-red-unit,precision-red-unit,red-browser,green-unit,unit,build,browser,types,lint,license,budget}.log`; pre-fix browser traces remain in `block-datetime-red-artifacts`.
|
||||
|
||||
Paired process timestamp and standalone editor captures were added/refreshed in light/dark at three sizes:24entries, no setup/runtime errors or document overflow. Inspected desktop process pairs, phone process inputs and dark phone standalone controls. The original process blocks show raw text and its regular form timestamps are blank; current controls show dates, seconds and the existing shared editor timezone hint. That hint adds height relative to the original block renderer and remains part of the broader composition review. Captures use UTC; separate acceptance uses America/New_York. Inventory101light/80dark states per size; no whole family approved.
|
||||
|
||||
Full CI36573705464 at121468b is now terminal: Firefox and WebKit passed, Chromium/touch failed dependency download before tests. This satisfies the hold on pushing local commits. Final full verification on the new source and the complete release audit remain required; no merge or publication.
|
||||
|
||||
## Earlier checkpoint — September 29, input configuration review
|
||||
|
||||
**VIS-024 corrected:** advancing a process moved focus from a configured autofocus input to the screen heading. A real-browser regression reproduced the inactive Plain Code field on screen two. The process now preserves focus already inside a marked autofocus editor; ordinary result screens still receive heading focus. Plain and shared metadata editors both pass the transition check.
|
||||
|
||||
Enter also inspected trimmed text while submitting the untrimmed process form. PRC-044 reproduced surrounding spaces in the actual SQL record. Plain/shared text blocks now normalize their own value before validation and submission. Required whitespace cannot advance; standalone shared blocks validate the normalized value before invoking their callback. Typed numeric/date/checkbox values and multiline, file and picker keyboard behavior retain their existing paths.
|
||||
|
||||
Verification: **1,791 unit tests and 25 real-backend cases across all five profiles pass** (PRC-037/044/062/063 and WID-074). Production export, types, changed-file lint, license, whitespace and bundle checks pass; total JavaScript is 1033.6KB gzip against 1050KB. PRC-063 checks placeholders, retained process values, first blur without premature validation, required whitespace, both autofocus paths, result heading focus and stored SQL text. Pre-fix traces remain in `block-enter-red-artifacts` and `block-focus-red-artifacts`; logs use `block-{enter,focus}-red-*` and `block-input-config-*`.
|
||||
|
||||
Added paired plain/formatted input focus captures in both themes at three sizes:24entries, no runtime/setup errors or document overflow. Inspected desktop plain pairs, phone formatted pairs and dark tablet formatted focus. These process cards preserve the original geometry; current required markers and focus rings are visible. Inventory is100light/79dark states per size. This closes the reproduced focus/Enter defects, not the full widget/process configuration audit or any whole-family visual approval.
|
||||
|
||||
Both previews use the updated fixtures; the original export is unchanged and the current export includes these fixes. Full remote run36573705464 at121468b: WebKit passed, Firefox running, Chromium/touch failed dependency download before tests. The final full release gate remains open; no merge or publication.
|
||||
|
||||
## Earlier checkpoint — September 29, record section keyboard review
|
||||
|
||||
**VIS-023 corrected:** record tabs had tab roles but no arrow navigation, single tab stop or tab/panel associations. The real-backend regression failed when Right Arrow left focus on Overview. Tabs now use manual activation: Left/Right/Home/End move focus; Enter/Space opens the section through the existing URL callback. This avoids mounting section widgets merely while moving focus. Tab enters the named panel, with a visible focus indicator. Phone accordion behavior is preserved. The approach follows the [WAI tabs pattern](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/).
|
||||
|
||||
Verification:1786unit tests, production export/typecheck, final types/lint/license/whitespace and bundle1033.5KB/1050KB pass. The related five-profile run passed32cases and found one ambiguous phone test selector (outer accordion and nested section share a region name). The test now follows the trigger’s actual aria-controls and retains name/value assertions; all five keyboard rechecks pass. Existing section/list/deep-link/joined-field checks passed. Logs: `record-tabs-{red-browser,green-browser,keyboard-green,unit,build,types,lint,final-lint,budget}.log`. The pre-fix trace is retained in `record-tabs-red-artifacts`.
|
||||
|
||||
Added record-section-focus and refreshed record-small pairs in light/dark at desktop/tablet/phone:24entries, no runtime/setup errors or page overflow. Inspected desktop original/current record card, dark tablet focus and phone accordion focus. Current layout retains the recovered original record composition; focus styling adds no layout spacing. Inventory is98light states/size and77dark. These are reviewed states, not whole-family approval. Configured/denied/error/loading variants and remaining form/widget review continue.
|
||||
|
||||
Local changes remain unpushed while fullCI36573705464 at121468b is active: Firefox/WebKit running; Chromium/touch failed during Corepack network setup before tests. General CI and image build passed. No merge or publication.
|
||||
|
||||
## Earlier checkpoint — September 29, compact report inputs restored
|
||||
|
||||
**VIS-020 corrected for standard report input screens:** restored the compact card, 384px ordinary field maximum, and left-aligned Generate Report action below the inputs. Removed the added visible Input/Step header and divided footer. Cancel remains a secondary inline action. The accessible form name, shared dynamic form, validation, typed values, possible values and process cancellation remain. Configured grid widths and rich editors retain their layout; longer or custom report processes retain their step navigation and components.
|
||||
|
||||
Before the change, two unit regressions failed because Generate Report was absent, and the real-browser layout regression failed because the extra Step header was visible. Final verification: **1786unit tests**, production export/typecheck, changed-file lint, license, whitespace and bundle checks pass. **30 real-backend cases across all five profiles pass**, covering report layout/required validation/database-matched output and shared mixed/filtered/required/composite process forms. Logs: `report-compact-{red-unit,red-browser,green-unit,full-unit,build,green-browser,lint,budget}.log`; pre-fix browser artifacts are retained in `report-compact-red-artifacts`.
|
||||
|
||||
Paired input, validation and result captures were refreshed in both themes at desktop, tablet and phone:36entries, no runtime/setup errors or document overflow. Inspected desktop input, phone input pairs, dark phone validation pairs, and tablet result. The normal card geometry now matches the original; inline Cancel and shared validation focus/message treatment remain visible differences. No whole report family is approved. Canonical inventory remains97light/75dark states per size. Current preview18769 includes this correction.
|
||||
|
||||
Final diff review kept rich/file/code/widget editors on their existing grid rules, including configured file widths. The final export,1786units and changed-file lint pass; RPT-006 passes again in all five profiles. Final logs are `report-compact-final-{build,unit,browser,lint,budget}.log`. Both current dependency audits report zero known advisories (316production dependencies;1038in the full audit); this does not replace the remaining application security checks.
|
||||
|
||||
Remote full acceptance36573705464 at121468b remains active: Firefox/WebKit running; Chromium/mobile/tablet failed before tests in Corepack dependency setup with network ETIMEDOUT/ENETUNREACH. Its log is retained as `ci-121468b-chromium-mobile-tablet.log`. General CI36573705788 and image36573705430 pass. Do not push the local report correction while the full run is live; final full verification and release remain open.
|
||||
|
||||
## Earlier checkpoint — September 29, saved-view reload correction
|
||||
|
||||
Full run36564701143 is terminal: Chromium/mobile/tablet and WebKit pass; Firefox failed QRY-093. The trace showed new filtered data before asynchronous page navigation updated the URL. Query URL changes now replace the current history entry synchronously through Next-integrated native history, preserving other parameters and the hash. No query layout was intentionally changed.
|
||||
|
||||
A deterministic Firefox regression failed before the fix with the old filter still in the URL. The corrected export passes25 saved-view, delayed-navigation, back/forward, quick-view and column-menu cases across five profiles, plus20 pagination, sorting, quick-search and return-navigation cases. The current tree passes1784unit tests, production export/typecheck, final standalone types, changed-file lint, license and whitespace checks; bundle1033KB gzip remains within1050KB. Logs: `query-url-{red-unit,red-browser,green-browser,related-browser,build,unit,types,lint}.log`. The final full run with this correction remains pending; these are focused checks.
|
||||
|
||||
The current preview includes the correction. Desktop report-input pairs were re-inspected against original source: original compact card has384px maximum-width inputs and an inline Generate Report action; current shared process screen adds header and footer dividers, step count and wider fields. VIS-020 remains open. Preserve the shared dynamic form/process lifecycle while restoring the report presentation. Full visual review and all family approvals remain open.
|
||||
|
||||
## Earlier checkpoint — September 29, composite metadata inputs
|
||||
|
||||
The block INPUT_FIELD integration now reuses Next's field editors for choices, formatting and supported adornments, retaining the process form and multipart upload path. Plain scalar input layout remains the reference. The richer code/filter/upload editors add genuine capabilities absent from the original block renderer; their additional height and composition are still under visual review. This does not approve the widget or process family.
|
||||
|
||||
**VIS-021, corrected in source:** the dispatcher initially treated the backend's default `%s` format as custom formatting. This changed ordinary scalar controls into the shared form layout. A fixture-based browser failure and unit regression exposed it; neutral formatting now preserves the original scalar path. Hidden and read-only metadata are respected in both hosts.
|
||||
|
||||
**VIS-022, corrected in source:** a standalone choice editor collapsed to approximately101px, placing its Clear button at the center of the control. A native hit test confirmed that a center click cleared the value instead of opening the choices. The original scalar input is approximately200px wide; standalone metadata editors now use a14rem minimum, also accommodating Firefox's approximately217px native input. The browser assertion compares their widths and retains the center click.
|
||||
|
||||
A typed-input fixture existed but was not registered, so the first new capture showed Page not found. It now has a dedicated `widgetInputEditors` app. The capture script treats unexpected not-found screens as failed evidence. Paired captures also cover `prcBlockEditors`, whose original renderer lacks the rich editors, and the unchanged `prcWidgets` route. These comparisons distinguish new capability from restored composition. Final verification and refreshed captures are recorded below when complete.
|
||||
|
||||
The remote full run36564701143 has WebKit success, Firefox520passes/1failure(QRY-093), and Chromium/mobile/tablet still running. The Firefox trace shows a concrete reload race: B-filter data appeared before asynchronous router replacement updated the address bar, so reload used the previous Avery filter. Fix remains open; no assertion has been weakened. Artifact11033840508 and its log are retained locally.
|
||||
|
||||
Final input-editor checkpoint: **1,783 unit tests pass**, production export/typecheck, changed-file lint, license and whitespace checks pass, and the bundle remains within1050KB gzip. **35 real-backend cases pass across five profiles**, comprising30 widget/process checks and five unchanged QRY-093 rechecks. Those rechecks do not resolve the proven CI URL race. Final logs are `block-editors-width-{build,acceptance,visual-light,visual-dark}.log`, `block-editors-final-{unit,types,lint,license,budget}.log`. The earlier width run's failures and traces are retained, including the aborted local run after repeated diagnosed failures; no remote job was canceled.
|
||||
|
||||
Canonical inventory:97light states/size(194original/current entries),75dark(150entries). All36 entries for the three compared scenarios have no setup/runtime errors or page overflow. Inspected final light desktop/phone standalone controls, light phone process lower controls, dark phone standalone controls, dark tablet process lower controls, and existing Widget Lab. Plain fields retain their compact treatment; actual adorned editors are larger than the original scalar controls. Complete configuration/autofocus and visual composition review remain open in the parity ledger. The two new acceptance rows are PRC-062 and WID-074; the generated whole-suite matrix still requires final release reconciliation.
|
||||
|
||||
## Earlier checkpoint — September 29, report interaction review
|
||||
|
||||
**Release not ready; full visual review incomplete.** Recovery head `06cf1b0` is now pushed to PR #14. General CI `36564701134` and image build `36564701149` pass; full acceptance `36564701143` remains running. The preceding full run ended with tablet/WebKit Export failures and Firefox saved-report opening delay. Local report review changes are verified separately and will stay local while the new full run is active.
|
||||
|
||||
Expanded the review with report inputs, required validation, completed output, filter variables, pivot editing/validation, schedule creation/day selection, sharing, and keyboard tab focus. The capture script now measures dialog-body scroll positions. Query/report dark coverage is paired at all three sizes; unavailable original editors remain explicitly non-equivalent. Other families still have dark/theme/interaction gaps. No whole family is approved.
|
||||
|
||||
**VIS-018 (medium), corrected:** the report filter/column tabs declared tab semantics but had no arrow-key behavior or labeled panel. Added automatic activation with Left/Right/Home/End, a single tab stop, a linked panel and visible keyboard focus using the existing script-tab pattern. Filter and column drafts persist across switches; hidden Columns metadata remains respected. Unit regression failed before the correction. The first browser run exposed Firefox's native tab stop on a scrolling panel at 1280×720; a native probe proved that an explicit `tabIndex=-1` moves Tab into the column search field. The assertion was preserved.
|
||||
|
||||
**VIS-019 (medium), corrected:** pivot validation text on its tinted background measured 4.12:1 in light mode and 4.21:1 in dark mode. Axe independently rejected both. The alert now uses darker light-mode red text and lighter dark-mode red text, preserving its border, tint, layout and validation behavior. Final native measurements are 5.48:1 in both themes, and all ten light/dark pivot contrast cases pass across the five browser profiles.
|
||||
|
||||
**VIS-020 (review open):** report inputs now use the shared process screen. The original compact input card and Generate Report button have gained an Input/Step header, wider desktop field, and Cancel/Submit footer. The full process lifecycle and dynamic fields must remain, but the additional height and action placement are not visually approved. Compare `report-inputs` and `report-input-validation` on desktop/phone before deciding the final presentation. Scheduling and sharing retain their original dialog layout in the inspected pairs; scheduling adds the metadata-driven Variable Values section. Its lower controls are reachable by scrolling.
|
||||
|
||||
Inspected pairs: populated desktop/phone filters; dark phone quick editor, columns and export; dark tablet relative-date menu; desktop/phone report inputs; phone schedule scrolls and sharing; desktop pivot; phone pivot validation including its lower controls; dark phone variables/pivot validation/sharing; dark tablet schedule Days popover. This list names reviewed evidence, not whole-family approval. Reports still need configured variants, denied/error/loading states, populated schedule/share interactions, complete keyboard closure, and review of the added editor composition. The functional embedded-query equivalence row remains Partial.
|
||||
|
||||
Evidence: `query-report-dark-completion.log`, `report-interactions-{initial,light,dark}.log`, `report-tabs-{red,green,focus-probe}.log`, `pivot-contrast-{light,dark,red}.log`; final verification and corrected screenshots follow this checkpoint. Guides now document report-tab keyboard behavior and the Firefox scroll-container distinction.
|
||||
|
||||
Final checkpoint verification: **1,773 unit tests and 45 real-backend report cases pass across five profiles**. Export/build typecheck, final standalone TypeScript check, changed-file lint, license headers, whitespace and bundle budget pass (1032.1 KB gzip / 1050 KB). Logs: `report-review-final-{unit,build,acceptance,budget}.log`, `report-tabs-focus-green.log`, `pivot-contrast-{light,dark}-green.log`. Corrected tab-focus and pivot-validation captures were refreshed in both themes at all three sizes and inspected on phone. Canonical inventory is now **95 light states and 73 dark states per size**, with 190/146 original/current entries. Report captures have no runtime/setup errors or document horizontal overflow. This remains partial release evidence; full visual review and the complete final release gate are open.
|
||||
|
||||
## Earlier checkpoint — September 29, inline profile naming restored
|
||||
|
||||
**Release not ready; full visual review incomplete.** PR #14 remains open at `4748b6c`; local recovery is verified separately. Full real-backend run `36557085746` now has Firefox519passes/1failure (saved-report record opening) and WebKit519passes/1failure (Export). Chromium/mobile/tablet remains running. WebKit's Export failure is corrected locally; Firefox is under diagnosis. Nothing is merged or published. No whole visual family is signed off.
|
||||
|
||||
The light inventory contains **85 scenarios at each of three sizes**. Dark coverage remains partial (47 desktop, 45 tablet, 47 phone scenarios, with some unpaired entries). Unavailable original interactions are explicitly marked. Captures are evidence to inspect, not approval.
|
||||
|
||||
New, Save As and Rename now use the original Next inline naming layout inside the profile section. Compact field/button styling, Enter behavior and focus return are restored while ownership, backend errors, copying and overwrite/delete confirmations remain. Paired light/dark captures at all three sizes show the recovered editor; added reset controls still increase the surrounding section height. This is not full process-family approval.
|
||||
|
||||
Current verification: **1,771 unit tests and 40 bulk-load real-backend cases across five profiles pass**. The earlier menu/Preferences checkpoint passed50/50 affected cases. Production export, final typecheck/lint, license and bundle budget pass (1031.9 KB gzip / 1050 KB). The PRC046 lifecycle test now targets the portaled menu correctly and verifies duplicate-name rejection. Parity recount is728Done,22DifferentUX,5Partial,2Missing,38N/A across795existing rows; the previous summary omitted one supplemental metadata row.
|
||||
|
||||
## Evidence and method
|
||||
|
||||
- Current source: `feature/next-1.0`; restoration, report editor and guide commits are integrated. The live current preview includes the verified recovery changes described below. Both comparison exports were refreshed on September 29.
|
||||
- Both builds run the same current sample JAR and acceptance fixture sources, with fresh admin browser contexts and the same Inter font bytes. Historical-reference type checking is skipped only in its isolated build; the current export completes its normal type check.
|
||||
- Local comparison gallery: <http://127.0.0.1:18770>. Earlier UI: <http://127.0.0.1:18768/app>. Current UI: <http://127.0.0.1:18769/app>.
|
||||
- Evidence is retained under `../qqq-frontend-next/test-results/visual-review/`: source/build provenance, capture scripts, manifests, screenshot sequences, and server/build logs. It is ignored generated output, not a published artifact.
|
||||
- Review dimensions: hierarchy, typography and spacing, color and icon treatment, control grouping, responsive layout, and interaction states. Confirm findings against source before changing defaults; preserve explicitly configured metadata behavior.
|
||||
|
||||
## Coverage
|
||||
|
||||
Current inventory: **106 light and 87 dark states per viewport**, each with original/current entries. The latest checkpoint refreshes standalone widget inputs and the open choice menu against the final export. Other states include older captures and need current-build verification before approval. The table below describes the initial inventory and its remaining review scope; later checkpoints document additional coverage, not whole-family approvals.
|
||||
|
||||
The initial pass captured 38 scenarios per build at desktop (1440×1000), tablet (810×1080), and phone (393×851): 228 build/state combinations and 447 screenshots, including long-page scroll sequences. Every entry viewport has received a paired visual scan. Detailed review of all scroll sequences, dark/configured themes, keyboard focus, and additional interaction states remains. No whole family is signed off yet. The baseline report-filter editor is unavailable because that functionality did not exist there; its capture is not an equivalent editor state. The desktop baseline delete capture was corrected during the populated-filter follow-up.
|
||||
|
||||
| Family | Captured desktop scenarios | Remaining review scope |
|
||||
|---|---|---|
|
||||
| Shell and navigation | Dashboard, app home, nested navigation, not found | Search, command menu, notifications, user menu, help, login/session, phone navigation, branding/theme variants |
|
||||
| Query | Small and wide table, filters, advanced filters, columns, views, export | Populated criteria and groups, quick-filter editing, column menus, density, cards, selection/bulk controls, saved-view states, pagination, empty/error/loading |
|
||||
| Records | Compact record, person, record lab, audit, delete | Every tab/list/card variant, associations, metadata menus, configured sidebar, collapse, sharing, warnings/errors |
|
||||
| Forms | Create, edit, copy, field lab | Each field editor, validation, disabled/read-only, dirty state, full copy, file/association editors, modal forms |
|
||||
| Processes | Wizard, component lab, widget step, bulk load | Subsequent steps, selection, validation review, progress, results, errors, cancellation, upload mapping |
|
||||
| Reports | Run inputs, saved report, edit, filter/column editor | Filter variables, columns, pivot editor, scheduling, sharing, results/download, validation |
|
||||
| Widgets | Sample dashboard, gallery, blocks, parents, controls, states, tables/charts, record widgets/editors | Map all 38 dispatcher labels to evidence; inspect every scroll capture, dropdown, tooltip, menu, loading/error/empty state |
|
||||
| Developer tools | Table and record developer pages | Script editor/viewer, code tabs, dialogs, ESB metadata/tools, responsive layouts |
|
||||
|
||||
## Findings
|
||||
|
||||
| ID | Severity | Observed difference | Evidence | Status |
|
||||
|---|---|---|---|---|
|
||||
| VIS-001 | High | Wide query tables now use very narrow numeric/date/boolean defaults. Header labels are reduced to fragments, dates wrap across lines, and monetary values clip. The reference keeps readable columns and scrolls horizontally. | `desktop/query-person`, `desktop/query-advanced`; `DataGrid.tsx` and `query-columns.ts` | Restored 150px defaults; configured/user widths preserved. Desktop dates/currency verified after rebuild. Long heading truncation remains under review. |
|
||||
| VIS-002 | Medium | Switching an empty query to Advanced adds a long unsaved-view status and links to the toolbar, moving Export and view controls to another row. The filter panel also grows substantially with separate mode and sort controls. | `desktop/query-advanced` | Corrected for the reproduced desktop regression: status shares available toolbar space and compact mode/sort controls restore original panel/table positions; 44px touch targets remain. See latest checkpoint. Whole query-family review remains open. |
|
||||
| VIS-003 | Medium | A record with process actions now has separate Copy and Delete buttons plus an Actions menu. The reference groups secondary actions in the menu. | `desktop/record-person`, `tablet/record-person`; `RecordActions.tsx` | Corrected and verified in desktop/tablet/phone record captures. Enabled menu actions suppress duplicate direct buttons, including nested menus. NAV-055 passes all five profiles. |
|
||||
| VIS-004 | Medium | Record tabs crowd the identity header and field groups have more vertical spacing. The earlier header has a clear gap above tabs. | `desktop/record-person`, `desktop/record-small`; `RecordViewHeader.tsx` display-contents wrapper and field layout | Normal wrapper and 16px field gap restored; desktop/tablet/phone record captures reviewed. |
|
||||
| VIS-005 | Medium | Widget header icons changed from colored outline glyphs to filled square tiles; the sample pie chart changed from a doughnut to a solid pie. | `desktop/widgets-sample`; `WidgetIconTile` and `QqqChartWidget` | Header glyph colors and doughnut restored; image paths, main-icon tiles, explicit colors, slice links, tooltips and legend toggles retained. Fresh desktop/phone captures reviewed; 30/30 affected five-profile widget cases pass. |
|
||||
| VIS-006 | High | Phone query pages always expose mode, sort, and quick-filter controls beneath the toolbar, pushing the first Person card from approximately y=474 to y=708. Opening the Filter sheet also adds unsaved-view links behind it. | `phone/query-person`, `phone/query-small`, `phone/query-filters`; `RecordQuery.tsx` unconditional phone controls | Corrected: shared controls now live in the phone Filter sheet; opening preserves mode. Fresh phone captures confirm first card returns to approximately y=474. Affected filtering/focus/record acceptance passes 83/83 across five profiles. |
|
||||
| VIS-007 | Medium | Phone app-page titles are reduced to a few characters after adding a home breadcrumb and a separate command-palette control. The earlier header preserves substantially more of the page name. | `phone/app-home`, `phone/widgets-sample`, `phone/not-found`; `Header.tsx`, `Breadcrumbs.tsx` | Corrected: compact mobile gaps, current crumb constrained to available width, and scrolling after metadata arrives or the trail resizes. Fresh phone app/widget captures reviewed; all header controls retained, navigation/focus/touch checks pass in five profiles. |
|
||||
| VIS-008 | Medium | Process widget buttons changed from compact primary buttons to full-width slate bars, with extra margins pushing the footer below the first phone viewport. | `process-widgets` on all sizes; process migration to shared `QqqComposite` inherited `ButtonBlock` defaults | Partially corrected: hosted process buttons use compact primary styling and disable during work; metadata colors/formats and shared action handling preserved. Desktop/phone captures reviewed; five-profile PRC-035–037 pass. Process text/label/header spacing still exceeds the earlier Next layout and remains under review. |
|
||||
|
||||
Existing defects also found in the reference: Views menus run off the right edge on phone/tablet; the phone Export menu opens partly off the left edge; records with many tabs have overlapping tablet labels; sample Big Number context overflows on phone. These are production-quality issues even though they predate the parity changes. They require focused interaction checks and fixes, not preservation as design intent.
|
||||
|
||||
Views and Export overflow is now corrected using the existing Radix dropdown pattern: collision-aware placement, available-height scrolling, arrow-key selection and Escape focus return. Existing item actions, native disabled buttons and permissions are preserved. Fresh desktop/phone captures reviewed; 45/45 saved-view/export real-server cases pass across all five profiles, including full viewport intersection assertions. Tablet tab labels and Big Number context are corrected in the responsive checkpoint below.
|
||||
|
||||
Dashboard, app-home content, wizard entry, basic process components, centered Person create/edit/copy forms, blocks/parent widgets, and developer-page structure retain the earlier overall layout in the captured states. Form headings and currency adornments differ. Reports and record-widget forms gained previously missing filter, pivot, dynamic-form, and variable sections; their additional length is functional work that must survive restoration. Typed widget table cells now render values instead of raw markup/object strings. These observations cover entry states only and do not approve the full workflows.
|
||||
|
||||
## Historical release checkpoint — September 28
|
||||
|
||||
- Next UI 1.0.0 remains unreleased. PR #14 remains open at `93b7b5a`; full real-backend run `36507768282` is in progress. The local query-menu follow-up described below is verified separately.
|
||||
- Remote source/typecheck/lint/unit/coverage, mocked E2E, Storybook, bundle, CircleCI and both architecture image builds pass. Full real-backend run `36501934828` completed with failures in every job. Chromium/mobile/tablet reported 143 failures and 1343 passes; Firefox reported 52 failures and 460 passes. This is not a green release gate.
|
||||
- A missing middleware API dependency in the separate security fixture caused many failures. Both fixture launch paths now share classpath resolution. The local security/no-search/process-field-widget rerun passes **124/124 in Firefox and WebKit**.
|
||||
- Report preview/editor verification passes **15/15 across all five profiles**. Read-only previews support temporary paging, sorting and resizing without writing to the saved report. Column widths from metadata are retained.
|
||||
- Current full unit checkpoint: **1765/1765**. Production export/typecheck, license headers and bundle budget pass (1032 KB gzip / 1050 KB). The remaining CI regression set passed 140 cases across all five profiles; its five script author/date selector failures were corrected and rerun successfully (5/5). This does not replace the final full gate.
|
||||
- Functional ledger remains **734 Done, 15 Done with different UX, 5 Partial, 2 Missing, 38 N/A** (794 rows). Remaining groups include base-path hosting, Google Drive picker, embedded query/report controls, complex block input adornments, unmapped icon names, and script autocomplete.
|
||||
- Developer, agent and CSS-hook guides are integrated; the developer/agent report-preview descriptions now reflect the paged grid. Final browser workflow and visual approval remain open.
|
||||
|
||||
## Release sequence
|
||||
|
||||
1. Complete the screen/state inventory and paired desktop, tablet, and phone evidence, plus theme and accessibility states.
|
||||
2. Review every capture and record regressions with source causes. Correct shared defaults and verify the affected families together.
|
||||
3. Reconcile the developer/agent guides and functional parity ledger with the chosen Next interactions.
|
||||
4. Run the final five-profile real-server acceptance, source, accessibility, security, performance, and packaging gates on the resulting source.
|
||||
5. Merge and publish Next UI 1.0.0 only after the complete release evidence is green. QQQ 4.1 remains unreleased.
|
||||
|
||||
September 28 follow-up: phone filter controls and restored widget styling are in the current preview on port 18769. The initial capture remains archived for comparison; manifests contain refreshed captures only for the scenarios explicitly recaptured. Remaining priority regressions are VIS-002 (dirty-view toolbar), VIS-007 (phone header identity), VIS-008 (process widget button defaults), plus responsive defects and detailed visual states.
|
||||
|
||||
Later September 28 checkpoint: VIS-007 and process button defaults are corrected in the refreshed current preview. Full unit suite: 174 files / 1763 tests pass. Production webpack export/typecheck, source lint (0 errors, 5 existing warnings), license and bundle budget pass. NAV-026/037/038 and PRC-035/036/037 pass 30/30 across the five profiles. Header metadata-arrival and process disabled-action regressions have red-to-green unit evidence. Process spacing, VIS-002, responsive defects, and detailed states remain open; these focused checks do not replace the final full release gate.
|
||||
|
||||
Query-menu checkpoint: full unit suite 174 files / 1765 tests passes. Static export and standalone webpack builds/typecheck, lint (0 errors, 5 existing warnings), license and bundle checks pass. Mocked E2E passes 102 tests with two existing skips; its advanced-filter helper now explicitly selects Advanced after opening Filter, matching the restored mode-preserving behavior. This corrects the 13 stale-helper failures in CI run 36501112971 at pushed head `75044c1`. Real-backend CI run 36501113002 on that head was still running at this checkpoint. Final all-surface visual review and release verification remain incomplete.
|
||||
|
||||
Mocked-suite follow-up: the two shortcut-suppression checks were skipping when the lazy-loaded query input had not appeared yet. They now wait for the input, verify the typed character, and assert the global dialog stays closed. Full mocked E2E passes **104/104, no skips** (`query-menus-mocked-e2e-final.log`). Menu checkpoint `5cb2f5a` is pushed; real-backend run 36501934828 and CI run 36501934819 are in progress. The previous real-backend run was cancelled by the new push, so it is not completion evidence. The test-only follow-up stays local until that run finishes or another necessary source checkpoint is ready.
|
||||
|
||||
Spacing review checkpoint: redundant desktop filter heading and nested padding removed; mode/sort controls share the header, while the phone sheet retains its accessible title. Process-hosted labels, text and non-card widget headers use compact spacing. Existing explicit metadata styling and coarse-pointer targets remain. Preview refreshed on port18769; screenshot evidence and `spacing-*.log` files are in the root evidence directory. The populated query still has both a preview and editable conditions; further hierarchy/mode-state review remains open. Tablet tab labels and phone Big Number context defects were reproduced with DOM measurements, and browser-only CSS trials confirmed their causes. Source corrections and regression assertions are the next checkpoint.
|
||||
|
||||
Responsive checkpoint: record tabs now use content-based flex sizing, preventing labels from drawing outside their buttons; the tab bar retains horizontal scrolling for larger sets of sections. Composite rows with space-between alignment wrap when needed, and Big Number widths stay within their available container. Explicit style overrides remain effective. `responsive-red.log` proves the new bounds assertions fail for clipped phone number context and tablet tab labels before the correction; `responsive-green.log` passes all18 applicable cases across5profiles (WID023/WID040/REC004). Production export/typecheck, affected ESLint and diffcheck pass. Fresh desktop/tablet/phone record, sample-widget, block and parent captures are stored in the manifests. Desktop sample-widget arrangement is preserved; phone context now wraps within the card. Both image architecture builds are green remotely; full real-backend CI36501934828 remains in progress.
|
||||
|
||||
Review inventory now has41scenarios at3sizes; the historical report-filter and quick-filter editors are unavailable and are explicitly recorded as non-equivalent captures. Populated condition and nested-group states were inspected at all3sizes, plus current quick-filter editing. Phone block/parent scroll viewports1 were compared with the reference and retain their content layout. Remaining work still includes all other scroll sequences, additional interaction states, themes/focus, mode/preview hierarchy, long table headings, remaining functional gaps, and final release gates.
|
||||
|
||||
|
||||
## Report preview and CI diagnosis checkpoint — September 28
|
||||
|
||||
The report view now shares the editor's paged, sortable, resizable grid. Preview state stays local; saved column widths are preserved. The report tests pass **15/15 across all five profiles**, including database before/after comparison proving that preview interactions do not change the saved report. The latest phone report capture shows contained pagination with an unbroken page indicator. Selected Basic/Advanced controls use solid theme primary/foreground tokens; measured dark-theme contrast is **6.70:1**. These changes do not close the remaining embedded-report control gaps.
|
||||
|
||||
The remote full Firefox/WebKit jobs failed. A shared security-fixture classpath correction resolves missing middleware API classes: **124/124** security, no-search, and process-field-widget checks now pass locally across those two browsers. Other failures include stale control-location assertions, fixture catalog changes, and a real column-filter defect that adds a condition while leaving its editor hidden in Basic mode. The latter now opens Advanced directly; the rebuilt export/typecheck passes and affected browser verification is running. General Filter opening still preserves mode. Final whole-suite release verification and the full visual review remain open.
|
||||
|
||||
|
||||
Visual state follow-up: paired form-validation and empty-query captures were added and inspected on desktop, tablet and phone, increasing the inventory to 43 scenarios. Empty-query composition and validation-field layout retain the earlier Next design. Form heading copy differs (Creating New Person versus Create Person), and configured currency adornments remain visible. Phone Field Lab scroll captures 1–6 were compared: field spacing, rich text, password/file controls and later metadata sections retain the original layout. These captures do not approve picker/editor interactions or all form states.
|
||||
|
||||
CI diagnosis follow-up: the column Filter action now opens Advanced so its newly added condition is visible; general Filter and keyboard opening preserve mode. Acceptance assertions follow the restored section headings, secondary action grouping, 150px default widths and statistics menus while retaining metadata-width and permission checks. Script revision tests now select required API metadata and assert the API line separately from author/date. The phone menu test uses actual taps: Firefox with touch emulation emitted no pointerdown for a synthetic mouse click, while four diagnostic touch taps opened the menu successfully.
|
||||
|
||||
Checkpoint verification: `recovery-five-profile.log` records 140 passes and five failures at the outdated author/date selector; `script-api-author-green.log` passes those five corrected cases. `recovery-full-unit.log` has 1765 passes. Final TypeScript and changed-file lint pass; source lint has zero errors and five existing warnings. License headers, diff check and bundle budget pass. Current preview port 18769 was refreshed with this export and responds 200.
|
||||
|
||||
Final old-run diagnosis: the Chromium/mobile/tablet log contains the same corrected failures plus two mobile export assertions receiving an intersection ratio of 0.9999999403953552 instead of exactly 1, and one WebKit internal navigation error. Viewport checks now measure all four edges with a half-CSS-pixel rounding tolerance; the remaining three-profile security/export rerun passed **184/184**, including the previously failed session-switch case. Together with the Firefox/WebKit run, the security classpath fix is verified on all five profiles. Validation error text was measured at 4.58:1 in light mode and 5.01:1 in dark mode on the sample form background.
|
||||
|
||||
Dark validation follow-up: phone and tablet comparisons retain the baseline field hierarchy, but both builds had nearly invisible native date-picker icons because no browser color-scheme was selected. The stylesheet now assigns light/dark color-scheme with the existing theme selectors; rendered verification passes in Chromium, Firefox and WebKit for both light and dark, with the OS preference deliberately opposite. A fresh tablet capture confirms the calendar icon is visible. Saved-view/export/metadata-theme checks pass **40/40 across all five profiles** after the viewport assertion correction.
|
||||
|
||||
|
||||
## Query controls and gallery checkpoint — September 28
|
||||
|
||||
The light capture inventory now has **51 scenarios at each of three sizes**. New states cover density menu/compact/comfortable, cards, Selection menu, subset dialog, selected rows, and Save View As. Selection/density and both dialogs also have paired dark captures at all three sizes. Inspected evidence includes desktop card layout, phone selected cards, phone/tablet Save View As, and light/dark phone menus. These preserve the reference composition; review of the remaining states continues. Captures alone do not approve the entire query family.
|
||||
|
||||
**VIS-009 (medium):** Selection extends beyond the phone viewport; Selection and Density lack arrow-key navigation, and Density ignores Escape. Browser diagnostics reproduce these failures before the change. Both now use the existing Radix dropdown primitive with the original control styling and actions, bounded placement, keyboard selection, dismissal, and focus return. The selected Density label had 2.43:1 dark-mode contrast; it now uses the same primary/foreground pair as Basic/Advanced. The subset dialog restores focus to Selection.
|
||||
|
||||
Verification: 1765/1765 unit tests; 30/30 real-backend density/selection/bulk/card cases across Chromium, Firefox, WebKit, phone and tablet; production export/typecheck and targeted lint; license and bundle budget (1032 KB gzip). The first acceptance run passed29 and failed the Firefox dedicated phone case because synthetic mouse input did not open the Radix trigger; the corrected test uses actual taps and asserts menu bounds. Exact diagnostics are query-menu-red.log/query-menu-green.log; final acceptance is query-controls-acceptance-green.log.
|
||||
|
||||
The gallery previously missed multi-profile capture manifests. They are now merged into canonical per-profile manifests, future captures update those manifests, and a theme selector exposes light/dark evidence. Browser checks verify both comparison images load at all six theme/size combinations. Save-dialog capture setup now uses the visible Save As action (the separate save-as hook only exists for an existing saved view).
|
||||
|
||||
**Still open:** tablet headings truncate more than the baseline because a hidden sort glyph, touch-sized column menu, and cell padding consume label space. Compare tablet query-save-dialog background and desktop query-density-menu. Preserve configured widths and touch targets when correcting this. Full query state review, all other families, remaining functional ledger gaps and final release gates remain open.
|
||||
|
||||
Final menu evidence: selected Density contrast is **6.70:1** in dark mode (`density-contrast-green.log`), with the final phone capture inspected. The final export/typecheck, changed-file lint, license and bundle checks pass. A further **10/10** density/selection checks pass across all five profiles on the final build, including explicit focus return after the subset dialog closes (`query-controls-final-acceptance.log`). Current preview was refreshed from this export.
|
||||
|
||||
|
||||
## Query headings, states and WebKit checkpoint — September 28
|
||||
|
||||
**VIS-010 (medium), corrected:** hidden unsorted-arrow space plus menu targets and cell padding clipped First Name/Last Name on touch layouts and Annual Salary on desktop. The header now reserves an arrow only for an active sort, reclaims trailing padding next to its menu, and permits two-line touch labels within the existing header height. Default widths remain150px; explicit SIZE widths and resize remain effective; sort/menu targets stay separate and touch menus remain44px. Browser measurements confirm the four tested Person labels fit without increasing the header height (37.5px desktop,60.5px touch). The new assertion failed on all3 initial profiles before the change; **45/45** affected checks pass on all5 profiles afterward, including sorted headings, sorting, resize, column menus, statistics and explicit metadata widths.
|
||||
|
||||
Added paired query statistics, loading and backend-error captures at desktop/tablet/phone. Loading deliberately holds the query response; it is a layout check, not evidence of backend latency. Desktop statistics retains the original arrangement with an added Export button. Phone statistics has a more crowded title beside the added action and remains a visual follow-up. Error-state composition is retained; the corrected dirty-view toolbar grouping remains contained. Column menus and relative-date controls did not exist in the original reference, so those baseline captures are marked non-equivalent. Inventory reaches56 light scenarios with the relative-date menu follow-up.
|
||||
|
||||
**VIS-011 (high), corrected:** full CI and local WebKit reproduced the relative-date menu extending above the viewport, leaving7daysago unreachable. The menu now caps its height by both70dvh and Radix's available placement height, with a viewport width bound. Acceptance asserts the menu's actual bounds before choosing a date. **25/25** date, phone saved-view/export, restriction and report-sharing cases pass across all5profiles after the correction.
|
||||
|
||||
Full remote WebKit job109212960890 finished **509passed/4failed** at93b7b5a. Besides the reproduced relative-date issue, failures were a saved-view touch trigger that stayed closed and two document-navigation diagnostic cases (SEC010 andRPT017). Traces place those API errors39–54ms after a full navigation began while initial reads were still starting; those two tests now let initial reads settle before deliberate document replacement, with API/security assertions unchanged. Saved-view opening now has an immediate expanded-state assertion, but its intermittent CI failure is **not declared resolved** by the focused pass. Other full-CI jobs remain in progress at last check. Artifacts/logs retained under ci-webkit-36507768282 and ci-36507768282-webkit.log.
|
||||
|
||||
Verification: query-heading-red.log then query-heading-green.log45passes; query-heading-measurements.log; query-heading-date-unit.log1765passes; relative-date-and-ci-green.log25passes. Production export/typecheck, changed-file lint, license, bundle anddiff checks pass. Current preview contains the heading/date fixes. Final full acceptance and the complete visual review remain open.
|
||||
|
||||
## September 29 — report controls and statistics checkpoint
|
||||
|
||||
The current preview has been refreshed from the production export including report Basic/Advanced modes, saved-view selection, refresh, density, column configuration and column actions. Existing editor tabs and Cancel → OK → host Save remain. Read-only preview interactions stay local. These additions use the existing Next controls. The original report filter editor was absent, so it has no equivalent baseline state. Light inventory is now 60 scenarios at each of three sizes; captures are not full-family approval.
|
||||
|
||||
Paired statistics screenshots revealed that adding Export squeezed the phone title into three lines. A wrapping header now gives the title its own phone row while keeping desktop actions alongside it. The rendered check changed from 84px (three lines) to 28px (one line) at 393px width. Fresh original/current captures exist at all three sizes. The inspected dark report states include desktop Advanced, tablet Basic, and phone Columns/Density/column menu: popups fit the viewport and the footer remains visible; phone preview pagination requires scrolling the modal body. Dark blue links remain a contrast investigation. No whole visual family is signed off.
|
||||
|
||||
Evidence: report-controls-corrected-acceptance.log has 90/90 cases across five profiles, including draft save/cancel, saved-view import, API-versioned refresh, paging/sort/resize/pins, copy full query, nested statistics, headings and previously intermittent menu/schedule cases. The earlier expanded run used a Person fixture without QUERY_STATS and failed those five statistics cases; corrected qryItem fixtures passed in the clean run. Source build/type checking passed. Latest remote full acceptance at6439944 remains failed; local focused success does not replace that release gate. Touch-menu tests now attach native input/state diagnostics on failure, and schedule tests assert CSV selection before submission.
|
||||
|
||||
## September 29 — dark-mode and form-title recovery
|
||||
|
||||
The report link contrast defect reproduced at 2.58:1 in a browser accessibility scan. Separating primary text from branded fills raised the measured sample link contrast to 8.83:1. Broader tests with OS preference opposite the selected UI mode exposed three more defects: unbranded dark primary buttons at 3.23:1, a record badge following OS dark mode on a light page, and dark table boolean values using a dark green. Tailwind dark variants now follow ThemeProvider's class; unbranded dark fills use the readable default blue, while true boolean values use a brighter green on dark surfaces. Light styling and explicit application theme rules remain. 53 checks across five profiles passed, covering both UI modes, default/brand themes, the six main accessibility routes, grids, record layouts and widgets.
|
||||
|
||||
Compared dark phone record/form entry states, all seven Field Lab phone captures, tablet process widgets, and desktop widget overview against the original Next build. Form geometry remains aligned; native date/time icons and link contrast are corrected. The Create heading still said Creating New: restored the original Create wording and retained CSS hooks, heading overrides and accessible Edit/Copy record labels. The parity row now accurately records this intentional Next presentation (733Done,16DifferentUX,5Partial,2Missing,38NA).
|
||||
|
||||
Evidence correction: widget long-page captures revealed duplicate near-top images because document smooth scrolling had not completed. The capture script now targets the actual main content scroller, uses instant scrolling and verifies actual versus requested position before each screenshot, recording positions in manifests. Scrolling the document could move the application out of view and produce blank evidence. Earlier numbered scroll captures without verified positions must not be counted as complete scroll review; affected long-page evidence is being regenerated. No whole visual family is signed off.
|
||||
|
||||
|
||||
## September 29 — chart restoration verified
|
||||
|
||||
Long-page comparison confirmed two presentation changes: stacked charts moved their value axis to the right, and small line charts gained an axis and grid. The original left axis and compact small-line presentation are restored. Integer ticks, dataset tooltips, metadata colors, legend toggles, navigation and accessible data remain. WID-069 and WID-010 pass 25/25 across Chromium, Firefox, WebKit, phone and tablet; visual-recovery unit tests pass 1768/1768, production export/typecheck succeeds and lint reports zero errors/five existing warnings. The ledger now records these intentional Next differences (731 Done, 18 Different UX, 5 Partial, 2 Missing, 38 N/A).
|
||||
|
||||
The current preview was refreshed from that export. Dark form-field and sample-widget captures were regenerated against the original at three sizes with verified main scroll positions. Phone lower-page comparisons confirm the compact line presentation, HTML/Stepper layout and chart card structure; current legend controls and the As of date add content/height. Remaining label-rotation differences need review. No whole family is signed off.
|
||||
|
||||
Remote c442613 WebKit completed with 513 passes and three failures: Annual Salary header clipping, Selection touch opening and Views touch opening. Its native input diagnostics are being retrieved. General CI and image builds pass; the other full acceptance jobs are still running. Focused local passes do not close these remote failures.
|
||||
|
||||
|
||||
## September 29 — CI menu and header diagnosis
|
||||
|
||||
The completed c442613 runs report Firefox 515 passes/one heading failure and WebKit 513 passes/three failures (heading plus Selection/Views). The Chromium/phone/tablet job remains in progress. Native WebKit input attachments prove the two menus open on pointer-down and close after the same tap's compatibility click. A local probe reproduces dismissal when focus returns to the trigger; the regression unit test failed before the correction. Selection and Views now exclude their own trigger from outside-interaction dismissal, while outside clicks, Escape, selection and focus return remain supported. The regression unit passes and 30 real-backend menu/basic/saved-view checks pass across five profiles, including explicit trigger-focus retention. The full remote suite must still verify this correction.
|
||||
|
||||
The heading failure is font-dependent: the application renders with its system/configured font, and the original two-pixel spacing adjustment did not fit the wider Linux font. A local Verdana probe reproduced 13px of clipping at the preserved150px width; allowing two lines removed clipping. Header labels now use the existing two-line limit on desktop as well as touch. The wider-font browser regression failed before the change;20/20 heading/sorting/shared-view checks now pass across five profiles. Affected grid/menu unit tests17/17, production export/typecheck, changed-file lint and diff checks pass; the full unit suite immediately before the CSS-only header correction passed1769/1769. Bundle budget passes at1031.5KB gzip of1050KB. Logs: menu-trigger-red/green/unit/acceptance, header-font-probe/red/acceptance/build/unit/lint, visual-recovery-final-bundle.
|
||||
|
||||
Current preview includes theme, Create-title, chart, menu and header corrections. Dark tablet Field Lab entry and phone record-person lower viewport were compared with the original and retain field/section geometry; date icons and primary-link contrast are corrected. Desktop/tablet sample-widget lower viewports retain card layout and the compact small chart; legend controls and conditional category rotation still account for visible differences. Complete chart-label review and all other open family/state reviews remain required.
|
||||
|
||||
|
||||
## September 29 — chart labels and single-list filter editing
|
||||
|
||||
**VIS-012, corrected:** the chart rotation heuristic treated every character as 6.5px wide and divided plot width by label count, turning month/fruit labels that fit horizontally in the original Next charts. SVG measurements show neighboring labels fit at desktop, tablet and phone widths. The category axis now measures actual glyph widths and tick positions, remeasuring on chart updates and font loading; long labels retain rotation and bounded axis space. The new WID-009 browser assertion failed before the correction; 40/40 chart data/legend/tooltip/rotation cases pass across all five profiles. Live measurements in chart-label-red.log/chart-label-green.log demonstrate horizontal labels with positive gaps afterward.
|
||||
|
||||
**VIS-013, corrected:** Advanced conditions appeared twice (preview chips plus editable rows), with two Clear actions opening the same dialog. The earlier Next single editable list is restored. Each row still deletes its own condition; one footer Clear action still confirms and retains sorting. Basic mode retains its quick filters and header Clear action. The now-unused duplicate component/test were removed; the replacement unit checks the single-list layout, and QRY-083 now compares SQL results for two conditions before deleting one and proving the second remains active. This is recorded as Different UX in the parity ledger (730 Done, 19 Different UX, 5 Partial, 2 Missing, 38 N/A). Required acceptance coverage remains enabled.
|
||||
|
||||
The new layout unit failed before the correction. Full unit verification passes 1768/1768 in 173 files (one duplicate-component test removed); source lint has zero errors/five existing warnings and license headers pass. The first build caught a Testing Library assertion using Playwright's exact option; that test-only typing error was corrected and the final production export/typecheck passes. Filter acceptance passes 55/55 across five profiles, covering mode switching, quick filters, saved views, per-condition deletion, confirmed clear, sort retention, field validation, dates, boolean/blob and long-text operators.
|
||||
|
||||
Fresh original/current captures cover populated/nested filters and sample widgets in both themes at all three sizes. Inspected light desktop/phone populated filters, tablet nested filters, dark phone populated filters, and restored desktop/phone chart labels. Single-list geometry is recovered; Basic/Advanced and sort controls remain necessary additions. Current quick-filter editing is captured; its baseline explicitly states that the control did not exist. All captured long-page positions are verified. No complete family is signed off yet.
|
||||
|
||||
Full c442613 acceptance 36549979322 is now terminal/failing: Chromium 515/1, phone 490/1, tablet 486/5, Firefox 515/1, WebKit 513/3. Besides heading/menu failures corrected in 5077c78, phone/tablet RPT-009 reproduced an undersized standalone report link (146x20); it now has a 44px target. The two tablet navigation traces show deliberate document replacements overlapping sidebar prefetch requests; those tests now wait for initial reads to finish. No diagnostics or assertions were waived, and their intermittent CI outcome still requires the next full run. Exact artifacts and logs remain in the root visual-review evidence directory.
|
||||
|
||||
Final follow-up verification: nested-and-ci-acceptance.log reproduced the report touch failure on phone and tablet (26 passes/two failures). After correction, nested-and-ci-green.log passes 28/28 across five profiles, including nested AND/OR filtering, lookups, owned/shared records and report touch targets. Affected report/filter units pass 21/21. Final production export/typecheck, changed-file lint, diff check and bundle budget pass (1031.3 KB gzip / 1050 KB). The completed old CI run remains failed; these local passes do not substitute for the next full run.
|
||||
|
||||
|
||||
## September 29 — expanded process review and saved-profile restoration
|
||||
|
||||
Added paired light/dark captures at desktop, tablet and phone for expanded help, required-field errors, component review/download, wizard details/validation/confirmation, cancel dialog, running progress, user-facing error, scanner, state-machine entry, widget result, bulk file mapping, value mapping, review and completed validation. All 192 new combinations have clean setup/runtime capture status and no document-wide horizontal overflow. Scroll captures record and verify main-content positions. The first required-field selectors were too strict about labels or matched a containing section; the final captures use the actual field roles. Completed bulk validation is identified by its result text, since the original did not expose the newer status hook.
|
||||
|
||||
Inspected pairs include desktop component review and both bulk-mapping viewports; phone required-field error and footer, wizard confirmation, cancellation, progress, scanner, error, loop and widget result; dark desktop bulk mapping, dark tablet value mapping and wizard validation, and dark phone bulk-mapping footer and cancellation. These process compositions retain the original Next arrangement. Added controls and readable dark primary text remain. More process variants, saved-profile menu/dialog states, internal-error detail, bulk selection/results and composite modal states still need visual review; this is not full process-family approval.
|
||||
|
||||
**VIS-014 (medium), corrected:** the original bordered Saved Bulk Load Profiles section, heading and visible current-profile status were removed during the profile-action expansion. Restored that section in the shared component. Restored the original `showCurrent` option so file/value mapping identifies the applied profile while the final review summary avoids duplicate status. Current menu, owner permissions, reset/rename/save/delete behavior, help, typed value mapping and searchable fields remain. The extra actions can increase phone height; verified scrolling reaches the footer. Unit assertions first failed for the missing named region and for missing value-step status, then passed after correction.
|
||||
|
||||
**VIS-008 clarification:** browser measurements show Reload occupies the same header row as the title. It increases a compact widget header to 44px on touch devices (24px on desktop); this is the touch-target requirement, not an accidental separate row. The hosted ad hoc body is 2.5px shorter than the original in the fixture. Preserve the usable controls; further composite-specific states remain open. Evidence: `process-layout-probe.mjs` and `process-layout-before.log`.
|
||||
|
||||
Verification: `bulk-profile-panel-acceptance.log` passes45/45 across five profiles; after the final value-step correction, `bulk-profile-final-acceptance.log` passes25/25 including upload/value mapping/review and profile actions. Final full units1768/1768 in173files (`bulk-profile-final-unit.log`), affected units10/10, export/typecheck, changed-file lint, license headers, diff check and production bundle budget pass (1031.4KB gzip/1050KB). Capture logs: `process-states-light`, `process-states-followup`, `process-states-dark-baseline`, `process-states-dark-current`, `process-bulk-restored-light`, and `process-values-final-{light,dark}`. Canonical manifests supersede failed interim captures and orphaned images.
|
||||
|
||||
|
||||
## September 29 — profile menus, shell overlays and Preferences
|
||||
|
||||
**VIS-015, corrected:** the expanded profile menu lacked arrow-key navigation and overflowed the tablet viewport with eight profiles (bottom1105px in a1080px viewport). It now uses the existing Radix dropdown primitive with collision placement and available-height scrolling. Existing menu contents, ownership restrictions, confirmations, status and backend behavior remain. Dialog focus returns to the actual opener, including WebKit clicks that activate without focusing; menu actions return to the persistent menu trigger. Native probes verify desktop/tablet/phone bounds and focus. Export now guards focus returning to its own trigger, fixing the native WebKit tap/focus dismissal reproduced by CI. Outside dismissal and download assertions remain.
|
||||
|
||||
The initial profile/browser follow-up produced27passes/3focus failures; actual-opener handling corrected those. A later34pass/1fail run exposed a separate Home/Enter test race. Trace shows Home at77867.082ms and Enter at77869.872ms; the resulting page had selected Review Profile8. Installed Radix roving-focus source defers focus with setTimeout. Waiting for the expected first menu item preserves and strengthens the keyboard check. `menu-home-focus-acceptance.log` passes35/35 across all profiles.
|
||||
|
||||
Added seven shell states in both themes at three sizes:84 clean captures. Inspected desktop Preferences/search results/command, phone navigation/user menu/Preferences/help/search results/command, tablet help, dark tablet Preferences and dark phone user menu/search results/command. Original navigation, help, user-menu and search geometry is retained; the command palette includes new table actions within its original shell. Preferences' default selection changes from25 to50 records through current metadata/default behavior. These states do not approve notifications, login/session, all branding variants, internal dialog scroll positions or the whole shell family.
|
||||
|
||||
**VIS-016, corrected:** Preferences used z-50 behind the phone sidebar's z-100; its controls were visually covered. It now uses `--qqq-z-overlay` and `--qqq-z-modal`. Closing returns focus to the user-menu button because the Preferences menu item unmounts on selection. The new phone hit-test assertion failed before correction; a unit focus-return assertion also failed. Both pass after correction, and fresh light/dark phone screenshots show the full dialog above the drawer without changing its internal layout.
|
||||
|
||||
Final verification: `preferences-menu-final-acceptance.log` passes50/50 across Chromium, Firefox, WebKit, mobile and tablet, including drawer navigation/focus, profile actions and Export. `preferences-final-unit.log` passes1771/1771 in173files. Final production export/typecheck, changed-file lint, license headers, diff check and bundle1031.7KB/1050KB pass. A test-only Playwright `exact` option was removed from Testing Library before the final build. Capture logs are `shell-states-{light,dark}` and `preferences-restored-{light,dark}`. No whole family is approved and the full remote gate remains incomplete.
|
||||
|
||||
|
||||
## September 29 — inline profile naming recovery
|
||||
|
||||
**VIS-017, corrected:** Save New, Save As and Rename had replaced the earlier inline Profile Name field with a centered modal. Restored the editor inside the named, bordered profile section using the original compact input and bordered Save Profile/Cancel buttons. No nested form is introduced: Enter saves the profile and stops propagation so the enclosing bulk-load process is not submitted. Escape/Cancel close the editor; saving restores focus to the actual opener or the persistent menu trigger if the opener disappears when the current profile changes. Ownership checks, backend name limits/errors, copy semantics, update confirmation and explicit delete behavior remain.
|
||||
|
||||
The inline-region and keyboard regression assertions first failed2/11, then passed11/11. Full units pass1771/1771. The final success-focus assertion also passes in the affected11tests. `profile-inline-acceptance.log` passes40/40 across all5profiles (PRC031/046/058/059/060), covering required/file/value mapping, profile CRUD, duplicate-name server validation, shared ownership, menu bounds and focus. PRC046 retained stale local-panel locators after the menu moved to a portal; those now find the portaled menu while all SQL and behavioral assertions remain. Production export, final typecheck/lint, license and bundle1031.9KB/1050KB pass. The first budget-report write was denied by the sandbox; the authorized rerun completed successfully.
|
||||
|
||||
Fresh `profile-inline-{light,dark}` captures replace the modal snapshots at desktop/tablet/phone. All12entries have clean setup/runtime status and no body horizontal overflow. Inspected light desktop, light/dark phone and dark tablet pairs: editor input/buttons match the original arrangement within the restored section. Expanded profile actions/reset controls still add height and remain part of the broader review. No full family is approved. The ledger explicitly records inline naming as Different UX; its existing-row recount corrects795total/51supplemental rows and the stale OS-driven dark-mode description.
|
||||
|
||||
|
||||
## September 29 — Copy alignment, selected sections and touch selection
|
||||
|
||||
The bounded record/form review inspected 210 existing images against original Next commit `e42ad2b2`. It found two concrete regressions: **RF-001**, Copy scope controls aligned to a wider container than the editable form; **RF-002**, a selected record section inherited Overview's half-width card and full-width field defaults. The Copy container now shares `max-w-4xl` with EntityForm. Selected tabs use a full-width fallback card and two field columns from `sm`; explicit metadata spans retain precedence. Overview and phone accordion defaults are preserved.
|
||||
|
||||
Both browser layout regressions failed on the old export, then passed 10/10 across Chromium, Firefox, WebKit, phone and tablet. Existing record view and create/edit/copy workflows passed 17/17 in Chromium. Full units 1,840/1,840, production export, types, changed-source lint, license headers and bundle budget 1,038 KB/1,050 KB pass. Independent code review found no additional issues.
|
||||
|
||||
**Touch selection:** the 44px selection label exceeded the 44px pinned selection cell because density padding was still applied. Removing horizontal selection-cell padding only for coarse pointers exposes both target edges. Browser geometry reproduced the overlap before the correction; 35 focused checks pass across all 5 profiles, covering both edges, every density and existing selection workflows. The first post-fix test attempt used coordinates from before the selection banner moved the grid; the corrected test remeasures after selection and uses native touch events for touch menus. Failed traces remain preserved.
|
||||
|
||||
Refreshed current captures for Copy, selected section, record lab, audit and populated query in both themes at all 3 sizes: 30 states/40 images, no capture/runtime errors or body overflow. This also replaces the stale light record lab/audit evidence. Root inspection confirms desktop light Copy/selectedsection and tablet touch query. All 34 refreshed record/form images have now been directly reviewed, closing RF-001, RF-002 and the stale-capture gap. The exact-path ledger is `record-form-review-d937507.md` in the ignored evidence directory. The fixed 210-image record/form inventory is closed; other visual families are still open. Logs/evidence: `record-default-layout-{red,green,build,units}.log`, `record-layout-workflows.log`, `query-selection-targets-final.log`, `layout-restored-{light,dark}.log`, and release-worktree `test-results/acceptance/{selection-target-evidence,record-default-layout-evidence}`.
|
||||
|
||||
**Publication hold:** the owner is testing immutable RC1 in real applications for a couple of weeks. These corrections are post-RC1 branch changes. Final publication requires renewed explicit owner approval; see [testing period](../releases/1.0-testing.md).
|
||||
|
||||
|
||||
## September 29 — query and shell fixed inventories reviewed
|
||||
|
||||
The query inventory is fully inspected: **348 images**, covering all 28 existing states in light/dark at desktop/tablet/phone sizes, including scroll positions. The reviewer directly inspected 314 images and retained 34 explicitly attributed root inspections. No new unresolved visual regression was found. Five original controls are unavailable for equivalent comparison; the ledger distinguishes those context images. Older selection-padding appearances are historical; refreshed populated-query images and the separate 35-case interaction gate cover the correction.
|
||||
|
||||
The seven-state shell inventory is fully inspected: **92 images**, comprising 84 initial-position images and eight Preferences scroll captures. Root directly inspected 64 images and retained 28 prior inspections. No new unresolved shell visual regression was found. All six current Preferences scroll captures expose the lower table/record layout choices, with the footer separate. Original search/help/navigation geometry is retained; added theme choices and command actions remain inside the existing layouts. Shell source has not changed between d937507 and e87fe90. Screenshots do not establish persistence for every preference.
|
||||
|
||||
Exact-path ledgers are `query-review-e87fe90.md` and `shell-review-e87fe90.md` in the ignored `test-results/visual-review` evidence directory. This closes those fixed inventories, alongside the previously closed record/form inventory. Process, report and widget review remains open; notifications, login/session and branding variants are not approved by this shell review. RC2 is in preparation and final 1.0 remains held for owner testing.
|
||||
|
||||
|
||||
## September 29 — report fixed inventory inspected
|
||||
|
||||
All **262 report images** in the six per-profile canonical manifests were directly inspected: 131 in each theme (desktop 40, tablet 41, phone 50). This corrects the older combined light manifest, which omitted later states. The exact-path ledger `report-review-defb3ab.md` has no missing or extra images and records no confirmed new visual regression. An initial concern about blank output-format text was withdrawn after direct focused reinspection.
|
||||
|
||||
This is inspection of the existing images, not a claim that all captures represent defb3ab: only 10 of 216 state entries have timestamps/export fingerprints. Fifty-four original editor-state entries lack an equivalent control, and the initial phone Advanced/Columns captures do not show the lower pager. Those comparison and provenance limits remain explicit. Embedded query equivalence and whole-product visual review remain open; final 1.0 is held.
|
||||
|
||||
|
||||
## September 29 — remaining fixed inventories inspected; child export corrected
|
||||
|
||||
All **1,621 unique images** in the six canonical per-profile manifests are now accounted for by completed inspection ledgers: record/forms 210, query 348, shell 92, process 402, report 262, widgets/dashboard 269 and developer/miscellaneous 38. Process review found no additional confirmed regression; explicit full-width upload metadata explains the wide upload control. Developer/miscellaneous content layout retains the original composition, with historical header truncation already addressed by VIS-007. Widget/dashboard review found the child export issue below and no other confirmed unresolved regression. Root independently reconciled the final 269 and 38 paths against the manifests and verified every file exists. Exact ledgers are `process-review-e87fe90.md`, `widgets-dashboard-review-2221999.md` and `developer-misc-review-2221999.md` in ignored evidence.
|
||||
|
||||
**Child-record export, corrected:** current child lists had a generic header icon unable to read the child payload plus a working Export control in a new toolbar row. Two component regressions and the real-backend fixture reproduced the duplicate. The first correction removed the generic icon but retained the extra row; fresh screenshots showed that still differed from original Next. The final correction reuses the child export control in the existing header position. Bare hosts retain inline export. Permission-filtered join columns, shown-row CSV, empty disabled state and the partial-result tooltip remain. Widget export controls follow the same loading/error/permission/required-selection gates as the body. The existing error boundary now covers header and body; Retry requests fresh data and a replacement payload clears the error. Review regressions reproduced and verified denied exports, malformed metadata containment and successful recovery.
|
||||
|
||||
Verification: **1,857 unit tests**, 41 focused widget tests, 10 real-backend child-list scenarios across five profiles, production export/types/lint/license/diff checks and 1038.4KB/1050KB bundle budget pass. Final browser checks verify one header export aligned with Reload, CSV contents against independent SQL, exclusion of the undisplayed third row, record navigation and empty lists. Desktop and phone captures were directly inspected: the extra toolbar row is gone. The first geometry check compared title and button centers and failed on touch because native targets are 44px and the header uses items-start; the final assertion requires Export inside the header and aligned with Reload. Failed evidence is retained; no CSS or target size was changed to pass it. Independent review has no remaining blocker. Logs use `child-export-*`; final evidence is `child-export-final-evidence` in the ignored root directory.
|
||||
|
||||
Inventory completion is **not** whole-product visual approval. Fifty-two gallery/table images repeat near-top content and cannot establish lower-page coverage. Six original choice-open states are unsupported. Most historical images lack build fingerprints; dark widget coverage only includes sample/typed-input/choice states, and developer/miscellaneous has no dark captures. Previously recorded uncaptured shell variants, report pager and provenance limitations remain. The original Next reference remains `e42ad2b2`; Material supplies behavior only. The owner preview remains unchanged while the owner validates it. Final 1.0 is explicitly held.
|
||||
|
||||
|
||||
## September 30: unpublished legacy icon compatibility follow-up
|
||||
|
||||
Existing Next Lucide mappings, widget overrides and image priority are preserved. Additional legacy names use the pinned local Material Icons inventory. Six formerly unmapped block icons, navigation and the raised main tile were checked in light/dark desktop/mobile views. Four workflows passed against the actual development JAR; its font and exported files were verified byte-for-byte. Saved desktop/mobile tile regions contain identical light/dark glyph pixels; the earlier suspected blank tile was an inspection error, and no repaint workaround was introduced.
|
||||
|
||||
The full local Chromium suite passed 589 tests and all 391 required rows, with zero failures, skips, flaky outcomes or report errors and three approved external-service exclusions. Independent code review approved the change. Evidence is retained under `test-results/visual-review/icon-compatibility-investigation/` in the original checkout. This checkpoint covers the icon change; whole-product visual approval remains open. RC5 is immutable and does not include this follow-up.
|
||||
|
||||
|
||||
## September 30: fresh RC6 dark desktop developer and miscellaneous review
|
||||
|
||||
Twelve original/current captures across app home, table developer, record developer and not-found pages were directly reviewed, including lower scroll positions. The fresh manifest records capture times and export fingerprints. No new layout regression was found in those states. Evidence: `test-results/visual-review/rc6-dark-developer-review/review.md` in the original checkout.
|
||||
|
||||
An inherited RapiDoc dark-link contrast defect is confirmed at 2.12:1 in both original and RC6; subsequent browser checks also find light-mode contact/authentication text below 4.5:1. A separate correction is being tested. This closes the missing desktop dark capture evidence for the four entry states; phone/tablet dark coverage, expanded endpoint interactions and whole-product approval remain open.
|
||||
|
||||
|
||||
## September 30 — phone developer view comparison
|
||||
|
||||
Eight fresh original/RC6 phone screenshots in light/dark were directly inspected (initial and lower scroll positions). Four capture entries had no runtime/state errors or body horizontal overflow. Evidence: `test-results/visual-review/rc6-phone-developer-review/` in the original checkout, including export fingerprints and provenance.
|
||||
|
||||
The main composition matches the original. RapiDoc's inner clipping is inherited: its 360px minimum exceeds the 343px phone panel, and hidden overflow clips controls. This remains open for a separate responsive correction. The added command button also reduces breadcrumb space; the trail scrolls to the current page, but header crowding remains a visual-review consideration. This is scoped evidence, not full visual approval.
|
||||
|
||||
## September 30 — API playground contrast follow-up
|
||||
|
||||
Original Next and RC6 both have an inherited dark playground link contrast defect (2.12:1). Focused browser checks also reproduced light contact/status contrast defects. An isolated follow-up applies existing theme tokens without changing layout. Four final desktop/mobile light/dark cases pass, including hovered controls and Preferences changes, with no failures, skips, flaky outcomes or report errors. All four final screenshots were directly inspected after transitions settled. Evidence is retained under `test-results/visual-review/api-playground-contrast/` in the original checkout.
|
||||
|
||||
This is scoped color verification, not whole-product visual approval. Narrow-screen inner clipping, expanded endpoint states and additional application palettes remain open. RC6 is unchanged.
|
||||
|
||||
## September 30 — API specification controls in narrow panels
|
||||
|
||||
Reproduced clipped specification buttons against the packaged RC7 candidate before any focus/click could conceal overflow by scrolling hidden ancestors. The follow-up lets RapiDoc fit its panel and uses a named overview container plus public CSS parts to wrap only the specification actions at narrow widths. It adds no dependency and does not mutate the shadow DOM.
|
||||
|
||||
Twelve real-backend desktop/mobile workflows pass: default device and 320px widths, light/dark themes, complete visibility, actual specification download, keyboard new-tab action, contrast and live Preferences changes. Final phone screenshots were directly inspected. At 1440px, the full playground screenshot is byte-identical to RC7 (SHA256 `ec6d108d5d632d1ee0b3a3ecd6c61ae9279bee705facd2a3b9e422d2e0f1ae37`), and overview/button geometry matches exactly.
|
||||
|
||||
All 1,904 units, types, lint/license, default production export and unchanged bundle budgets pass. Evidence, including earlier test/setup failures, is retained under original-checkout `test-results/visual-review/api-playground-width/`. This is a separate follow-up, not part of frozen RC7. Expanded endpoint states, additional application palettes and whole-product visual approval remain open.
|
||||
|
||||
|
||||
## September 30 — desktop and tablet process comparison
|
||||
|
||||
Compared original Next `e42ad2b2` with the actual RC8 candidate JAR in 32 light/dark desktop/tablet screenshots: bulk file mapping at two scroll positions, running process progress, and expanded internal-error details. Both builds used fresh isolated instances of the same fixture/backend. Capture manifests include export fingerprints and report zero runtime/state errors or body/main horizontal overflow.
|
||||
|
||||
Progress and error panel composition are retained. Bulk mapping visibly changes the saved-profile actions and Add Fields control and introduces explanatory text. Main scroll height increases from 1330 to 1390px on desktop and 1556 to 1748px on tablet. This added density/height remains an owner-review item. Tablet breadcrumb ancestors are more crowded with the added command control; the current page remains visible. Dark mode retains the existing composition with revised readable accent colors. No screen redesign was applied during this review.
|
||||
|
||||
Evidence: original-checkout `test-results/visual-review/rc8-desktop-tablet-process-review/`, including four inspected contact sheets and all 32 source images. This is scoped process evidence, not whole-product approval; the existing uncaptured states and compatibility gaps remain open.
|
||||
|
||||
|
||||
## RC8 lower widget review: desktop, tablet and phone
|
||||
|
||||
Original Next `e42ad2b2` and corrected RC8 `cd3d473` were compared on Widget Gallery and Widget Tables And Charts using the actual candidate JAR, matching fixtures and backend `e0d57dd31`. Evidence: `rc8-widget-lower-review/` in the visual-review archive. Desktop supplied 28 images; tablet/phone supplied 86 more, for 114 images across 24 entries in both themes. Twelve contact sheets were directly inspected. Every lower capture records and verifies its actual scroll position. All entries had zero runtime/state errors and no body/main horizontal overflow. This closes the earlier repeated-near-top capture gap for these two entry states across all three viewport sizes.
|
||||
|
||||
The card structure is retained. Recorded changes include map geography, formatted table cells, per-table export/footers, paging controls, metadata chart colors, readable long category labels, legends, connected line data and As Of timestamps. These additions increase page height, including an extra phone viewport on Tables And Charts. Long phone header text now truncates alongside the added command control. Dark map/plot contrast remains a review concern; this comparison did not measure contrast ratios or verify horizontally scrolled table columns. Expanded interaction states, other widget surfaces and owner visual approval remain open. No product code changed during this review.
|
||||
@@ -0,0 +1,88 @@
|
||||
# Performance budgets (QRun-IO/qqq#710)
|
||||
|
||||
Two automated checks. A breach fails CI. To fix one, make the change smaller or faster. If a
|
||||
budget really has to rise, put the new measurement and the reason in the commit.
|
||||
|
||||
| Check | What it measures | Budget file | Runs in |
|
||||
|---|---|---|---|
|
||||
| Bundle budget | First-load JS per route of the static export, total JS, largest chunk, CSS | `perf-budget.json` | `ci.yml` job "Bundle budget (static export)": `pnpm build:export && pnpm perf:budget` |
|
||||
| Runtime budgets | Large table, heavy dashboard, long process in the production export against the real sample backend | `tests/acceptance/specs/performance/budgets.ts` | `acceptance.yml`: matrix rows PRF-001 to PRF-005 in every browser |
|
||||
|
||||
```bash
|
||||
pnpm build:export && pnpm perf:budget # bundle; report in test-results/performance/bundle.json
|
||||
QQQ_ACCEPTANCE_BROWSERS=chromium,firefox,webkit node scripts/acceptance.mjs --skip-build specs/performance
|
||||
```
|
||||
|
||||
## Bundle
|
||||
|
||||
First-load JS for a route is every script and stylesheet that the route's exported HTML loads
|
||||
before hydration, gzip level 9. Next 16 no longer prints these sizes, so the script measures
|
||||
them from `out/`. `_` stands for a dynamic segment: `/app/_` is `/app/[slug]` (app home, record
|
||||
query, process, report) and `/app/_/_` is the record view.
|
||||
|
||||
| Route | Before (KB gzip) | 1.0 (KB gzip) | Budget |
|
||||
|---|---|---|---|
|
||||
| `/`, `/login`, `/callback`, `/token`, `/404` | 218–219 | 218–219 | 230 |
|
||||
| `/app`, `/app/developer`, `/app/search`, `/app/_/key`, `/app/_/dev`, `/app/_/_/dev` | 309–311 | 309–311 | 330 |
|
||||
| `/app/_` (app home, record query, process, report) | **562.9** | **448.2** | 470 |
|
||||
| `/app/_/_` (record view) | 431.6 | 431.5 | 455 |
|
||||
| `/app/_/create`, `/app/_/_/edit`, `/app/_/_/copy` | 351–356 | 351–356 | 370–375 |
|
||||
| `/app/_/savedView/_` | 371.2 | 371.1 | 390 |
|
||||
| Total JS (all chunks) | 689.2 in 54 chunks | 675.0 in 51 chunks | 710 |
|
||||
| Largest chunk (Recharts + d3, loaded only by chart widgets) | 93.8 | 93.8 | 100 |
|
||||
| CSS | 16 | 16 | 18 |
|
||||
|
||||
Regression fixed: `/app/[slug]` imported `AppHome` from the widgets barrel, and that barrel
|
||||
re-exports the chart widgets. Recharts was therefore in the first load of every table and
|
||||
process page, even though `WidgetRenderer` loads charts lazily. The page now imports its
|
||||
components by module path.
|
||||
|
||||
## Runtime
|
||||
|
||||
Fixture: `tests/acceptance/fixture/PerformanceFixtures.java`, all synthetic data.
|
||||
|
||||
- `prfWide`: 10,000 rows, 40 columns of mixed types, read-only.
|
||||
- `prfDashboard`: 24 widgets: 6 statistics, 6 bar, 4 line (60 points) and 4 pie charts, and
|
||||
4 tables of 100 rows.
|
||||
- `prfLongRun`: a job that walks all 10,000 rows in 40 pages with a 250 ms pause per page.
|
||||
|
||||
Each test measures wall time from the user action until the exact expected content is on
|
||||
screen, and records it as a `timing` annotation in `test-results/acceptance/report.json`.
|
||||
|
||||
| Row | Measurement | Budget (ms) | chromium | firefox | webkit |
|
||||
|---|---|---|---|---|---|
|
||||
| PRF-001 | Open the 10,000-row table until the first 25 rows × 40 columns show | 5,000 | 535 | 353 | 602 |
|
||||
| PRF-002 | Page size 25 → 250 (10,000 cells) | 10,000 | 462 | 665 | 738 |
|
||||
| PRF-002 | Next page at 250 rows | 10,000 | 636 | 969 | 1,188 |
|
||||
| PRF-002 | Last page (page 40) at 250 rows | 10,000 | 612 | 1,158 | 1,064 |
|
||||
| PRF-003 | Sort by Name over 10,000 rows | 3,000 | 155 | 234 | 299 |
|
||||
| PRF-003 | Filter Count 04 = 0 (40 matches) | 3,000 | 173 | 157 | 150 |
|
||||
| PRF-004 | Open the 24-widget dashboard until every widget has loaded | 14,000 | 1,086 | 1,567 | 1,760 |
|
||||
| PRF-005 | Job finished until its result screen shows (10.15 s job, 5 status polls) | 5,000 | 415 | 838 | 521 |
|
||||
|
||||
These numbers come from an otherwise idle run of the performance area on an Apple M3 Max
|
||||
(macOS, Darwin 27), Playwright 1.58.2. Each budget is about 8× the slowest browser's time.
|
||||
In a full four-project run with other acceptance suites on the same machine, the same steps
|
||||
took up to about 4× longer: Firefox next page 4,191 ms, dashboard 8,841 ms. Retries are 0,
|
||||
so a tighter budget would flake. The budgets catch order-of-magnitude regressions. The
|
||||
structural checks below catch smaller ones, and don't depend on machine load.
|
||||
|
||||
The rows also check behavior besides time:
|
||||
|
||||
- Only one page of rows is in the DOM: 25 or 250 rows × 40 cells, not the whole table.
|
||||
- The backend is asked for `skip`/`limit` 250 on each page.
|
||||
- Each dashboard widget's data is requested exactly once.
|
||||
- Status polls never overlap, and each poll waits at least 1.5 s (`POLL_INITIAL_MILLIS`)
|
||||
after the previous answer. The job never gets more polls than its length allows.
|
||||
|
||||
Grid rendering fixed for large pages: body rows are memoized. The grid used to re-render all
|
||||
10,000 cells before the page request was sent, and again for the URL sync. Date cells no
|
||||
longer build an `Intl.DateTimeFormat` per cell. A 250-row page turn in chromium, from click to
|
||||
rendered rows: 659–669 ms → 518–561 ms.
|
||||
|
||||
Observed, not changed here:
|
||||
|
||||
- Grid columns follow the field order of the metadata map, not the section order, and not
|
||||
Material's alphabetical-by-label order. For a 40-column table this looks unordered.
|
||||
- The pagination range is not locale-formatted, while the total is ("Showing 9751–10000 of
|
||||
10,000").
|
||||
@@ -0,0 +1,53 @@
|
||||
# Agent Prompt: Package 1 — Scaffold, Auth, and Layout Shell
|
||||
|
||||
## Your Role
|
||||
|
||||
You are an implementation agent. Your job is to execute the implementation plan for Work Package 1 and produce working, tested code. You do NOT make architectural decisions — the plan specifies everything. Follow it exactly.
|
||||
|
||||
## Before Writing Any Code, Read These Files
|
||||
|
||||
1. **Your work package plan:** `docs/implementation-plans/01-scaffold-and-auth.md` — This is your primary instruction set. Execute every step.
|
||||
2. **Shared context — API contract:** `docs/implementation-plans/shared-context/api-contract.md` — Every API call must match this exactly.
|
||||
3. **Shared context — Type definitions:** `docs/implementation-plans/shared-context/type-definitions.md` — Port these types into `/src/types/`.
|
||||
4. **Shared context — Coding conventions:** `docs/implementation-plans/shared-context/coding-conventions.md` — Follow these patterns.
|
||||
5. **Project overview:** `docs/implementation-plans/00-project-overview.md` — Directory structure and shared conventions.
|
||||
6. **CLAUDE.md** — Project-level conventions.
|
||||
|
||||
## Reference Code (Read for Behavior Parity)
|
||||
|
||||
Look at the sibling repo `qqq-frontend-material-dashboard` for:
|
||||
- `src/App.tsx` — Route generation from appTree, auth dispatch, layout structure
|
||||
- `src/qqq/components/horseshoe/` — SideNav, NavBar, Breadcrumbs, Footer
|
||||
- `src/qqq/components/misc/Banners.tsx` — Banner rendering
|
||||
|
||||
Also look at `qqq-frontend-core` for:
|
||||
- `src/controllers/QController.ts` — Auth methods (getAuthenticationMetaData, manageSession)
|
||||
- `src/model/**/*.ts` — All TypeScript model types to port
|
||||
|
||||
## What You Must Deliver
|
||||
|
||||
Execute Steps 1-11 from the work package plan. After completion:
|
||||
|
||||
1. `pnpm dev` runs without errors
|
||||
2. `pnpm build` succeeds
|
||||
3. `pnpm tsc --noEmit` — zero errors
|
||||
4. `pnpm test` — all unit tests pass
|
||||
5. Sidebar renders navigation from metadata (use mock data if no backend)
|
||||
6. Auth flow initializes (AUTH_0, OAUTH2, FULLY_ANONYMOUS)
|
||||
7. All placeholder pages exist and render within the dashboard layout
|
||||
8. Theme system injects CSS custom properties
|
||||
9. 401 interceptor triggers redirect
|
||||
10. Breadcrumbs render from pathname
|
||||
|
||||
## Verification
|
||||
|
||||
After completing all steps, check every acceptance criterion in Section 9 of the work package plan. Every criterion must pass before this package is done.
|
||||
|
||||
## Important Rules
|
||||
|
||||
- Use `pnpm` (not npm or yarn)
|
||||
- TypeScript strict mode — no `any` in component props
|
||||
- Every interactive element gets a `data-qqq-id` attribute
|
||||
- All API calls go through `src/lib/api/` — never raw fetch
|
||||
- Follow the import order convention from CLAUDE.md
|
||||
- Commit after each major step so progress isn't lost
|
||||
@@ -0,0 +1,72 @@
|
||||
# Agent Prompt: Package 2 — Record Query Page
|
||||
|
||||
## Your Role
|
||||
|
||||
You are an implementation agent. Execute the implementation plan for Work Package 2 and produce working, tested code. Follow the plan exactly — do not make architectural decisions.
|
||||
|
||||
## Before Writing Any Code, Read These Files
|
||||
|
||||
1. **Your plan:** `docs/implementation-plans/02-record-query.md`
|
||||
2. **API contract:** `docs/implementation-plans/shared-context/api-contract.md`
|
||||
3. **Type definitions:** `docs/implementation-plans/shared-context/type-definitions.md`
|
||||
4. **Coding conventions:** `docs/implementation-plans/shared-context/coding-conventions.md`
|
||||
5. **Project overview:** `docs/implementation-plans/00-project-overview.md`
|
||||
6. **CLAUDE.md**
|
||||
|
||||
## Reference Code (Read for Behavior Parity)
|
||||
|
||||
From `qqq-frontend-material-dashboard`:
|
||||
- `src/qqq/pages/records/query/RecordQuery.tsx` — THE most complex page. Read the whole thing. Note the 260+ state variables, the page state machine, query deduplication via latestQueryId, localStorage persistence.
|
||||
- `src/qqq/components/query/FilterCriteriaRow.tsx` — Per-row filter UI with operator selection per field type
|
||||
- `src/qqq/components/query/BasicAndAdvancedQueryControls.tsx` — Basic/advanced filter toggle
|
||||
- `src/qqq/components/query/CustomFilterPanel.tsx` — Full filter builder
|
||||
- `src/qqq/components/query/CustomColumnsPanel.tsx` — Column show/hide/reorder
|
||||
- `src/qqq/components/misc/SavedViews.tsx` — Saved view CRUD
|
||||
- `src/qqq/components/query/ExportMenuItem.tsx` — Export to CSV
|
||||
|
||||
From `qqq-frontend-core`:
|
||||
- `src/controllers/QControllerV1.ts` — `query()` and `count()` methods
|
||||
- `src/model/query/QQueryFilter.ts` — Filter structure
|
||||
- `src/model/query/QCriteriaOperator.ts` — All 20 operators
|
||||
|
||||
## What Package 1 Already Provides (Import From It)
|
||||
|
||||
- Types: `import type { QTableMetaData, QFieldMetaData, QRecord, QQueryFilter, QFilterCriteria, QCriteriaOperator } from '@/types'`
|
||||
- API client: `import apiClient from '@/lib/api/client'`
|
||||
- Query keys: `import { queryKeys } from '@/lib/query-client'`
|
||||
- Metadata: `import { loadTableMetaData } from '@/lib/api/metadata'`
|
||||
- Context: `import { useQContext } from '@/lib/context/q-context'`
|
||||
- Layout components are in place — your page renders inside the dashboard layout
|
||||
|
||||
## What You Must Build
|
||||
|
||||
Replace the placeholder at `src/app/(dashboard)/app/[tableName]/page.tsx` with a full Record Query page featuring:
|
||||
- TanStack Table v8 data grid with dynamic columns from QTableMetaData.fields
|
||||
- Server-side pagination (offset/limit via POST /table/{t}/query + /count)
|
||||
- Cell renderers per QFieldType (STRING, INTEGER, DECIMAL, BOOLEAN, DATE, DATE_TIME, etc.)
|
||||
- Cell renderers per AdornmentType (LINK, CHIP, FILE_DOWNLOAD, etc.)
|
||||
- Basic filter (quick text search) and advanced filter builder (all 20 operators, AND/OR, sub-filters)
|
||||
- Column config (show/hide, reorder, persist to localStorage)
|
||||
- Row selection (checkbox column, single and multi)
|
||||
- Export to CSV
|
||||
- Saved views (save/load filter + column configs)
|
||||
- Pagination with page size selector (10/25/50/100)
|
||||
- Table variant selection
|
||||
- Toolbar (create button, process launcher, column config, density, saved views)
|
||||
- Row click → navigate to record view
|
||||
|
||||
## Verification
|
||||
|
||||
After each step: `pnpm tsc --noEmit`
|
||||
When done: verify ALL acceptance criteria in Section 9 of the plan.
|
||||
Final check: `pnpm build && pnpm test`
|
||||
Commit your work.
|
||||
|
||||
## Important Rules
|
||||
|
||||
- All API calls through `src/lib/api/` — create `src/lib/api/tables.ts` for query/count functions
|
||||
- Use TanStack Table v8 (NOT MUI DataGrid)
|
||||
- Use TanStack Query v5 for data fetching
|
||||
- Every component gets `data-qqq-id` attribute
|
||||
- No hardcoded column names — everything from metadata
|
||||
- TypeScript strict — no `any` in props
|
||||
@@ -0,0 +1,66 @@
|
||||
# Agent Prompt: Package 3 — Record View and CRUD
|
||||
|
||||
## Your Role
|
||||
|
||||
You are an implementation agent. Execute Work Package 3. Follow the plan exactly.
|
||||
|
||||
## Before Writing Any Code, Read These Files
|
||||
|
||||
1. **Your plan:** `docs/implementation-plans/03-record-view-and-crud.md`
|
||||
2. **API contract:** `docs/implementation-plans/shared-context/api-contract.md`
|
||||
3. **Type definitions:** `docs/implementation-plans/shared-context/type-definitions.md`
|
||||
4. **Coding conventions:** `docs/implementation-plans/shared-context/coding-conventions.md`
|
||||
5. **Project overview:** `docs/implementation-plans/00-project-overview.md`
|
||||
6. **CLAUDE.md**
|
||||
|
||||
## Reference Code (Read for Behavior Parity)
|
||||
|
||||
From `qqq-frontend-material-dashboard`:
|
||||
- `src/qqq/pages/records/view/RecordView.tsx` — Section rendering, T1/non-T1, widget loading, permission-gated action menu, keyboard shortcuts (n/e/c/d/a)
|
||||
- `src/qqq/components/forms/EntityForm.tsx` — Form orchestrator with section-based layout
|
||||
- `src/qqq/components/forms/DynamicForm.tsx` — Formik-based form with dynamic fields
|
||||
- `src/qqq/components/forms/DynamicFormField.tsx` — Per-field renderer by type
|
||||
- `src/qqq/components/forms/DynamicSelect.tsx` — Possible values autocomplete
|
||||
- `src/qqq/components/forms/BooleanFieldSwitch.tsx` — Boolean null/false/true
|
||||
- `src/qqq/components/forms/FileInputField.tsx` — File upload
|
||||
|
||||
From `qqq-frontend-core`:
|
||||
- `src/controllers/QController.ts` — `get()`, `create()`, `update()`, `delete()` methods
|
||||
- `src/model/metaData/QFieldMetaData.ts` — Field type, adornments, constraints
|
||||
|
||||
## What Package 1 Already Provides (Import From It)
|
||||
|
||||
- Types: `import type { QTableMetaData, QFieldMetaData, QRecord, QPossibleValue, QFieldType } from '@/types'`
|
||||
- API client: `import apiClient from '@/lib/api/client'`
|
||||
- Metadata: `import { loadTableMetaData } from '@/lib/api/metadata'`
|
||||
- Context: `import { useQContext } from '@/lib/context/q-context'`
|
||||
|
||||
## What You Must Build
|
||||
|
||||
1. **Record View page** at `src/app/(dashboard)/app/[tableName]/[recordId]/page.tsx`
|
||||
- Section-organized field display (T1 section at top, others below)
|
||||
- Field value rendering for ALL 12 QFieldTypes
|
||||
- All adornment types (LINK, CHIP, SIZE, FILE_DOWNLOAD, TOOLTIP, etc.)
|
||||
- Action menu (edit, delete, copy, launch process)
|
||||
- Associated records display
|
||||
- Keyboard shortcuts (n=new, e=edit, c=copy, d=delete)
|
||||
|
||||
2. **Entity Form** at `src/components/forms/EntityForm.tsx`
|
||||
- Dynamic form generation from QFieldMetaData
|
||||
- React Hook Form + Zod validation (schemas from metadata: required, maxLength, type)
|
||||
- All field types: text, number, boolean, date, time, datetime, select, file, HTML, password
|
||||
- Possible values autocomplete with debounced search (300ms)
|
||||
- Section-based layout with gridColumns
|
||||
- File upload with preview
|
||||
|
||||
3. **Create/Edit/Copy pages** using EntityForm
|
||||
4. **Delete** with confirmation dialog
|
||||
5. **API functions** in `src/lib/api/tables.ts`: getRecord, insertRecord, updateRecord, deleteRecord
|
||||
6. **Possible values** in `src/lib/api/possible-values.ts`: fetchPossibleValues
|
||||
|
||||
## Verification
|
||||
|
||||
After each step: `pnpm tsc --noEmit`
|
||||
When done: verify ALL acceptance criteria.
|
||||
Final: `pnpm build && pnpm test`
|
||||
Commit your work.
|
||||
@@ -0,0 +1,70 @@
|
||||
# Agent Prompt: Package 4 — Process Execution
|
||||
|
||||
## Your Role
|
||||
|
||||
You are an implementation agent. Execute Work Package 4. Follow the plan exactly.
|
||||
|
||||
## Before Writing Any Code, Read These Files
|
||||
|
||||
1. **Your plan:** `docs/implementation-plans/04-process-execution.md`
|
||||
2. **API contract:** `docs/implementation-plans/shared-context/api-contract.md`
|
||||
3. **Type definitions:** `docs/implementation-plans/shared-context/type-definitions.md`
|
||||
4. **Coding conventions:** `docs/implementation-plans/shared-context/coding-conventions.md`
|
||||
5. **CLAUDE.md**
|
||||
|
||||
## Reference Code (Read for Behavior Parity)
|
||||
|
||||
From `qqq-frontend-material-dashboard`:
|
||||
- `src/qqq/pages/processes/ProcessRun.tsx` — THE process page. Read the whole thing. Note: step state machine, 14 component types, async job polling with exponential backoff (1.5s initial, 1.5x multiplier, 12s max), file upload, record selection modes
|
||||
- `src/qqq/components/processes/` — All process components
|
||||
- `src/qqq/components/processes/ProcessViewForm.tsx` — View-only form steps
|
||||
- `src/qqq/components/processes/ProcessSummaryResults.tsx` — Results display
|
||||
- `src/qqq/components/processes/ValidationReview.tsx` — Pre-commit validation
|
||||
- `src/qqq/components/processes/BulkLoadFileMappingForm.tsx` — Column-to-field mapping
|
||||
- `src/qqq/components/processes/BulkLoadValueMappingForm.tsx` — Value mapping
|
||||
- `src/qqq/components/processes/SavedBulkLoadProfiles.tsx` — Profile save/load
|
||||
|
||||
From `qqq-frontend-core`:
|
||||
- `src/controllers/QController.ts` — processInit, processStep, processJobStatus, processRecords, processCancel
|
||||
- `src/model/processes/` — QJobStarted, QJobRunning, QJobComplete, QJobError, ProcessMetaDataAdjustment
|
||||
|
||||
## What Previous Packages Provide (Import From Them)
|
||||
|
||||
From Package 1: types, API client, context, layout
|
||||
From Package 3 (CRITICAL — reuse these):
|
||||
- `import { EntityForm } from '@/components/forms/EntityForm'` — For form steps
|
||||
- `import { DynamicFormField } from '@/components/forms/DynamicFormField'` — For individual fields
|
||||
- `import { fetchPossibleValues } from '@/lib/api/possible-values'` — For select fields in process forms
|
||||
- Form validation patterns from Package 3
|
||||
|
||||
## What You Must Build
|
||||
|
||||
1. **Process Run page** at `src/app/(dashboard)/app/[processName]/page.tsx`
|
||||
- Step wizard with stepper UI showing progress
|
||||
- Dynamic step rendering from QFrontendStepMetaData.components
|
||||
|
||||
2. **All 14 component types:** HELP_TEXT, BULK_EDIT_FORM, BULK_LOAD_FILE_MAPPING_FORM, BULK_LOAD_VALUE_MAPPING_FORM, BULK_LOAD_PROFILE_FORM, VALIDATION_REVIEW_SCREEN, EDIT_FORM, VIEW_FORM, DOWNLOAD_FORM, RECORD_LIST, PROCESS_SUMMARY_RESULTS, GOOGLE_DRIVE_SELECT_FOLDER, WIDGET, HTML
|
||||
|
||||
3. **Async job handling:**
|
||||
- Detect QJobStarted → start polling GET /status/{jobUUID}
|
||||
- Exponential backoff: 1.5s initial, 1.5x multiplier, 12s max cap
|
||||
- Display QJobRunning progress (message, current/total)
|
||||
- Handle QJobComplete (advance to next step or show results)
|
||||
- Handle QJobError (display error, allow retry)
|
||||
|
||||
4. **Bulk load workflows:** file upload, column-to-field mapping, value mapping, profile save/load
|
||||
|
||||
5. **Process records** endpoint for paginated result viewing
|
||||
|
||||
6. **Process cancellation**
|
||||
|
||||
7. **Both launch modes:** standalone (/app/{processName}) and table-scoped (with recordIds or filterJSON)
|
||||
|
||||
8. **API functions** in `src/lib/api/processes.ts`: processInit, processStep, processStatus, processRecords, processCancel
|
||||
|
||||
## Verification
|
||||
|
||||
After each step: `pnpm tsc --noEmit`
|
||||
When done: verify ALL acceptance criteria.
|
||||
Final: `pnpm build && pnpm test`
|
||||
Commit your work.
|
||||
@@ -0,0 +1,68 @@
|
||||
# Agent Prompt: Package 5 — Dashboard and Widgets
|
||||
|
||||
## Your Role
|
||||
|
||||
You are an implementation agent. Execute Work Package 5. Follow the plan exactly.
|
||||
|
||||
## Before Writing Any Code, Read These Files
|
||||
|
||||
1. **Your plan:** `docs/implementation-plans/05-dashboard-and-widgets.md`
|
||||
2. **API contract:** `docs/implementation-plans/shared-context/api-contract.md`
|
||||
3. **Type definitions:** `docs/implementation-plans/shared-context/type-definitions.md`
|
||||
4. **Coding conventions:** `docs/implementation-plans/shared-context/coding-conventions.md`
|
||||
5. **CLAUDE.md**
|
||||
|
||||
## Reference Code (Read for Behavior Parity)
|
||||
|
||||
From `qqq-frontend-material-dashboard`:
|
||||
- `src/qqq/components/widgets/Widget.tsx` — Widget container (label, dropdown, reload, export, help)
|
||||
- `src/qqq/components/widgets/DashboardWidgets.tsx` — Widget grid orchestrator
|
||||
- `src/qqq/components/widgets/CompositeWidget.tsx` — Multi-widget container
|
||||
- `src/qqq/components/widgets/ParentWidget.tsx` — Parent managing children
|
||||
- `src/qqq/components/widgets/RecordGridWidget.tsx` — Embedded DataGrid with CRUD
|
||||
- `src/qqq/components/widgets/charts/` — All chart components
|
||||
- `src/qqq/components/widgets/blocks/` — All block renderers
|
||||
- `src/qqq/pages/apps/AppHome.tsx` — App home page with dashboard layout
|
||||
|
||||
## What Previous Packages Provide (Import From Them)
|
||||
|
||||
From Package 1: types, API client, context, layout
|
||||
From Package 2 (reuse):
|
||||
- `import { DataGrid } from '@/components/query/DataGrid'` — For RecordGridWidget
|
||||
- Cell renderers for field value display in widgets
|
||||
From Package 3 (reuse):
|
||||
- `import { EntityForm } from '@/components/forms/EntityForm'` — For DynamicFormWidget
|
||||
- `import { fetchPossibleValues } from '@/lib/api/possible-values'` — For widget dropdown options
|
||||
|
||||
## What You Must Build
|
||||
|
||||
1. **App Home page** at `src/app/(dashboard)/app/[appName]/page.tsx`
|
||||
- Dashboard layout with widget grid
|
||||
- Widgets loaded from QAppMetaData.widgets
|
||||
|
||||
2. **Widget container** — label, dropdown selectors, reload, export, help
|
||||
- Dropdown changes trigger cancel-and-replace data fetch (AbortController)
|
||||
|
||||
3. **DashboardWidgets orchestrator** — loads multiple widgets, manages lifecycle
|
||||
|
||||
4. **Charts (Recharts):** line, bar, horizontal bar, stacked bar, pie/doughnut
|
||||
- Use ResponsiveContainer for all charts
|
||||
|
||||
5. **Statistics:** MiniStatisticsCard, MultiStatisticsCard, StatisticsCard
|
||||
|
||||
6. **All block types:** TextBlock, BigNumberBlock, UpOrDownNumberBlock, ProgressBarBlock, ButtonBlock, IconBlock, ImageBlock, AudioBlock, DividerBlock, InputFieldBlock
|
||||
|
||||
7. **RecordGridWidget** — embedded DataGrid (reuse from P2) with add/edit/delete
|
||||
|
||||
8. **Composite/Parent widgets**
|
||||
|
||||
9. **Specialized widgets:** PivotTableSetup, FilterAndColumnsSetup, StepperCard, CronUI, ScriptViewer, CustomComponent mount point
|
||||
|
||||
10. **API function** in `src/lib/api/widgets.ts`: fetchWidgetData with AbortController cancel-and-replace
|
||||
|
||||
## Verification
|
||||
|
||||
After each step: `pnpm tsc --noEmit`
|
||||
When done: verify ALL acceptance criteria.
|
||||
Final: `pnpm build && pnpm test`
|
||||
Commit your work.
|
||||
@@ -0,0 +1,105 @@
|
||||
# Agent Prompt: Package 6 — Polish, Parity, and Production Readiness
|
||||
|
||||
## Your Role
|
||||
|
||||
You are an implementation agent. Execute Work Package 6 — the final package. ALL previous packages (1-5) are complete. Your job is polish, parity, and production readiness.
|
||||
|
||||
## Before Writing Any Code, Read These Files
|
||||
|
||||
1. **Your plan:** `docs/implementation-plans/06-polish-and-parity.md`
|
||||
2. **Coding conventions:** `docs/implementation-plans/shared-context/coding-conventions.md`
|
||||
3. **CLAUDE.md**
|
||||
4. **Requirements doc Section 5.4 (responsive), 5.6 (performance), 5.7 (DX):** `docs/qqq-admin-modernization-requirements.md`
|
||||
|
||||
## Reference Code
|
||||
|
||||
From `qqq-frontend-material-dashboard`:
|
||||
- `src/qqq/components/misc/CommandMenu.tsx` — Command palette (Cmd+K)
|
||||
- `src/qqq/components/audits/AuditBody.tsx` — Audit trail display
|
||||
- `src/qqq/pages/records/developer/RecordDeveloperView.tsx` — Developer tools
|
||||
- `src/qqq/pages/records/developer/TableDeveloperView.tsx` — Table dev view
|
||||
- `src/qqq/components/scripts/ScriptViewer.tsx` — Code editor
|
||||
|
||||
## What You Must Build
|
||||
|
||||
### Responsive Design Audit & Fixes
|
||||
- Mobile card view for Record Query (< 768px)
|
||||
- Off-canvas sidebar drawer on mobile with hamburger trigger
|
||||
- Bottom sheet filter panel on mobile
|
||||
- 44px minimum tap targets everywhere
|
||||
- Sticky headers and bottom-anchored action buttons on mobile
|
||||
- Breadcrumb truncation on mobile
|
||||
- Responsive widget grid (1/2/N columns)
|
||||
|
||||
### Accessibility (WCAG 2.1 AA)
|
||||
- axe-core integration in test suite
|
||||
- Focus management in modals (trap + restore)
|
||||
- aria-live regions for toasts and loading states
|
||||
- Color contrast audit (4.5:1 / 3:1)
|
||||
- Skip-to-content link
|
||||
- Keyboard navigation for all interactive elements
|
||||
|
||||
### Command Palette
|
||||
- Cmd+K / Ctrl+K trigger
|
||||
- Search tables, processes, reports from metadata
|
||||
- Recently accessed items
|
||||
- Keyboard navigation (arrow keys, Enter)
|
||||
|
||||
### Keyboard Shortcuts
|
||||
- Global: Cmd+K, ?, Escape
|
||||
- Record Query: n, /, f
|
||||
- Record View: n, e, c, d, a
|
||||
- Shortcuts reference panel
|
||||
|
||||
### Audit Trail
|
||||
- Audit log panel on Record View
|
||||
- Timeline with field-level changes
|
||||
- Filter by date/user/field
|
||||
|
||||
### Developer Tools
|
||||
- Table dev view: metadata inspector
|
||||
- Record dev view: raw values, scripts, logs
|
||||
- Script editor (CodeMirror or Monaco)
|
||||
- Script test execution
|
||||
|
||||
### Report Pages
|
||||
- Reuse ProcessRun from Package 4
|
||||
|
||||
### Storybook
|
||||
- Setup with all shared components
|
||||
- Stories for loading/empty/error/populated states
|
||||
- a11y addon
|
||||
|
||||
### Playwright E2E
|
||||
- Auth flow, query, CRUD, process, widgets
|
||||
- Responsive viewport tests
|
||||
- Error handling tests
|
||||
|
||||
### Performance
|
||||
- Bundle analysis (next-bundle-analyzer)
|
||||
- Lazy loading for charts, code editor, file upload
|
||||
- TanStack Query cache tuning
|
||||
- Lighthouse CI integration
|
||||
|
||||
### Theme Tokens
|
||||
- All 60+ CSS custom properties
|
||||
- Light/dark mode with localStorage persistence
|
||||
- data-qqq-id verification on ALL elements
|
||||
- customCss injection support
|
||||
|
||||
### Session Timeout UX
|
||||
- Re-auth modal (not full redirect)
|
||||
- Preserve form state across re-auth
|
||||
|
||||
## Verification
|
||||
|
||||
After each step: `pnpm tsc --noEmit`
|
||||
When done: verify ALL acceptance criteria in the plan.
|
||||
Final checks:
|
||||
```bash
|
||||
pnpm build # Must succeed
|
||||
pnpm test # All tests pass
|
||||
pnpm lint # Zero errors
|
||||
pnpm tsc --noEmit # Zero errors
|
||||
```
|
||||
Commit your work.
|
||||
@@ -0,0 +1,241 @@
|
||||
# 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.
|
||||
|
||||
> **Live roadmap and known issues:** [QRun-IO/qqq#713](https://github.com/QRun-IO/qqq/issues/713). GitHub issues own current status and remaining work. Follow the [issue-tracking policy](../ISSUE-TRACKING.md) when reporting or resolving findings; keep this guide focused on usage and evidence.
|
||||
|
||||
This is an implementation playbook for coding agents working on the QQQ Next UI. Read the [developer guide](./next-ui-developer-guide.md) for user and integrator behavior, the repository [AGENTS.md](../../AGENTS.md) and [CLAUDE.md](../../CLAUDE.md) for scoped conventions, and the [real-server feature matrix](../acceptance/feature-matrix.md) plus [Material parity ledger](../acceptance/material-parity.md) before claiming compatibility. This guide reflects the `feature/next-1.0` release worktree as of 2026-10-01; the previously verified public candidate at the RC13 preparation checkpoint was [1.0.0-RC.12](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.12) at `e333223`, and final `1.0.0` remains on hold. Later branch changes must not be attributed to that immutable release. Some ledger entries predate integrated changes, so reconcile them with source and real-server evidence. A green subset of tests is evidence for that subset, not proof of complete Material parity or a published 1.0 release.
|
||||
|
||||
## Operating rule: metadata is the product boundary
|
||||
|
||||
QQQ's Java backend owns tables, fields, processes, apps, navigation, widgets, permissions, and human labels. The browser adapts to the metadata of the current user. Never make a QQQ application work by hardcoding its table names, field names, routes, or widget data in a React component. Add general rendering behavior and a real backend fixture, or change the backend metadata producer where the contract originates. Do not invent JSON keys because a frontend type accepts `Record<string, unknown>`; inspect the actual producer, versioned API response, and acceptance fixture.
|
||||
|
||||
The UI supports a **versioned** API at `/qqq/v1`. The acceptance diagnostics reject browser calls to legacy unversioned paths. All browser requests should use typed functions in [`src/lib/api`](../../src/lib/api); TanStack Query keys come from [`src/lib/query-client.ts`](../../src/lib/query-client.ts). Treat permission flags as display controls only; the backend must refuse the same forbidden operation.
|
||||
|
||||
## Find the right source before editing
|
||||
|
||||
| Need | Start here | Why |
|
||||
|---|---|
|
||||
| Instance, app, table, field, process, report, widget shapes | [`src/types/metadata.ts`](../../src/types/metadata.ts), [`src/types/enums.ts`](../../src/types/enums.ts) | Frontend contract and enumerated types. |
|
||||
| Backend HTTP contract | [`src/lib/api`](../../src/lib/api), [`src/lib/api/client.ts`](../../src/lib/api/client.ts) | Versioned routes, typed request/response handling, 401 behavior. |
|
||||
| Route and sidebar dispatch | [`src/app/(dashboard)/app/[slug]/page.tsx`](../../src/app/%28dashboard%29/app/%5Bslug%5D/page.tsx), [`use-routes.ts`](../../src/lib/hooks/use-routes.ts), [`use-route-params.ts`](../../src/lib/hooks/use-route-params.ts), [`legacy-paths.ts`](../../src/lib/utils/legacy-paths.ts) | Slug precedence, hidden entries, nested nav, static-export and Material deep links. |
|
||||
| Query and saved views | [`src/components/query`](../../src/components/query), [`use-record-query.ts`](../../src/lib/hooks/use-record-query.ts), [`use-saved-views.ts`](../../src/lib/hooks/use-saved-views.ts) | URL state, filters, variant, selection, backend saved views. |
|
||||
| Record display and forms | [`src/components/records`](../../src/components/records), [`src/components/forms`](../../src/components/forms), [`FormWidgetSection.tsx`](../../src/components/forms/FormWidgetSection.tsx), [`zod-from-metadata.ts`](../../src/lib/utils/zod-from-metadata.ts) | Sections, fields, form widget context, validation, adornments. |
|
||||
| Processes and reports | [`src/components/process`](../../src/components/process), [`ReportRun.tsx`](../../src/components/reports/ReportRun.tsx), [`src/lib/api/processes.ts`](../../src/lib/api/processes.ts) | Step components, state transitions, polling, files. |
|
||||
| Widget entry and dispatch | [`ConnectedWidget.tsx`](../../src/components/widgets/ConnectedWidget.tsx), [`WidgetBlock.tsx`](../../src/components/widgets/WidgetBlock.tsx), [`WidgetRenderer.tsx`](../../src/components/widgets/WidgetRenderer.tsx) | Fetch/params, chrome, data renderer. |
|
||||
| Widget payload details | [`src/components/widgets`](../../src/components/widgets), [`widget-types.ts`](../../src/components/widgets/widget-types.ts), [`src/mocks/fixtures/widgets`](../../src/mocks/fixtures/widgets) | Concrete canonical and demo shapes; demo fixtures are not production guarantees. |
|
||||
| Branding, analytics and CSS | [`material-theme.ts`](../../src/lib/theme/material-theme.ts), [`src/lib/theme`](../../src/lib/theme), [`src/lib/analytics`](../../src/lib/analytics), [`src/styles`](../../src/styles) | Supplemental Material theme, implemented tokens, analytics privacy and `data-qqq-id` customization hooks. |
|
||||
| Owned backend acceptance | [`tests/acceptance`](../../tests/acceptance), [`tests/acceptance/README.md`](../../tests/acceptance/README.md) | QQQ sample fixtures, browser projects, SQL assertions, diagnostics gate. |
|
||||
| Current gaps and risk | [Material parity ledger](../acceptance/material-parity.md), [security review](../security/next-ui-review.md), [browser matrix](../acceptance/browser-matrix.md) | What has actually been verified and what remains open. |
|
||||
|
||||
The repo's [implementation plans](../implementation-plans) explain intended architecture, but some examples predate the current code. Prefer source and executable acceptance evidence. In particular, current `package.json` uses Next.js 16 even though older `CLAUDE.md` and plans mention 15.
|
||||
|
||||
## Preserve the Next visual baseline
|
||||
|
||||
Use the September 25 Next design (`e42ad2b2`) as the visual reference. Use Material source to establish functional behavior and backend contracts. Changes to parity behavior must preserve the Next layout unless the user requests a visual change.
|
||||
|
||||
- Desktop query mode controls, sort and chips belong inside the Filter panel. On phones, Filter opens a sheet containing those same controls and preserves the mode; Advanced shows the nested builder inside it. Check [`RecordQuery.tsx`](../../src/components/query/RecordQuery.tsx).
|
||||
- Query column widths default to 150 px. Preserve `SIZE` adornment widths and saved/user-resized widths.
|
||||
- The record section sidebar requires literal `showRecordSidebar: true` in `supplementalMetaData.materialDashboard` (legacy fallback: `supplementalTableMetaData.materialDashboard`). An absent setting keeps it off.
|
||||
- T1 identity fields omit the primary key and values already contained in `recordLabel`, checking display values before raw values. Preserve the compact header and card field spacing in [`RecordViewHeader.tsx`](../../src/components/records/RecordViewHeader.tsx) and [`RecordView.tsx`](../../src/components/records/RecordView.tsx).
|
||||
- Desktop Copy/Delete remain direct actions unless the Actions menu offers that enabled action, including in a nested menu. Preserve accessibility when a custom menu omits or disables the action; see [`RecordActions.tsx`](../../src/components/records/RecordActions.tsx).
|
||||
- Preserve manual activation in `RecordViewTabs`: Left/Right/Home/End move focus, Enter/Space activates through the existing URL callback. A focused but inactive tab must not mount widgets. Keep one selected tab stop, unique tab/panel links and a focusable named panel. Phone accordion assertions should follow the trigger’s `aria-controls`; the nested section can legitimately have the same accessible name.
|
||||
- Create, edit and copy share the centered `max-w-4xl` container in [`EntityForm.tsx`](../../src/components/forms/EntityForm.tsx).
|
||||
- A fresh profile starts in light mode. A saved dark preference applies only without an application theme; an application theme keeps light mode active. Check [`theme-provider.tsx`](../../src/lib/theme/theme-provider.tsx).
|
||||
|
||||
- Preserve inline naming in `SavedBulkLoadProfiles`: New, Save As and Rename belong inside the original profile section. Prevent Enter from submitting the enclosing process form; retain ownership rules, backend validation and overwrite/delete confirmations.
|
||||
- Dialogs launched from the phone sidebar must use the existing `--qqq-z-overlay` and `--qqq-z-modal` layers. A hardcoded `z-50` puts them behind the drawer. Return focus to a persistent control when the opening menu item unmounts.
|
||||
- For Radix menus, check keyboard navigation, collision placement, available-height scrolling and native touch focus. Returning focus to the same trigger must not dismiss its newly opened menu; outside interactions must still dismiss it. Record a dialog's actual event opener because WebKit can activate buttons without focusing them.
|
||||
- Search-dialog checks must exercise empty and populated Tab cycles, Clear removing its own button, Escape and backdrop dismissal, keyboard reopening, pointer openers that browsers do not automatically focus, and close-before-autofocus cleanup. Build `pnpm build:export` before Javalin acceptance; `pnpm build` produces a separate Node artifact.
|
||||
- Browser keyboard checks must await the resulting focus before sending the next dependent key; Radix defers roving focus. Testing Library `getByRole` uses an exact string name by default and does not accept Playwright's `exact` option.
|
||||
- Standard report input-then-download flows use `compactReportInputs` in `ProcessStepScreen`: preserve the compact card, inline Generate Report action, and accessible form name. Keep the shared form and cancellation; do not replace metadata-driven fields with primitive inputs. Custom components, section labels, process fields and longer flows retain normal step presentation. `DynamicForm.compactInputs` changes ordinary flat-list defaults while preserving explicit grid widths and rich editors.
|
||||
- Test report editor tabs at a viewport short enough to make their panel scroll. Firefox can add a native focus stop on an overflowing container unless it has an explicit `tabIndex`; the report panel uses `-1` so Tab enters the first control. Verify draft preservation and metadata that hides Columns. Check validation contrast on the actual tinted alert background in both themes.
|
||||
- A saved-view reset must discard both the applied report filter and any unapplied quick-filter input. `FilterAndColumnsSetupEditor` changes the quick-filter session key on view selection/reset; do not reset it on ordinary typing. Report view selection/reset must not call saved-view store/delete APIs. Verify Cancel against persisted report and saved-view rows.
|
||||
- Report preview column indicators derive from complete criteria at every nesting level. Opening an indicator reveals the existing filter without appending an empty condition. Preserve read-only preview isolation and editor Cancel behavior; `report-filter-indicators.spec.ts` checks preview rows against SQL and stored report JSON.
|
||||
- Treat table variants as part of the request and cache identity. Report previews reuse `VariantPicker` and the shared per-table storage key; do not query before selection or display rows from the preceding variant while loading. Scope copy and statistics too. Remounting a grid does not cancel an awaiting copy function: guard it against unmount before further requests and clipboard writes. Preserve the delayed-response regression in `report-variants.spec.ts`.
|
||||
|
||||
- Error text must be checked against its rendered background, including Retry hover and visible notifications. The error palette follows configured `errorColor`; Sonner currently uses a light notification surface in both page modes. Request the backend fixture before navigating in theme tests so instance metadata is reset. Assert the actual root theme as well as setting its preference.
|
||||
|
||||
Verify affected screens with paired desktop and phone captures, including a fresh preference state and explicit metadata overrides. Source inspection and passing behavioral tests do not establish full visual review.
|
||||
|
||||
Query URL synchronization in `use-record-query.ts` uses Next-integrated `window.history.replaceState(null, '', url)`. Keep updates synchronous with displayed query state, preserve unrelated parameters/hash, and replace the current entry. Do not reintroduce asynchronous page navigation for filter changes: QRY-093 holds an RSC navigation while checking immediate reload; QRY-017 covers back/forward and legacy JSON links.
|
||||
|
||||
## Workflow for a metadata-driven feature
|
||||
|
||||
1. **Find the Material behavior and acceptance row.** Search the parity ledger by screen or widget type and identify the owning issue and matrix ID. Read the current Material implementation when the behavior is ambiguous. Preserve negative cases and permissions.
|
||||
2. **Trace the QQQ producer.** Locate the Java metadata or renderer that emits the relevant response. Record the exact versioned route and response, including whether data is omitted for unauthorized users. Check the real sample fixture. Do not assume the TypeScript interface is a complete backend specification.
|
||||
For field choices, inspect `inlinePossibleValueSource` in the actual `/qqq/v1/metaData/table/{name}` response. The core Java frontend model can have a property that the v1 response wrapper omits; `REC-062` caught and closed this gap for inline enums.
|
||||
3. **Pick the existing frontend seam.** Add an API function for a new endpoint, a query key for server state, a renderer case for a new widget type, or a metadata-driven component for a new field behavior. Keep application identifiers in metadata/fixtures only.
|
||||
4. **Define behavior for loading, empty, invalid, denied, and failed states.** A malformed widget must not crash neighboring widgets. A denied action must be hidden or disabled and refused by the server. Preserve record identity, selected variant, return path, and unsaved form changes across interactions where applicable.
|
||||
5. **Add focused unit tests for logic and an acceptance scenario for user behavior.** Acceptance tests use the owned QQQ server and check values or SQL state, not only visibility. Give every acceptance test a matrix ID, and use the `diagnostics` fixture. New rows live in the owning `tests/acceptance/matrix/*.json`; the Markdown matrix is generated from those files.
|
||||
6. **Run the affected quality gates, then the full release gate when warranted.** Record commands, build/backend revisions, browsers, and results. Do not write “parity complete” while open Partial/Missing rows remain.
|
||||
7. **Update documentation with verified behavior.** Explain the metadata producer, request/response, UI behavior, and any remaining limitation. Revise this guide's catalog if a widget type changes.
|
||||
|
||||
For code changes, follow import order and file naming in [CLAUDE.md](../../CLAUDE.md): React/Next, external packages, types, library, components; PascalCase component filenames, kebab-case utilities, co-located `*.test.ts(x)`. Client components with interactivity use `'use client'`. User text comes from `label` rather than internal `name`. Interactive elements need a stable `data-qqq-id`, an accessible name, keyboard operation, and appropriate focus management. Inspect existing IDs and [`material-theme.ts`](../../src/lib/theme/material-theme.ts) before promising Material selector or token compatibility.
|
||||
|
||||
## Configuration decision map
|
||||
|
||||
| Desired behavior | Correct source | Verify |
|
||||
|---|---|---|
|
||||
| Add a sidebar entry or nested app | Backend `QInstance.appTree` with `QAppTreeNode.type`; corresponding object in `apps`, `tables`, `processes`, or `reports` | Visible route and `useAppTreeRoutes`; hidden objects omitted from nav but authorized deep link still works; denied user's metadata. |
|
||||
| Add an app home section | `QAppMetaData.sections` (`tables`, `processes`, `reports` by name) | Labels, order, links and permissions in app home. |
|
||||
| Place dashboard widgets | `QAppMetaData.widgets` ordered names; matching `QInstance.widgets` metadata | `WidgetGrid`, widget POST, grid span, denied cases. |
|
||||
| Place a record widget | `QTableSection.widgetName` and compatible record context; [`FormWidgetSection.tsx`](../../src/components/forms/FormWidgetSection.tsx) for editing | Two records render their own values; form widget changes join host validation/save; no cross-record cache leak. |
|
||||
| Configure record layout | `QTableSection.alternatives.RECORD_VIEW`, field/section/widget `gridColumns`, section `collapsible`; Material supplemental table `showRecordSidebar` and `recordViewActionsPlacement`; instance `recordViewActionsPlacement` override | Use [`record-layout-utils.ts`](../../src/lib/utils/record-layout-utils.ts); sidebar defaults off and requires explicit `true`. Preserve the original Next defaults for unset widths: Overview half-width cards, selected section full-width card and two field columns (one on phones); explicit metadata spans win. Verify scroll, saved collapse state, and action location in desktop and phone layouts. |
|
||||
| Configure record action menus | `QTableMetaData.menus` slots `VIEW_SCREEN_ACTIONS` and `VIEW_SCREEN_ADDITIONAL`; [`record-menu-utils.ts`](../../src/lib/utils/record-menu-utils.ts) | Permissions, nested menus, process deduplication, file downloads, and direct Copy/Delete availability. |
|
||||
| Choose basic filter fields | Table dashboard `defaultQuickFilterFieldNames`, otherwise T1 section fields | Read [`quick-filter-utils.ts`](../../src/lib/utils/quick-filter-utils.ts); confirm resolved names, deduplication, basic/advanced conversion and phone sheet. |
|
||||
| Add virtual query columns | Backend `virtualFields` with `isQuerySelectable`; filtering/sorting also requires `isQueryCriteria` | Query payload selects the virtual field; display-only virtual fields cannot become filter or sort criteria. |
|
||||
| Add fields or editors | `QTableMetaData.fields`, sections, adornments, possible-value source | Form, view, validation, server save, keyboard and phone. |
|
||||
| Show a joined field in a record section | Put `joinTable.field` in `QTableSection.fieldNames` and declare a readable `exposedJoins` path | The record GET includes that join; the qualified value renders with joined field metadata. REC-064 covers the real sample in five profiles; permission-denied paths stay absent. |
|
||||
| Read a variant table record | `QTableMetaData.usesVariants` and the query screen's `qqq.tableVariant.<table>` choice | Pass [`storedRecordVariantJson`](../../src/lib/utils/table-variant.ts) to each view/edit/copy record GET; verify the same ID resolves to the selected variant's values ([REC-063](../acceptance/feature-matrix.md)). |
|
||||
| Add a process screen | `QProcessMetaData.frontendSteps` and a supported `QFrontendComponent.type` | Init, submit, async status, cancel/retry, output and server mutations. |
|
||||
| Add report | `QInstance.reports`, optional `processName` | CSV/XLSX/JSON and required inputs; actual downloaded bytes. |
|
||||
| Customize identity | `QInstance.branding` for app/company/logo/banners; `supplementalInstanceMetaData.materialDashboardTheme` for the active application theme | Login subset, footer, header, colors, density, icons, contrast, CSP. `QInstance.theme` is not the active theme input. |
|
||||
| Customize widget chrome | `QWidgetMetaData` fields such as `gridColumns`, `isCard`, `icon`, `collapsible`, `dropdowns`, `showReloadButton`, `showExportButton`, `helpContent`, `icons`, `footerHTML` | Widget response's labels, lists/CSV and interaction; remembered collapse and dropdown state. |
|
||||
| Customize application CSS | `supplementalInstanceMetaData.materialDashboardTheme.customCss` against observed `data-qqq-id`/implemented CSS tokens | Browser rendering, contrast, and actual Material selector/token mapping. |
|
||||
| Configure analytics | `QInstance.environmentValues` with provider settings and privacy opt-ins | No provider initializes without settings; IDs/search/labels withheld by default; CSP for scripts. |
|
||||
| Switch host mode | `QQQ_NEXT_OUTPUT=export` for Javalin jar, or standalone build with `QQQ_BACKEND_URL` | Root deep link, `/qqq/v1` calls, asset paths, browser refresh. |
|
||||
|
||||
Table dashboard settings use `supplementalMetaData.materialDashboard` in v1; `supplementalTableMetaData.materialDashboard` is the legacy fallback. When both objects exist, the v1 object wins. Use [`materialDashboardTableMetaData`](../../src/lib/utils/goto-utils.ts) rather than reading only one spelling.
|
||||
|
||||
The exact type names and optional fields are in [`metadata.ts`](../../src/types/metadata.ts). `supplementalInstanceMetaData`, `supplementalTableMetaData`, and widget `defaultValues` are extension maps, not licenses to invent arbitrary frontend behavior. Inspect each consumer and QQQ backend producer first.
|
||||
|
||||
### A safe widget change, end to end
|
||||
|
||||
For an existing `statistics` widget, the backend can declare metadata equivalent to this TypeScript value:
|
||||
|
||||
```ts
|
||||
import type { QWidgetMetaData } from '@/types'
|
||||
|
||||
const metadata = {
|
||||
name: 'openOrders',
|
||||
label: 'Open orders',
|
||||
type: 'statistics',
|
||||
hasPermission: true,
|
||||
gridColumns: 4,
|
||||
showReloadButton: true,
|
||||
} satisfies QWidgetMetaData
|
||||
```
|
||||
|
||||
The canonical QQQ renderer then returns a payload such as `{ "type": "statistics", "count": 7, "countContext": "orders awaiting review" }`. `ConnectedWidget` calls the typed widget API, `WidgetBlock` renders the frame, and `WidgetRenderer` selects `QqqStatisticsWidget` because `count` is present. The local demo statistic instead uses `value`; do not copy that shape into a QQQ Java renderer without verifying it. To add a new renderer type, add the dispatcher branch, a focused component, a malformed/empty case, real backend fixture, and a WID acceptance row. For existing types, do not add a duplicate dispatcher path.
|
||||
|
||||
For a dropdown, metadata alone is insufficient. The backend payload supplies parallel `dropdownNameList`, `dropdownLabelList`, `dropdownDataList`, and `dropdownDefaultValueList`. The selected option ID is sent back as a widget request parameter. A PVS dropdown uses `possibleValueSourceName` as that parameter name; a `DATE_PICKER` uses its `name`. `storeDropdownSelections` writes a local preference only. Test a stale stored choice and a required selection ([WID-047–WID-050](../acceptance/feature-matrix.md)).
|
||||
|
||||
For `customComponent`, metadata `defaultValues.componentName` and `componentSourceUrl` select a trusted external bundle. The browser loads `window[componentName][componentName]`, exposes `window.React`/`window.ReactDOM`, and passes `qfmdBridge` and `qContext` to the component ([`QqqContainerWidgets.tsx`](../../src/components/widgets/QqqContainerWidgets.tsx), [`qfmd-bridge.tsx`](../../src/components/widgets/qfmd-bridge.tsx)). Verify an existing application's real bundle and CSP before claiming compatibility; the in-repo bridge test alone does not prove it.
|
||||
|
||||
### Widget editors and host forms
|
||||
|
||||
[`WidgetRenderer.tsx`](../../src/components/widgets/WidgetRenderer.tsx) dispatches all 38 renderer labels cataloged in the [developer guide](./next-ui-developer-guide.md#7-widgets-placement-configuration-and-data). Pass the host's `formContext`, `dataVersion`, and `onWidgetData` through the existing connected boundary when the widget edits data. A record form's [`FormWidgetSection.tsx`](../../src/components/forms/FormWidgetSection.tsx) selects editable widgets; a process `WIDGET` step has its own host ([`WidgetComponent.tsx`](../../src/components/process/WidgetComponent.tsx)). Do not make a display widget write directly to the backend or silently mutate a neighboring form field.
|
||||
|
||||
For a row builder, inspect [`row-builder-model.ts`](../../src/components/widgets/row-builder-model.ts) and the backend producer before setting `defaultValues`. It reads `frontendFields` (else `fields`), `associationName` or `outputFieldName`, `orderByFieldName`, `mayReorderRows`, `isForRecordViewAndEditScreen`, `isEditable`, `useModalEditor`, `inlineHeading`, `modalTitle`, and `defaultValuesForNewRowsFromParentRecord`. A record form uses association values when `associationName` is set; a process step uses the named output field. Verify add/delete/reorder, validation, save/cancel and reload with real data. The metadata flags accept booleans or the strings `"true"`/`"false"` where the model explicitly parses them.
|
||||
|
||||
For `pivotTableSetup`, the record view reads `pivotTableJson`; a form supplies the editor context. The editor needs `tableName` and `columnsJson` in the host form and writes serialized `pivotTableJson`. Verify switching the pivot on/off through the actual host form and database readback: a widget-only callback assertion does not prove that clearing survives form validation. Also check modal cancel/save, invalid or missing columns and a persisted reload ([`PivotTableSetupWidget.tsx`](../../src/components/widgets/PivotTableSetupWidget.tsx)). For `dynamicForm`, inspect the payload's `fieldList`/`recordOfFieldValues` and `defaultValues.isEditable`; its editable fields join the process or record form's validation and submission. For a record widget that merges to JSON, verify the named table field exists in a hidden section (rather than marking the field `isHidden`), the renderer returns stored values in `recordOfFieldValues`, required fields block Save, and only the JSON field reaches the record API. [REC-061](../acceptance/feature-matrix.md) proves the full round trip across five browser profiles. `childRecordList` can manage an association when `defaultValues.manageAssociationName` is set. `dataBagViewer` can edit through `storeDataBagVersion` when that process exists.
|
||||
|
||||
`filterAndColumnsSetup` renders a summary and paged, sortable, resizable preview on record view, and an editor when a form context is present, unless the payload sets `overrideIsEditable: false`. [`FilterAndColumnsSetupWidget.tsx`](../../src/components/widgets/FilterAndColumnsSetupWidget.tsx) hands the form to [`FilterAndColumnsSetupEditor.tsx`](../../src/components/widgets/FilterAndColumnsSetupEditor.tsx). The editor loads the selected table's metadata, uses `FilterBuilder` and `ColumnConfig`, offers ordered sort levels and a draft preview inside the modal, and writes `queryFilterJson`/`columnsJson` (or payload-selected field names) to host values on `OK`. Edit opens the filter tab; + Add Filters and + Add Columns appear when the respective saved content is absent and open the matching tab. `Cancel` and closing the dialog discard the draft; persistence still requires saving the host record or advancing a process. A `WIDGET` field adornment with `values.widgetName` uses [`WidgetAdornmentField.tsx`](../../src/components/forms/WidgetAdornmentField.tsx): it sends the host form values and `__formFieldAsWidget_FieldName` to the widget request, overrides `filterFieldName` with the actual field name (including a row index), and writes only that field back. Material supports `filterAndColumnsSetup` in this field path; other widget types show a contained unsupported message. Inspect [`filter-and-columns-utils.ts`](../../src/components/widgets/filter-and-columns-utils.ts) and the backend producer for `hideColumns`, `hideSortBy`, `hidePreview`, `allowVariables`, API-version settings, default filter fields, omitted joins, and custom labels. A missing table disables editing; missing required default-field values produce an alert when an editor control is pressed. A variable saves as a `FilterVariableExpression`; unresolved variables suppress preview and a query opened in a new window until a value is supplied. WID-030, WID-070, WID-071 and WID-072 cover the summary, preview, variable assignment, editor save, field-adornment process path and API-versioned metadata/query/count across five browser profiles. WID-072 supplies version flags to the widget response while the metadata and data requests go to real QQQ API-aware routes. The editor now shares Basic/Advanced controls, saved-view selection, density, refresh, column menus, and statistics with the query components. Saved-view selection is intentionally read-only in this host: it imports the view into the report draft. Read-only preview changes remain local; edited widths/pins serialize into columnsJson on OK and host Save. Starting in RC4, `omitExposedJoins` limits new filter/column choices in both the read-only preview and editor. Existing saved joined columns, criteria and sorts remain usable, and reordering available columns preserves omitted columns in place. Opening the editor and pressing OK preserves those saved fields; Basic/Advanced changes and host Save/reopen retain their values and widths. This is a choice restriction, not a backend permission boundary; existing joined data still uses the normal query permissions. WID-073 checks the configured and unconfigured cases against SQL, including local edits and reload. WID-073 verifies pin/width persistence, full-query copying, and nested statistics. Check the remaining embedded-query parity ledger before claiming complete equivalence; variable assignment currently uses Advanced mode.
|
||||
|
||||
### Composite input editors
|
||||
|
||||
For complete record submissions, call `wireValuesFromForm(table, values, originalValues)`: EntityForm passes `record.values`, association editors pass the existing row values, and process child editors pass the original dialog defaults. Ordinary updates still omit unchanged DATE_TIME fields. Preserve this distinction for copies and child-row edits; omitting the original can shift a repeated DST hour and truncate fractional seconds. REC-065 verifies the copy wire value and SQL source/copy; both child-editor paths have component regressions.
|
||||
|
||||
Use `toLocalDateTimeInput` for seeded process/standalone DATE_TIME values and `fromLocalDateTimeInput(value, originalValue)` at submission. Supplying the original preserves an unchanged instant in the repeated DST hour and its hidden subsecond precision. Normalize the final process payload after contributors have added values, so bulk-edit fields use the same contract. Do not slice UTC text into datetime-local controls or send local text as an instant. PRC-064 and WID-075 exercise America/New_York dates and seconds. Published RC7 sets a one-second step on plain TIME blocks, matching regular/shared fields. Keep TIME values as local clock strings; do not apply instant/timezone conversion. PRC-064 checks unchanged and edited seconds with SQL readback, and WID-074 checks standalone native stepping. This correction is published in RC7 at `f65cf6b`; RC6 remains unchanged.
|
||||
|
||||
`ProcessRun` preserves focus already held inside a configured `[data-qqq-autofocus="true"]` editor; other later screens still focus their heading. Preserve this precedence when changing screen lifecycle. On scalar Enter, normalize text in the hosting form before validation/submission; do not trim unrelated fields or convert typed values to display strings. PRC-063 covers later-screen focus, placeholders, prior values, first blur, whitespace validation and backend-stored text.
|
||||
|
||||
Keep plain block controls visually stable. Backend field metadata supplies the neutral `displayFormat: "%s"`; do not treat that as a request for a different editor. Reuse `BlockInputEditor` and `DynamicFormField` for choices, formatting, behaviors and supported field adornments. Process blocks must bind to the existing screen form and send file contents as multipart files. Never create nested forms or serialize a file as its name. Surrounding process values belong in a field widget's request context, but widget changes must update only its own field.
|
||||
|
||||
Verify hidden/read-only flags, typed choice IDs, numeric conversion, multiline Enter, required uploads and uploaded bytes. For uppercase/lowercase behaviors, replace part of mixed-case text with an uppercase character and check the caret plus stored values. Include Unicode expansion (`ß` to `SS`, `İ` to `i` plus combining dot), then type another character to verify the insertion point; retain backward selection direction. Cover both `STRING` and `TEXT`: textarea Enter must insert a newline without submitting, including with `submitOnEnter: true` (PRC-066/WID-074). For explicitly configured `PASSWORD` fields, verify masked editing, Unicode expansion, show/hide value preservation, Enter/Submit, and independent database readback. Also prove that passwords without a case behavior retain mixed case. The standalone choice field must not collapse below the neighboring plain input's width: a narrow control can place its Clear button under an ordinary center click. Check system and wider native fonts, plus a 320px viewport. The metadata-editor minimum is capped by available width. Sample-dashboard acceptance must initialize the owned backend session and diagnostics before its page-opening hook. PRC-062 exercises the backend round trip, and WID-074 checks the standalone controls. Screenshot comparisons must include the richer editors' height and scroll states; functional success does not approve a changed layout.
|
||||
|
||||
## Navigation and route rules
|
||||
|
||||
`/app/{slug}` resolves an app, table, process, or report from loaded instance metadata, in that order. Do not create a static page for each QQQ object. `isHidden` removes a table/process/report from navigation, while an authorized direct route still works. New dynamic route shapes must work in both standalone and Javalin static export. The export prerenders `_` placeholder paths; [`useRouteParams`](../../src/lib/hooks/use-route-params.ts) translates them from the browser path. Preserve `trailingSlash` handling and avoid constructing deep links that a static host cannot serve. Use [`material-links.ts`](../../src/lib/utils/material-links.ts) for generated links. The not-found route's [`LegacyPathRedirect`](../../src/components/layout/LegacyPathRedirect.tsx) recognizes nested Material paths and `QInstance.redirects` exact or trailing `/*` rules while preserving query/hash. Test deny/not-found loops, a child create URL (`/app/{table}/{id}/createChild/{childTable}`), and refresh of each route shape. Root-path hosting is the documented target; the parity ledger lists base-path support as Missing.
|
||||
|
||||
The [developer route catalog](./next-ui-developer-guide.md#4-navigation-and-deep-links) includes authentication callbacks, table/process developer views, record actions and child creation. `/app/{table}/{id}/{process}` resolves registered processes (table exact name, table suffix, then instance exact name) and redirects with record and return context; the runner enforces permission; preserve that route alongside the reserved edit/copy/dev pages.
|
||||
|
||||
The query URL owns `page`, `pageSize`, `filter`, and `q`; do not move shareable filter state into local storage. Saved views have `/app/{table}/savedView/{id}` and backend persistence. Local storage is for preferences, such as density, column configuration, and widget selections. See [state management](../STATE-MANAGEMENT.md). Put app shell labels, icons, and nested nav in metadata and verify breadcrumb titles against the same tree.
|
||||
|
||||
### Keep configuration coverage executable
|
||||
|
||||
The developer guide's [widget configuration recipes](./next-ui-developer-guide.md#widget-configuration-recipes), [link parameters](./next-ui-developer-guide.md#link-parameters-and-launch-examples), [theme properties](./next-ui-developer-guide.md#theme-property-reference), and [analytics setup](./next-ui-developer-guide.md#analytics-configuration-example) cover the supported keys and copyable response fragments. Update the matching reference whenever a consumer changes; a catalog row with only a widget name is insufficient documentation for new configuration.
|
||||
|
||||
- Compare all `WidgetRenderer` cases with the catalog, including canonical/demo payload branches. Keep `QqqComposite` leaf types and layouts covered separately; they are not top-level widget dispatch labels.
|
||||
- Compare theme properties with `THEME_PROPERTY_KINDS`, including generated typography and radius keys. Keep browser-public `environmentValues` separate from build variables and backend secrets.
|
||||
- Trace dropdown behavior through `ConnectedWidget`: lists pair by position, payload names are request keys, response defaults must be offered options, and `dropdownNeedsSelectedText` gates the body. The interface's `isRequired` and `defaultValue` fields do not by themselves implement those behaviors.
|
||||
- Preserve `gridCols:sizeClass:*` overrides in widget metadata defaults. Their Material breakpoint widths differ from ordinary Tailwind defaults; explicit overrides can keep widgets side by side on smaller screens.
|
||||
- Document `table` cell helper values and `subRows`, chart dataset/URL arrays, and host-specific composite behavior when changing these families. Modal control codes work locally; process `conditional` and interpolation depend on the composite host. RC6 icon compatibility preserves existing Lucide mappings, then lazily resolves valid legacy names through `LegacyMaterialIcon` and the pinned local font. Unknown names retain their fallback. Keep the codepoint inventory, font and license together; verify real JAR font delivery, both themes, existing mapped icons, image precedence, and bundle budgets before integration. Do not attribute this behavior to RC5 or claim support for every Material Symbols name/style.
|
||||
- Check both table-scoped aliases (`/app/{table}/{process}` and `/app/{table}/{report}`) before record GET behavior. Process aliases take precedence over reports; matching names can shadow record IDs. Preserve form hash presets, record tab/view/return parameters, process selection/default inputs, and safe return-path validation.
|
||||
- Preserve the distinction between a displayed preview, a host-form draft, and persisted data. A payload example demonstrates the contract shape; it does not establish backend acceptance for every combination of options.
|
||||
|
||||
## Security and accessibility review points
|
||||
|
||||
- Use the typed API client and same-origin cookie session. Do not put access tokens, passwords, or server secrets in browser storage, public environment variables, widget metadata, or tests.
|
||||
- Session resume now calls `manageSession` with an empty request so browser code can use HttpOnly session cookies; do not restore a `document.cookie` dependency. Test sign-in, refresh, expiry and sign-out against cookie attributes and the real server. Authentication metadata's `?clearAuthenticationMetaDataLocalStorage` switch is a cache reset, not a permission bypass.
|
||||
- Preserve existing identity-provider callback registrations: OAUTH2 uses `/token`, AUTH_0 uses `/`. Provider descriptions may reach login only through the state-matched callback and a one-time browser session value; never render free text from `?error_description=` on a direct link.
|
||||
- Check permission at the UI and the backend. Exercise a denied persona in acceptance; a hidden button alone is insufficient. Do not call widget, process, or record endpoints for an omitted or disabled object.
|
||||
- For exposed query joins, check every target and intermediate table named in `joinPath`. The query screen filters its metadata before rendering columns or filters, and the request hook applies the same rule before sending a join. Keep both paths aligned when changing join UI.
|
||||
- Sanitize HTML. Review CSS, external links, iframes, custom bundle URLs, and CSP when adding a new embed. The Javalin host and standalone server supply security headers; test the deployed response. The [security review](../security/next-ui-review.md) predates some integrated fixes, so reconcile each follow-up with source and current acceptance evidence.
|
||||
- Give forms real labels, `aria-required`/`aria-invalid` where relevant, keyboard operation, error text, and a sensible focus target. Dialogs and phone sheets must trap focus and restore it. Check both desktop keyboard and phone/tablet touch interactions.
|
||||
- The nonmodal Columns popup focuses its named container on open and returns focus to its trigger on explicit close/Escape. Outside clicks keep focus on the clicked control. Embedded report column editors must not take focus when mounted. Verify keyboard search and native browser Tab behavior with QRY-092.
|
||||
- Keep errors local when possible: widget failures show a contained retry and leave neighboring widgets usable; route errors and missing records have dedicated states. Never swallow an API failure into a blank page.
|
||||
|
||||
## Quality gates and evidence
|
||||
|
||||
| Command | Evidence it supplies |
|
||||
|---|---|
|
||||
| `pnpm typecheck` | TypeScript contract check. |
|
||||
| `pnpm lint` | ESLint and license headers. |
|
||||
| `pnpm test` | Vitest unit/regression behavior. |
|
||||
| `pnpm test:e2e` | Mocked Playwright UI paths, useful but not real backend compatibility. |
|
||||
| `pnpm build:export` | Javalin static-export build and dynamic placeholder output. |
|
||||
| `pnpm perf:budget` | Static-export JS/CSS budget after `build:export`; see [performance guide](../acceptance/performance.md). |
|
||||
| `QQQ_ACCEPTANCE_BROWSERS=chromium,firefox,webkit,mobile,tablet pnpm test:acceptance` | Full real-sample browser and backend gate when `QQQ_SAMPLE_JAR` points to a compatible sample jar. |
|
||||
|
||||
For RC1, prioritize workflow depth in Chromium: normal entry/defaults, validation, edit/cancel, save/reopen, dependent selections, permissions/recovery and persisted records or downloads. Preserve broader tests and report their status; do not treat scenario counts as proof that everything works.
|
||||
|
||||
Read [acceptance setup](../../tests/acceptance/README.md) and [browser matrix](../acceptance/browser-matrix.md) before running the final gate. Default acceptance runs only Chromium; the five-project command is necessary to claim the stated browser scope. Phone and tablet run tagged tests, with matrix coverage enforced by the gate. Every test title needs its matrix ID; use fixtures with a reset database, verify persisted effects through SQL, and include `diagnostics` for console, network, and CSP failures. Do not loosen a matrix row, skip a failing browser, or declare an external service excluded without the documented approval path.
|
||||
|
||||
## Common mistakes to avoid
|
||||
|
||||
- **Inventing config from a plan or TypeScript escape hatch.** Read the actual QQQ producer and renderer; `Record<string, unknown>` does not prove a key is honored.
|
||||
- **Treating mocked widgets as a canonical backend contract.** `statistics.value` and several demo aliases are fixtures; canonical QQQ statistics uses `count`.
|
||||
- **Editing a route for one table.** The slug route and app tree are generic; change backend metadata or general dispatch behavior.
|
||||
- **Fetching inside a presentational metadata component.** Keep metadata as a prop where the component pattern expects it; use hooks/API modules at the connected boundary.
|
||||
- **Calling raw `fetch` for QQQ endpoints.** It bypasses versioned base URL, credentials, and centralized error/session behavior.
|
||||
- **Using `name` as display text.** It is an internal identifier; use `label` and a deliberate fallback only when metadata is genuinely absent.
|
||||
- **Claiming Material theme, custom bundle, URL, or workflow parity from a happy path.** Check current code, the parity ledger, negative/permission rows, and actual browser/backend acceptance evidence. The filter/column editor has real-server coverage in WID-070–WID-072, while the embedded full query screen remains parity work.
|
||||
- **Testing a static deep link on a generic file server.** QQQ Javalin serves placeholder pages for dynamic paths; standalone Next has its own routing path.
|
||||
|
||||
## Completion checklist for an agent change
|
||||
|
||||
1. The backend metadata or widget/process producer and the frontend consumer agree on names, types, and payload fields.
|
||||
2. An authorized user can complete the workflow, while an unauthorized user cannot perform it through the API.
|
||||
3. Loading, empty, malformed, missing, failed, and retry states are addressed where they apply.
|
||||
4. Keyboard, focus, labels, responsive phone/tablet behavior, and contrast have been checked for changed UI.
|
||||
5. Focused tests and the applicable real-server matrix row pass. The final browser claim names the projects actually run.
|
||||
6. Static export or standalone build, as applicable, works on refresh and direct deep link.
|
||||
7. Docs and the parity ledger reflect the observed behavior; any open Partial/Missing rows remain visible until proven closed.
|
||||
|
||||
### Appearance preferences
|
||||
|
||||
Use the real ThemeProvider when rendering UserPreferencesDialog or a sidebar with its user footer. The saved `darkModePreference` is distinct from effective `isDarkMode`: an application theme overrides dark mode without erasing the choice. Preferences exposes Light/Dark, explains a theme lock, and resets both saved appearance and layout defaults. Preserve `qqq-dark-mode` storage compatibility. Verify changes through the actual Preferences flow and reload (NAV-056), including phone drawer layering/focus (NAV-038); injecting localStorage alone does not exercise the user control. There is currently no system appearance choice or account/device synchronization.
|
||||
|
||||
|
||||
### Google Drive integration boundary
|
||||
|
||||
Google Drive picker changes span the frontend component/SDK loader and Javalin's CSP
|
||||
and v1 public-environment allow-list (#704; backend PR #908 merged to develop as `bac663044`). The frontend needs those backend changes to receive its public configuration and load under the configured CSP. PRC-065 tests controlled SDK responses with
|
||||
real process execution and SQL; PRC-039 is the approved real-account exclusion. Preserve
|
||||
the shared-drive view, folder-only validation, cancellation and token expiry handling.
|
||||
Never log or persist the access token, and never publish OAuth client secrets in metadata. Javalin applies conditional CSP to its static export; standalone Node/container deployments derive the same sources from backend `dashboardCspSources` authentication metadata. Older backends that omit that optional field require runtime `QQQ_DASHBOARD_CSP_SOURCES` using the [documented Google directives](./next-ui-developer-guide.md#google-drive-process-configuration). Check the actual dashboard HTML response from the selected host. RC1 remains immutable and retains its historical placeholder; these changes belong to the ongoing final 1.0 work.
|
||||
|
||||
### Script editor suggestions
|
||||
|
||||
`CodeEditor` autocomplete is opt-in from `ScriptEditorDialog`; keep other metadata code fields unchanged unless their contract calls for it. QQQ helpers, language keywords and file identifiers are local suggestions. Preserve native undo/redo, composition input, read-only behavior, caret/selection validation, and Escape-then-Tab focus escape. `records/script-completions.spec.ts` verifies loaded-revision undo/redo, prefix and pointer/touch insertion, both-theme accessibility, and SQL-backed save/reload. Native browsers group consecutive edits differently; do not claim every insertion creates a separate undo transaction.
|
||||
|
||||
|
||||
### RC8 implementation notes
|
||||
|
||||
Public JAR checksum/signature/fresh-consumer checks and 28 affected desktop/mobile workflows passed. Full tag Chromium passed 602 and mobile 563; Firefox, WebKit and tablet retain failures, including tablet Clear-search focus. Consult the release notes before claiming cross-browser completion.
|
||||
|
||||
- Search must contain the entire Tab/Shift+Tab cycle, handle Escape from every dialog control, restore the opener after dismissal, and move focus to the input before Clear removes its own button. Explicitly focus a phone pointer opener because WebKit does not necessarily focus clicked buttons. Cancel delayed autofocus when closing. `navigation/search-focus.spec.ts` covers light/dark and pointer/keyboard openings; preserve the existing layout.
|
||||
- Keep RapiDoc width handling on its public CSS parts. The named `qqq-api-overview` and `qqq-api-overview-wide` containers restrict narrow wrapping and wide content sizing to overview specification actions; do not apply it to authentication or endpoint controls. Test full visibility before focus can scroll a clipped control into view, real JSON downloads, keyboard new-tab behavior, both themes, and label fit with wider application fonts at desktop and phone widths.
|
||||
- LONG range validation must run only after integer syntax succeeds. Preserve integer-string bounds as `BigInt` values; converting through `Number` rounds large bounds. Keep the mixed numeric comparison for fractional numeric bounds. Test `safeParse` error results without exceptions, exact positive/negative boundaries, optional blanks, and correction/submission through block and shared process forms. These changes do not introduce a backend range-metadata contract.
|
||||
@@ -0,0 +1,518 @@
|
||||
# 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.
|
||||
|
||||
> **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.
|
||||
|
||||
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.
|
||||
|
||||
## 1. What an application supplies
|
||||
|
||||
QQQ supplies an instance metadata document with `apps`, ordered `appTree`, `tables`, `processes`, `reports`, `widgets`, and `branding`; help content, environment values, supplemental metadata, and redirect rules may also be present. The UI renders those objects for the signed-in user's permissions. A table, widget, or route name is an internal identifier; its `label` is the text users see. Define names and behavior on the backend, then verify the versioned metadata response. Do not edit the frontend to add an application-specific table or menu item.
|
||||
|
||||
The source of truth for the frontend shape is [`src/types/metadata.ts`](../../src/types/metadata.ts). A small, **partial response fragment** illustrates the relationships; it is not a complete `QInstance` response:
|
||||
|
||||
```json
|
||||
{
|
||||
"appTree": [{ "name": "operations", "label": "Operations", "type": "APP", "children": [{ "name": "orders", "label": "Orders", "type": "TABLE" }] }],
|
||||
"apps": { "operations": { "name": "operations", "label": "Operations", "widgets": ["ordersToday"], "sections": [{ "name": "work", "label": "Work", "tables": ["orders"] }] } },
|
||||
"widgets": { "ordersToday": { "name": "ordersToday", "label": "Orders today", "type": "statistics", "hasPermission": true, "gridColumns": 4 } },
|
||||
"branding": { "appName": "Operations", "accentColor": "#1d4ed8" }
|
||||
}
|
||||
```
|
||||
|
||||
`appTree` controls the sidebar; `apps[name].widgets` controls ordered dashboard widgets; `apps[name].sections` controls the app home cards. The server may omit denied objects or send disabled permission flags; the frontend also checks flags before requesting data. The server must still enforce authorization. A widget can also be placed in a table section through `QTableSection.widgetName`, or embedded in a process step of type `WIDGET`. `QInstance.redirects` can map old exact or wildcard Material paths to new targets. `QInstance.homeAppName` (QQQ [#1059](https://github.com/QRun-IO/qqq/issues/1059)) designates the app whose dashboard is the instance's home page; it is published only to users who may open that app. `supplementalInstanceMetaData.materialDashboardTheme` supplies the application theme, and `environmentValues` can configure analytics.
|
||||
|
||||
## 2. Install and run
|
||||
|
||||
The preferred QQQ-hosted distribution is the `com.kingsrook.qqq:qqq-frontend-next` Maven jar. A QQQ 4.1+ application's `QApplicationJavalinServer` serves its static export from `next-dashboard/` at `/` on the same origin as `/qqq/v1`. The [repository README](../../README.md) and [jar README](../maven-artifact/README.md) describe this packaging. The QQQ [sample quickstart](https://github.com/QRun-IO/qqq/blob/main/qqq-sample-project/README.md#quickstart-with-next) exercises the packaged path. Material can be selected with `withServeFrontendMaterialDashboard(true)` or `-Dqqq.javalin.frontend=material`; do not assume Material and Next use the same URL layout.
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>com.kingsrook.qqq</groupId>
|
||||
<artifactId>qqq-frontend-next</artifactId>
|
||||
<version>1.0.0-RC.21</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
For frontend development, use a compatible Node.js version (the README specifies 20.19+ or 22.12+) and pnpm 9.15.9:
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
NEXT_PUBLIC_MOCK_API=true pnpm dev
|
||||
pnpm build:export
|
||||
pnpm build:jar
|
||||
```
|
||||
|
||||
The mock setting is for isolated development. Set `NEXT_PUBLIC_MOCK_API=false` and connect a real QQQ backend for integration. `build:export` writes `out/`; `build:jar` packages it with Maven. The Maven POM enforces the presence of static placeholder pages. The default `pnpm build` instead creates a standalone Node server, whose `QQQ_BACKEND_URL` build setting adds same-origin rewrites. For the container path see [`docker/quickstart/Dockerfile`](../../docker/quickstart/Dockerfile). This checkout targets `1.0.0-RC.13`, overriding an older BOM-managed frontend; confirm its availability in the [RC13 GitHub release](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.13) before installing. It requires a published QQQ 4.1 candidate (4.1.0-RC.1 or later; 4.1.0-RC.2 or later for standalone branding images, QQQ #1009; 4.1.0-RC.3 or later for FILE_PREVIEW thumbnails and the instance home app, QQQ #1056/#1059); see the release notes for the verified backend. Use a matching local backend/frontend build to test later branch changes.
|
||||
|
||||
| Setting | Behavior in this checkout |
|
||||
|---|---|
|
||||
| `NEXT_PUBLIC_API_BASE_URL` | Defaults to `/qqq/v1`; baked into browser requests. Keep the UI and API on the same origin where possible. |
|
||||
| `NEXT_PUBLIC_MOCK_API` | `true` enables MSW development fixtures; `build:export` forces `false`. |
|
||||
| `QQQ_NEXT_OUTPUT` | `export` selects the Javalin-hosted static export; otherwise standalone build. |
|
||||
| `QQQ_BACKEND_URL` | Standalone build only: backend origin for Next rewrites; fixed at build time. |
|
||||
| `NEXT_PUBLIC_APP_VERSION` | Optional version reported to the backend; defaults to `package.json` version. |
|
||||
| `QQQ_DASHBOARD_CSP_SOURCES` | Standalone server runtime setting: additional CSP directives/sources for external services. See Google Drive hosting below. |
|
||||
|
||||
The static export is built for `/`, with `trailingSlash: true`. QQQ serves placeholder HTML for dynamic route shapes; [`useRouteParams`](../../src/lib/hooks/use-route-params.ts) reads real path segments from the browser. Keep that behavior intact when adding routes. The [parity ledger](../acceptance/material-parity.md) currently lists sub-path hosting as unsupported. A plain file server without the QQQ route fallback is not a valid deep-link test.
|
||||
|
||||
## 3. Authentication and security
|
||||
|
||||
`GET /qqq/v1/metaData/authentication` selects `AUTH_0`, `OAUTH2`, `FULLY_ANONYMOUS`, `MOCK`, or `TABLE_BASED` (see [`QAuthenticationMetaData`](../../src/types/metadata.ts) and [`auth-provider.tsx`](../../src/lib/auth/auth-provider.tsx)). OAuth flows use authorization code and PKCE; table-based sign-in sends credentials to the QQQ session endpoint. The server issues `sessionUUID`/`sessionId` cookies; current Next code resumes an existing session with an empty `manageSession` request, allowing HttpOnly cookies that browser script cannot read. Browser API calls use credentials. A 401 clears session state and returns to sign-in. The pre-login response exposes only limited login branding, not banners or custom CSS. Authentication metadata is cached briefly; `?clearAuthenticationMetaDataLocalStorage` clears that cache.
|
||||
|
||||
Existing provider registrations remain usable: OAUTH2 redirects to `{origin}/token`, while AUTH_0 redirects to `{origin}/`. Both use the same state-checked PKCE callback. A state-matched provider denial displays its description once on the login page; a forged callback or direct login link shows only a safe generic error.
|
||||
|
||||
Permissions apply to metadata and data: check table capabilities and the corresponding read/insert/edit/delete flags, `process.hasPermission`, `report.hasPermission`, and `widget.hasPermission`. A disabled widget can show a permission message without fetching; omitted metadata cannot be guessed from the UI. Use the backend to prove a forbidden operation is rejected. HTML from metadata or widget payloads is sanitized before display. Treat theme `customCss` and custom component bundles as trusted application configuration with a stricter review boundary. The Javalin dashboard and standalone Next server configure security headers and CSP; embedding an external provider needs an allowed source, not a disabled policy. The [security review](../security/next-ui-review.md) is an earlier audit; compare its follow-ups with current code and the latest acceptance gate before treating them as resolved.
|
||||
|
||||
## 4. Navigation and deep links
|
||||
|
||||
[`useAppTreeRoutes`](../../src/lib/hooks/use-routes.ts) recursively walks `appTree` to build sidebar links, labels, ancestor breadcrumbs, and the first accessible app. `APP` nodes nest; visible `TABLE`, `PROCESS`, and `REPORT` nodes navigate to `/app/{name}`. Hidden objects are omitted from navigation but can still have direct routes when authorized. An app home uses its `widgets` and `sections`; a table slug opens its query page; a process slug opens its wizard; a report slug opens its runner. The unified route resolves name collisions in that order: app, table, process, report. Use unique names across these maps.
|
||||
|
||||
| URL | Screen |
|
||||
|---|---|
|
||||
| `/` | Opens `/app`, or handles an Auth0 authorization callback. |
|
||||
| `/login`, `/callback`, `/token` | Sign-in and supported authorization callback routes. |
|
||||
| `/app` | Instance home page: the `homeAppName` app's dashboard when the instance designates one the user may open (client-side redirect to `/app/{homeAppName}`; the sidebar logo and breadcrumb Home crumb link there and the sidebar has no separate Dashboard entry), otherwise Next's overview. |
|
||||
| `/app/{appName}` | App home and its ordered widgets/sections. |
|
||||
| `/app/{tableName}` | Query or key lookup, according to table capability. |
|
||||
| `/app/{tableName}/create` | Create record. |
|
||||
| `/app/{tableOrProcessName}/dev` | Table or process developer view; table takes precedence. |
|
||||
| `/app/{tableName}/{recordId}` | Record view; `/edit`, `/copy`, and `/dev` are subroutes. |
|
||||
| `/app/{tableName}/{processName}` | Table-scoped process alias; redirects to the process runner with query parameters and a return path to the table. |
|
||||
| `/app/{tableName}/{reportName}` | Alias for a report whose `tableName` matches; redirects to its runner. |
|
||||
| `/app/{tableName}/{recordId}/createChild/{childTable}` | Child create form with parent association values. |
|
||||
| `/app/{tableName}/{recordId}/{processName}` | Resolves a registered process and redirects to its runner with record and return context; execution still checks permission. |
|
||||
| `/app/{tableName}/key?...` | Unique-key lookup. |
|
||||
| `/app/{tableName}/savedView/{viewId}` | Server-backed saved query view. |
|
||||
| `/app/{processName}` | Process run. |
|
||||
| `/app/{reportName}` | Report run and download. |
|
||||
| `/app/search?q=...` | Navigation, recent-record, and available record search results. |
|
||||
| `/app/developer` | Developer information available to permitted users. |
|
||||
|
||||
The header search and `/` dialog locate navigation targets and recent records; record search is offered when the backend supports it. RC8 includes the search-focus correction: Tab/Shift+Tab remain within the search dialog, Clear returns focus to the search input, and Escape or backdrop dismissal restores focus to the opener. A pointer-opened phone search restores focus to its actual button in verified Chromium/mobile cases. RC8 tablet WebKit has two open Clear-search focus failures; see the release notes for remaining browser issues. RC7 does not include this correction. The command palette opens with `.` or Cmd/Ctrl+K and has a header button; `?` opens keyboard help. Keyboard shortcuts on query and record pages are listed in the [feature matrix](../acceptance/feature-matrix.md). The sidebar has a phone drawer, nested app groups, banners, and a user menu. Branding can add a company footer and a themed bar above the shell. `?helpHelp` reveals help-slot keys for content authors.
|
||||
|
||||
Material's nested `/{app}/{table}/{id}` URLs are not the native Next route. A not-found page now checks the signed-in instance metadata, applies exact or `/*` redirect rules, and resolves nested Material app paths to `/app/...` while preserving query/hash ([`legacy-paths.ts`](../../src/lib/utils/legacy-paths.ts), [`LegacyPathRedirect.tsx`](../../src/components/layout/LegacyPathRedirect.tsx)). Verify a specific old bookmark rather than assuming arbitrary legacy URLs match. For links generated by widgets use [`material-links.ts`](../../src/lib/utils/material-links.ts), which normalizes supported legacy links and preserves a trailing slash for static-export routes.
|
||||
|
||||
### Link parameters and launch examples
|
||||
|
||||
The table-scoped process/report aliases are resolved before a record GET, so avoid record IDs that collide with those names. Process resolution checks an exact table process, then an instance process; the record-scoped route also accepts a table process name suffix. A matching process takes precedence over a report. Use full process names for unambiguous new links. See [`material-links.ts`](../../src/lib/utils/material-links.ts).
|
||||
|
||||
| Link context | Supported parameters and behavior |
|
||||
|---|---|
|
||||
| Record view query | `tab` selects a section; `view=tabs` or `view=list` selects the layout. `from` is a safe local return path and `fromLabel` labels it. The default view comes from Preferences. |
|
||||
| Record view hash | `#audit` opens audits when available; `#/launchProcess=<name>` launches a process; `#<sectionName>` selects a section. `#/createChild=<table>` starts child creation and accepts the presets below. |
|
||||
| Create/child-create hash | `#/defaultValues=<encoded JSON object>/disabledFields=<encoded JSON array>` seeds and locks fields. `disabledFields` also accepts a JSON object whose truthy entries name locked fields. These are UI presets; backend validation and authorization still apply. |
|
||||
| Process query | `recordsParam=recordIds&recordIds=1,2` selects IDs; `recordsParam=filterJSON&filterJSON=<encoded QQueryFilter>` selects by filter. The legacy selector `queryFilter` uses the same `filterJSON` value. |
|
||||
| Process context | `defaultProcessValues=<encoded JSON object>` presets inputs; `returnTo=<encoded local path>` supplies the return destination. `tableName` supplies launch context only for a process without its own table and only when that table exists in metadata. |
|
||||
| Dashboard hash | `#<widgetName>` targets a widget anchor. An embedded `process` widget also reads the page's `recordIds` query parameter. |
|
||||
|
||||
Build encoded links instead of concatenating raw JSON. For example, where metadata declares the supplied process/table names:
|
||||
|
||||
```ts
|
||||
const params = new URLSearchParams({
|
||||
recordsParam: 'recordIds',
|
||||
recordIds: selectedIds.join(','),
|
||||
defaultProcessValues: JSON.stringify({ sendNotification: true }),
|
||||
returnTo: `/app/${encodeURIComponent(tableName)}`,
|
||||
})
|
||||
const processHref = `/app/${encodeURIComponent(processName)}/?${params}`
|
||||
const createHref = `/app/${encodeURIComponent(tableName)}/create/#/defaultValues=${encodeURIComponent(JSON.stringify(defaultValues))}/disabledFields=${encodeURIComponent(JSON.stringify(disabledFieldNames))}`
|
||||
```
|
||||
|
||||
Only preset fields actually declared by the application's metadata. Record/form hash decoding is implemented by [`formPresetsFromHash`](../../src/lib/utils/material-links.ts); process query decoding lives in the [slug page](../../src/app/%28dashboard%29/app/%5Bslug%5D/page.tsx). Test a generated link on the static-export host and after refresh.
|
||||
|
||||
## 5. Tables, queries, records, and forms
|
||||
|
||||
A failed query displays its backend error with Retry and Dismiss controls. Retry repeats the request; Dismiss closes the panel without changing the filter. Correct or reset an invalid filter to load records again. Query and pivot error text use theme-derived shades for readability; the top error notification also uses the configured error palette.
|
||||
|
||||
Tables are described by [`QTableMetaData`](../../src/types/metadata.ts): `primaryKeyField`, `fields`, `sections`, `associations`, `exposedJoins`, capabilities, and permission flags. A field supplies `name`, `label`, `type`, `isRequired`, `isEditable`, `isHeavy`, `isHidden`, and `adornments`; possible values, bounds, formats, and help are optional. The supported primitive field types are `STRING`, `INTEGER`, `LONG`, `DECIMAL`, `BOOLEAN`, `DATE`, `TIME`, `DATE_TIME`, `TEXT`, `HTML`, `PASSWORD`, and `BLOB`. [`DynamicFormField`](../../src/components/forms/DynamicFormField.tsx) chooses editors, while [`zod-from-metadata.ts`](../../src/lib/utils/zod-from-metadata.ts) builds validation. Configure fields in QQQ metadata, not in a per-table React component.
|
||||
|
||||
Table dashboard settings are read from `supplementalMetaData.materialDashboard` in the v1 response, with `supplementalTableMetaData.materialDashboard` accepted as a legacy fallback. The v1 object takes precedence when both are present. These settings include `defaultQuickFilterFieldNames`, `gotoFieldNames`, `showRecordSidebar`, and `recordViewActionsPlacement`. Quick-filter defaults use the configured nonempty field list, otherwise T1 section fields; unresolved names and duplicates are removed. Query-selectable `virtualFields` also become columns, but only virtual fields with `isQueryCriteria` can be filtered or sorted.
|
||||
|
||||
For a fixed choice list, put `inlinePossibleValueSource` on the QQQ field with `type: ENUM`, an `idType` matching the field, and ordered `enumValues` containing `id` and `label`. Do not also set `possibleValueSourceName`: QQQ rejects both sources on one field. Next filters inline labels by prefix without a possible-values request and stores the selected `id`. A `CHIP` field adornment with keys such as `color.HIGH` and `icon.HIGH` styles both record values and form options. The v1 table metadata response must include the inline source; [REC-062](../acceptance/feature-matrix.md) verifies its rendering, persistence, and reload against the sample server.
|
||||
|
||||
The query page supports basic quick-filter chips and an advanced nested filter builder, quick search, sort, column visibility/order/width/pinning, density, pagination, CSV export, row selection, bulk process launch, column statistics, and table variants where declared. On desktop, Filter opens a panel containing the Basic/Advanced switch, sort controls, quick-filter chips, and advanced builder. On phones, Filter opens a sheet containing the same mode, sort, and quick-filter controls; Advanced shows the nested builder inside that sheet. Opening Filter preserves the selected mode. Advanced mode uses a single editable condition list with per-row removal and one confirmed Clear action in the footer. Complex criteria remain in Advanced mode; the basic chips expose metadata-selected fields and let the user add more. The Columns popup supports keyboard entry, search, reordering and focus return on close; embedded report column editors keep the surrounding editor focus. Query columns start at 150 px unless a `SIZE` adornment or a saved/resized width supplies another value. `TABLE_QUERY`, `TABLE_GET`, `TABLE_COUNT`, `TABLE_INSERT`, `TABLE_UPDATE`, `TABLE_DELETE`, `TABLE_EXPORT`, and `QUERY_STATS` gate operations alongside permissions. For tables with `usesVariants`, choose a backend-provided variant before querying; it is stored under `qqq.tableVariant.<table>` and sent to query, process and record view/edit/copy GET requests. Backend field-download URLs carry the same variant; inline BLOB data comes from the variant-specific record response. [REC-063](../acceptance/feature-matrix.md) verifies view/edit/copy against duplicate record IDs in two variants. URL parameters `page`, `pageSize`, `filter`, and `q` carry shareable query state ([`use-record-query.ts`](../../src/lib/hooks/use-record-query.ts)). Saved views have backend persistence, quick-view shortcuts and counts, modified-state comparison, and their own routes when the backend capabilities exist ([`use-saved-views.ts`](../../src/lib/hooks/use-saved-views.ts)). Browser local storage holds personal display preferences and last view state, not authoritative records or permissions.
|
||||
|
||||
Record pages render sections, field values, associated records, widgets, metadata-defined actions, help, and audit history. `QTableSection.alternatives.RECORD_VIEW` replaces the base section for this screen. Visible sections appear in metadata order; fields, sections and widget cards use `gridColumns` as twelfths on large screens and stack on narrow screens. When widths are not configured, Overview uses half-width cards on large screens; a selected section uses a full-width card with two field columns from the small breakpoint, stacking fields on phones. Explicit field, section, or widget widths take precedence over those presentation defaults. A collapsible section uses `collapsible.isCollapsible` and `initiallyOpen`, then remembers the choice under `qqq.recordView.collapsibleSectionOpenStates.<table>.<section>`; a section's rule overrides its widget's rule. The sticky desktop section sidebar is off by default; only `showRecordSidebar: true` in the table dashboard settings enables it. `recordViewActionsPlacement` can be set on the table or `supplementalInstanceMetaData.materialDashboard`, with the instance winning; `INLINE_WITH_PAGE_TITLE` places desktop actions beside the local record title, while the default `IN_IDENTITY_SECTION` keeps them in the identity controls. The identity card omits the primary-key T1 field and T1 values already contained in the record label; display values take precedence over raw values for that comparison. Desktop Copy and Delete buttons are omitted when the Actions menu already contains the corresponding enabled action, including in a submenu. An absent or disabled menu action preserves its available direct button. The Audit action reads through `GetAuditsForRecord` when permitted, otherwise through the `audit` table; its dialog groups entries by the viewer's local date, supports oldest/newest sort saved under `audit.sortDirection`, and reports when the 1,000-detail cap truncates results. Create/edit/copy forms use a centered container capped at `max-w-4xl` and follow field metadata, section layout, adjusters, possible-value choices, and server validation; `FILE_UPLOAD` width `full` spans the form grid, while `half` uses the default half width. Copy defaults to Base Copy, which duplicates editable fields; choose Full Copy to edit and submit loaded named associations with the new record. A widget section can edit association rows or form values through `WidgetFormContext` when its type and defaults allow it. Process launch actions can receive selected record IDs or a filter. `gotoFieldNames` in Material supplemental table metadata enables a Go To dialog for the primary key or configured unique keys. Developer views are capability and permission dependent. The record developer view (`/app/{table}/{id}/dev`) keeps the record screen's header and actions and adds a sticky section navigation over fields, API access (v1 routes and application APIs from their OpenAPI specs, filtered by the user's table permissions, with curl, fetch and Java examples that only use placeholder credentials), the panels returned by `GET /qqq/v1/table/{table}/{pk}/developer/panels` (rendered generically from their blocks: KEY_VALUES, TEXT, CODE, TABLE, LINKS and ACTIONS; app-relative or http(s) links only), relations, recent audits, associated scripts, raw JSON and a searchable table-metadata explorer. Modules and qbits add panels by registering a `DeveloperPanelProviderInterface` in QQQ; no frontend change is needed. Consult [record and query acceptance rows](../acceptance/feature-matrix.md) for exact supported cases; do not infer coverage from the existence of a component alone.
|
||||
|
||||
Query changes replace the current URL immediately, preserving unrelated parameters and the hash. Reload and browser back/forward restore the visible filter, sort, search and pagination state; changing those controls does not add a separate history entry.
|
||||
|
||||
Record section tabs support Left/Right, Home and End to move focus. Enter or Space opens the focused section; Tab moves into the named content panel. Moving focus alone does not load section widgets. Phone sections use accordion buttons with Enter/Space activation. The selected tab remains reflected in the URL.
|
||||
|
||||
Query header labels can wrap to two lines when their text and sort indicator do not fit, including with wider system or configured fonts. Configured column widths and 44px touch menu targets remain effective. Active sorts display their direction arrow; unsorted headings remain labeled sort buttons. Relative-date preset menus scroll within the space available beside their trigger.
|
||||
|
||||
Selection and display-density menus support arrow-key navigation, Enter to choose, and Escape to close and return focus to the trigger. They stay within the viewport on phone and tablet. Density choices expose radio-menu semantics; the selected option uses the theme primary/foreground color pair.
|
||||
|
||||
Record action menus come from `QTableMetaData.menus`. The first `VIEW_SCREEN_ACTIONS` menu replaces the default Actions menu; `VIEW_SCREEN_ADDITIONAL` menus add ordered menu buttons. Item types are `BUILT_IN`, `RUN_PROCESS`, `DOWNLOAD_FILE`, `SUB_MENU`, `SUB_LIST`, and `DIVIDER`. Their `values` supply `option`, `processName`, `fieldName`, or nested `items`, respectively. Built-in options are `NEW`, `COPY`, `EDIT`, `DELETE`, `DEVELOPER_MODE`, `AUDIT`, `THIS_TABLE_PROCESS_LIST`, and `ALL_TABLES_PROCESS_LIST`; permissions filter available operations. See [`record-menu-utils.ts`](../../src/lib/utils/record-menu-utils.ts) for resolution and process deduplication.
|
||||
|
||||
For a joined field in a record section, list its qualified name (`person.firstName`) in `QTableSection.fieldNames` and expose a readable join through `QTableMetaData.exposedJoins`. Next asks for that join on the record GET and uses the joined table's field metadata to render the qualified response value. Join paths through tables without read permission are omitted. [REC-064](../acceptance/feature-matrix.md) checks the real sample server in five browser profiles.
|
||||
|
||||
## 6. Processes and reports
|
||||
|
||||
`QProcessMetaData` declares the process name, label, table association, permission, limits on input records, and ordered `frontendSteps`. The wizard initializes a run, submits each step, polls asynchronous jobs, handles validation results, and cancels or retries as appropriate ([`ProcessRun.tsx`](../../src/components/process/ProcessRun.tsx), [`processes.ts`](../../src/lib/api/processes.ts)). It preserves return navigation to its caller. `QFrontendComponent.type` selects a process component. The currently registered types are:
|
||||
|
||||
| Type | Purpose |
|
||||
|---|---|
|
||||
| `HELP_TEXT`, `HTML` | Guidance and sanitized markup. |
|
||||
| `EDIT_FORM`, `VIEW_FORM` | Metadata-driven writable and read-only fields. |
|
||||
| `RECORD_LIST`, `PROCESS_SUMMARY_RESULTS` | Input/output records and completion summary. |
|
||||
| `BULK_EDIT_FORM` | Changes applied to selected records. |
|
||||
| `BULK_LOAD_FILE_MAPPING_FORM`, `BULK_LOAD_VALUE_MAPPING_FORM`, `BULK_LOAD_PROFILE_FORM` | File preview; flat/tall/wide layout and header-row choices; field-to-column/default mappings; per-value translations; saved load/edit profiles. |
|
||||
| `VALIDATION_REVIEW_SCREEN` | Review validation findings before commit. |
|
||||
| `DOWNLOAD_FORM` | Download parameters and result. |
|
||||
| `GOOGLE_DRIVE_SELECT_FOLDER` | Selects one folder from a shared drive using Google OAuth. Requires `GOOGLE_APP_CLIENT_ID` and `GOOGLE_APP_API_KEY` in instance environment values. Submits `googleDriveAccessToken`, `googleDriveFolderId`, and `googleDriveFolderName`; cancellation clears them. |
|
||||
| `WIDGET` | Embedded connected widget. |
|
||||
|
||||
Do not invent `values` keys for these components: their shapes vary by backend process implementation. Start with the QQQ producer and a [real fixture](../../tests/acceptance/fixture), then verify the versioned response and end-to-end step submission. A `WIDGET` step uses the named widget's full dashboard controls; an editable `childRecordList` contributes `frontendRecords`, while an editable `rowBuilder` writes its configured output field. Ad hoc composite blocks share the dashboard block renderer, including modal control codes, typed inputs and layouts. A dashboard widget of type `process` embeds the wizard without its stepper or Cancel action; Return starts a fresh run in place. For bulk load, test duplicate headers, required fields, saved profiles, value mappings, validation review, and the committed records. Reports appear in `QInstance.reports`. [`ReportRun.tsx`](../../src/components/reports/ReportRun.tsx) offers CSV, XLSX, and JSON, handles required inputs and asynchronous process-backed reports, and provides a download. A report without a backing process uses the versioned streaming report route. Saved and scheduled report behavior is covered by the report rows of the acceptance matrix.
|
||||
|
||||
For bulk load, the mapping screen derives required fields, child associations, layout and suggested mappings from the backend's table structure and process values. Its Add Fields menu searches the main and child-table groups; WIDE layouts may repeat a child field. A mapped field can read a file column or a typed default, including possible values, dates and booleans. Header-row changes remap by name and warn about duplicates. File and value mapping show the current profile in a bordered Saved Bulk Load Profiles section; the final review shows that status in its profile summary. Saved bulk-load profiles are grouped by owner; owners can save, rename and delete, while shared profiles can be selected but not modified. Open the profile menu with Enter or Arrow Down; use arrow keys or Home/End to move, Enter to choose, and Escape to close. Long lists scroll within the viewport. New, Save As and Rename open an inline Profile Name field inside the profile section. Save Profile or Enter saves the name without advancing the process; Cancel or Escape closes it. Duplicate-name errors stay beside the editor. Closing returns focus to the opening control, or the profile-menu button if that control was replaced after saving. Updates and deletion retain their confirmations. New, Empty Mapping, Suggested Mapping and Reset All Changes affect only the draft until the process step is submitted. The validation preview can show the associated child grids declared by `previewRecordAssociated*` values.
|
||||
|
||||
Report output formats come from the backend's `reportFormat` possible values unless a process screen declares its own `reportFormat` field. Report input screens use the same dynamic form renderer as other processes, so declared possible values, Booleans, dates and validation rules apply. Intermediate report process screens render before the download; the completed file can be a server path or storage reference.
|
||||
|
||||
Standard report processes with an input form followed by a download use the original compact input card and **Generate Report** action. Cancel remains available beside it. Ordinary fields stack at a 384px maximum width; explicitly configured grid widths and rich editors keep their layout. These inputs retain the shared dynamic form, required validation, possible values and typed submission. Longer or custom report processes retain their step headings and navigation.
|
||||
|
||||
## 7. Widgets: placement, configuration, and data
|
||||
|
||||
Each app dashboard lists widget **names** in order. The matching `QWidgetMetaData` supplies `name`, `label`, optional `type`, and `hasPermission`. The UI posts to `/qqq/v1/widget/{name}` with current dropdown and record parameters ([`ConnectedWidget.tsx`](../../src/components/widgets/ConnectedWidget.tsx), [`widgets.ts`](../../src/lib/api/widgets.ts)). The widget response contains its data and sometimes its type. [`WidgetRenderer.tsx`](../../src/components/widgets/WidgetRenderer.tsx) uses `metadata.type` first, falling back to `payload.type`; it contains errors and unknown types within the widget.
|
||||
|
||||
A valid TypeScript metadata example, suitable for an instance metadata fixture (the backend should produce the equivalent JSON), is:
|
||||
|
||||
```ts
|
||||
import type { QWidgetMetaData } from '@/types'
|
||||
|
||||
const ordersToday = {
|
||||
name: 'ordersToday',
|
||||
label: 'Orders today',
|
||||
type: 'statistics',
|
||||
hasPermission: true,
|
||||
gridColumns: 4,
|
||||
isCard: true,
|
||||
showReloadButton: true,
|
||||
showExportButton: true,
|
||||
tooltip: 'Orders created since midnight',
|
||||
icon: 'inventory_2',
|
||||
collapsible: { isCollapsible: true, initiallyOpen: true },
|
||||
dropdowns: [{ name: 'region', label: 'Region', possibleValueSourceName: 'regions', type: 'POSSIBLE_VALUE_SOURCE' }],
|
||||
storeDropdownSelections: true,
|
||||
} satisfies QWidgetMetaData
|
||||
```
|
||||
|
||||
`gridColumns` spans 1–12 twelfths on large screens (default 12); smaller screens stack widgets unless explicit breakpoint overrides are supplied in `defaultValues` (see the configuration recipes below). `isCard: false` removes card chrome. `icon` supplies the main header glyph using the Next outline presentation. `collapsible.isCollapsible` enables a header toggle, starts according to `initiallyOpen`, and remembers state under `qqq.widget.collapsibleOpenState.<name>`. `minHeight`, `footerHTML`, help content, `icons.topLeftInsideCard`/`topRightInsideCard`, tooltip, reload, and export customize the frame. Payload `label`, `sublabel`, and `footerHTML` can override or add frame text; a parent payload's `isLabelPageTitle` promotes the label to a page title. HTML is sanitized. For export, provide `csvData` as rows of cells; a table payload may also export `columns` and `rows`. The dropdown controls are declared in metadata **and populated by parallel payload lists** (`dropdownNameList`, `dropdownLabelList`, `dropdownDataList`, `dropdownDefaultValueList`). Controls offer searchable choices or a native date input; metadata can set `width`, `startIconName`, `allowBackAndForth`, `backAndForthInverted`, and `disableClearable`. The selected ID becomes a request parameter; a possible-value source uses its source name. Required selection can delay content, and `storeDropdownSelections` persists choices at `qqq.widgets.dropdownData.<widget>.<param>`. See [WID-040–WID-050](../acceptance/feature-matrix.md) for the tested contract.
|
||||
|
||||
Choice dropdowns support search and keyboard selection. A `DATE_PICKER` uses the platform calendar and a Today action; set `allowBackAndForth` for previous/next day buttons. If the `timeframe` dropdown offers `custom`, selecting it opens start and end inputs; after both are entered, the UI sends `custom,<UTC start>,<UTC end>`.
|
||||
|
||||
For example, a canonical QQQ statistics response uses `count`, not the demo fixture's `value`:
|
||||
|
||||
```json
|
||||
{ "type": "statistics", "count": 42, "countContext": "open orders", "percentageAmount": 12.5, "percentageLabel": "vs previous week", "increaseIsGood": true }
|
||||
```
|
||||
|
||||
The catalog below lists **every explicit dispatch type** in `WidgetRenderer.tsx`. Payload fields are from the corresponding renderer; they are examples of what the backend emits, not a replacement for the QQQ Java renderer API. When `type` is `chart` or a chart variant, canonical `chartData.labels` and `chartData.datasets` select the QQQ chart path. A payload with `chartType`, `seriesData`, or demo `data` instead uses the legacy/demo chart path.
|
||||
|
||||
### Canonical QQQ display widgets
|
||||
|
||||
| `type` | Use and important response fields |
|
||||
|---|---|
|
||||
| `alert` | Status message: `html`, `alertType`, optional `bulletList` and `hideWidget`. A demo `message` payload uses the demo alert renderer. |
|
||||
| `divider` | Horizontal separator, optionally `label`; rendered without normal card chrome. |
|
||||
| `fieldValueList` | Labeled record fields: `fields`, `record.values`, optional `record.displayValues`, prefix icon maps, indentation map. Display values win. |
|
||||
| `generic` | Common widget frame fields, especially `sublabel` and sanitized `footerHTML`; optional `html` payload. |
|
||||
| `html` | Sanitized `html` string. A demo payload with `blocks` uses the demo block renderer. |
|
||||
| `location` | `imageUrl`, `title`, `description`, `location`, `footerText`. |
|
||||
| `quickSightChart` | Provider embed `url` in an iframe; supply a server-authorized URL and appropriate CSP. Real AWS account coverage is an approved external exclusion. |
|
||||
| `stepper` | `steps` with labels and optional links/icons, plus zero-based `activeStep` and optional `title`. |
|
||||
| `usaMap` | `mapMarkerList` of names and latitude/longitude; optional CSS `height`. |
|
||||
|
||||
### Canonical QQQ charts, statistics, and tables
|
||||
|
||||
| `type` | Use and important response fields |
|
||||
|---|---|
|
||||
| `barChart` | Vertical bars from `chartData.labels` and `chartData.datasets`; dataset `data`, colors, and URLs. |
|
||||
| `horizontalBarChart` | Horizontal bars; supports negative values relative to zero. |
|
||||
| `stackedBarChart` | Multiple datasets stacked for each label. |
|
||||
| `lineChart` | Line points from labels and datasets. |
|
||||
| `smallLineChart` | Compact line presentation with `title` and optional sanitized `description`. |
|
||||
| `pieChart` | Colored slices and legend; optional `chartSubheaderData`. |
|
||||
| `chart` | Generic canonical chart, displayed as a bar variant when `chartData` is used; can be inferred from payload type. |
|
||||
| `statistics` | Headline `count`, optional `countContext`, `percentageAmount`, `increaseIsGood`, and count/percentage links. Demo `value` payload uses the demo statistic renderer. |
|
||||
| `multiStatistics` | `statisticsGroupData`: groups with `header`, `subheader`, icon, and statistic list. |
|
||||
| `table` | `columns` (`header`, `accessor`, alignment, optional help/width) and `rows`; typed number, HTML, tooltip, image, composite/block cells; hidden helper columns, expandable subrows, optional `noRowsFoundHTML`, entries-per-page, sticky header and last-row behavior. |
|
||||
| `multiTable` | `tableDataList` of table payloads, each with its own label, rows and export action. |
|
||||
|
||||
### Composite and interactive widgets
|
||||
|
||||
| `type` | Use and important response fields |
|
||||
|---|---|
|
||||
| `composite` | QQQ block tree: `blocks`/`blockTypeName`, layout, values, links, tooltips and styles. Without those fields, demo `childWidgets` uses `CompositeWidget`. |
|
||||
| `parentWidget` | Server response `childWidgetNameList`; renders permitted registered children in a 12-column grid or `layoutType: "TABS"`. Children fetch their own data. |
|
||||
| `process` | Inline wizard using `processMetaData`, optional `defaultValues` and selected record IDs. Server permission still applies. |
|
||||
| `childRecordList` | Joined child records from `queryOutput.records` and `childFrontendTableMetaData`/`childTableMetaData`; `includeExposedJoinTables` adds readable join columns, and `defaultValuesForNewChildRecords` hides parent-key columns on screen. Optional `viewAllLink`, add-child defaults, row limits, `disableRowClick`, and CSV export via `showExportButton`. On a record form, `defaultValues.manageAssociationName` enables association editing. |
|
||||
| `customComponent` | Loads a trusted bundle from metadata `defaultValues.componentSourceUrl` and `componentName`, then resolves `window[componentName][componentName]`. The component receives widget metadata/data/record, `qContext`, and `qfmdBridge`; `window.React`/`window.ReactDOM` are exposed. Check CSP and test the actual older bundle before claiming compatibility. |
|
||||
| `cronUI` | Record view displays the expression, time zone and backend `cronDescription`. Put the widget in a table section and set `defaultValues.includeOnRecordEditScreen: true`, `cronExpressionFieldName`, and `timeZoneFieldName` to edit the named record fields on create/edit forms. The expression editor offers Basic days/hours/minutes, Advanced Quartz input, Clear, live description and validation. Give the time-zone field a possible-value source to render a select. |
|
||||
| `dynamicForm` | Labeled `fieldList` and `recordOfFieldValues`, including merged JSON field values. Editable in a form/process host when `defaultValues.isEditable` allows it; fields join host validation and submission. In a record form, `mergedDynamicFormValuesIntoFieldName` names the declared JSON table field that stores the values. Put that field in a hidden section, so QQQ still returns it to the widget renderer; a field marked `isHidden` is omitted from normal record reads. The renderer should return current values in `recordOfFieldValues` for reloads. |
|
||||
| `dataBagViewer` | Data bag/version browser with raw/preview tabs and version editor when `storeDataBagVersion` is available; uses `queryParams.id` or record ID and the built-in `dataBag` and `dataBagVersion` tables. |
|
||||
| `pivotTableSetup` | Record view summarizes saved `pivotTableJson` (`rows`, `columns`, `values`). In a form, a switch and modal edit its definition after table/`columnsJson` fields are available. OK changes the form draft; Save persists it. Turning the switch off clears the stored definition when the host form is saved. |
|
||||
| `filterAndColumnsSetup` | Record view summarizes saved filter, sort and columns JSON and shows a paged, sortable, resizable preview unless `hidePreview` is set. A host form offers Edit, + Add Filters and + Add Columns controls, then opens an editor for nested filters, multiple sort levels, visible/ordered columns and a draft preview; `OK` writes JSON into the host form, `Cancel` discards the draft. When payload `allowVariables` is true, a criterion value can become a `FilterVariableExpression`; its `${VARIABLE}` chip is saved in `queryFilterJson`, and preview and the opened query page wait for a value. Saving the host record or advancing a process persists the JSON. A `WIDGET` field adornment can host the setup in a process or row-builder field; its `widgetName` names the widget and only that field receives its output. `overrideIsEditable: false` keeps it read-only. |
|
||||
| `rowBuilder` | Payload `records`, with columns from `defaultValues.frontendFields` or `fields`. Record view/dashboard is read-only; a form/process host can add, edit, delete and reorder rows when the metadata's edit flags allow it. |
|
||||
| `scriptViewer` | Script revisions, files, logs, test runner, docs and editor; uses `queryParams.id` or record ID and the built-in script tables. |
|
||||
| `ESB_OVERVIEW` | ESB destination/trigger overview; fetches its own ESB API data and does not use the ordinary widget payload for content. Requires the ESB backend module. |
|
||||
|
||||
For `scriptViewer`, pass a script ID through `queryParams.id` or host it on that script's record. The built-in script and script-type tables supply revisions, schema-ordered files, docs and logs. Register `storeScriptRevision` to expose editing and `testScript` to expose the test panel. The editor supports per-file syntax coloring, split panes, API name/version selection when those fields exist, and a guard for unsaved changes. Local suggestions include QQQ API/logger helpers, language keywords and identifiers in the current file. Type a prefix or press Ctrl+Space, use arrows to choose and Enter/Tab to insert, or click/tap a suggestion. Escape dismisses the list; Escape followed by Tab leaves the editor. Completion supports native undo/redo; the browser controls how consecutive edits are grouped. Suggestions do not validate or execute code.
|
||||
|
||||
QQQ composite block types rendered by [`QqqBlocks.tsx`](../../src/components/widgets/blocks/QqqBlocks.tsx) include `TEXT`, `BIG_NUMBER`, `UP_OR_DOWN_NUMBER`, `NUMBER_ICON_BADGE`, `ICON`, `TABLE_SUB_ROW_DETAIL_ROW`, `PROGRESS_BAR`, `DIVIDER`, `IMAGE`, `AUDIO`, `INPUT_FIELD`, and `BUTTON`. Supported layouts include `FLEX_COLUMN`, `FLEX_ROW`, `FLEX_ROW_WRAPPED`, `FLEX_ROW_SPACE_BETWEEN`, `FLEX_ROW_CENTER`, `TABLE_SUB_ROW_DETAILS`, and `BADGES_WRAPPER`. Buttons that submit process actions and standalone input submissions need a host callback. Modal control codes also work locally on dashboard composites. Unknown block types show a contained warning. See [WID-057–WID-059](../acceptance/feature-matrix.md).
|
||||
|
||||
`INPUT_FIELD` blocks read `values.fieldMetaData`, optional `value`, `placeholder`, `autoFocus`, and `submitOnEnter`. Ordinary scalar inputs retain the compact block controls; the backend's neutral `%s` display format does not change their presentation. Possible-value sources, explicit formatting/behaviors, and `CODE_EDITOR`, `FILE_UPLOAD`, or `WIDGET` adornments use the shared Next field editors. Hidden fields are omitted and read-only fields cannot be changed. Standalone metadata editors retain a 14rem minimum width so short choice labels do not collapse the control. For `STRING`, `TEXT`, and explicitly configured `PASSWORD` blocks, `fieldMetaData.behaviors` accepts `TO_UPPER_CASE` or `TO_LOWER_CASE`: text changes case while typing and the caret remains at the edit position, including conversions that expand characters such as `ß` to `SS`. Selection boundaries and direction are preserved. For single-line inputs, process Enter and Submit both send the transformed value; standalone Enter requires `submitOnEnter` and a host callback. Multiline `TEXT` fields preserve newlines: Enter edits the text even when `submitOnEnter` is true, and the process Submit button sends the transformed value. Password inputs remain masked by default; showing or hiding the value does not change it. Passwords without a case behavior retain their entered case. The behavior names come from backend metadata, so do not apply a second conversion in an action callback.
|
||||
|
||||
Numeric block controls in RC4 blur on mouse-wheel scrolling to preserve the entered value, matching shared numeric form fields. Clicking back into the control allows normal typing and arrow-key edits. Text controls keep focus when scrolling. This correction is included in published RC4 (`3c409f9`); RC3 does not contain it.
|
||||
|
||||
In a process, block editors use the screen's form, validation and submission. A BLOB or FILE_UPLOAD field sends the selected `File` as multipart content, and required files block submission until chosen. Named choices on block-only fields search their standalone possible-value source; declared process fields use the process source. WIDGET fields receive surrounding process values (for example `tableName`) merged with current form values; editing writes only the adorned field. Scalar Enter submits only when `submitOnEnter` is true; `->code` submits an action code. Multiline code, choice searches and file controls own their keyboard interactions. Standalone blocks keep their draft locally and send scalar Enter values through their action callback. PRC-062 and WID-074 cover the process and standalone fixture paths. Enter trims text before form validation and submission; required whitespace cannot advance. Process initial values come from the process state, while standalone values come from the block. Configured autofocus takes precedence over heading focus on subsequent process screens, and the first blur does not display a premature required error. PRC-063 verifies this configuration across screen transitions and checks the actual stored text.
|
||||
|
||||
Ordinary record edits omit unchanged timestamps. Base Copy and both child-row editor hosts resubmit complete values, so they retain each unchanged timestamp's source instant and fractional seconds. REC-065 verifies copy persistence during the repeated daylight-saving hour; component regressions cover association and in-process child rows.
|
||||
|
||||
`DATE_TIME` process fields and input blocks display instants in the browser's local timezone, retain seconds, and send edited times as zoned UTC values. Unchanged values retain their original instant and subsecond precision, including a repeated daylight-saving hour. `DATE` and `TIME` remain calendar and wall-clock values. PRC-064 checks seeded and edited values across plain blocks, metadata editors and regular process forms against backend SQL; WID-075 checks standalone display.
|
||||
|
||||
The filter/column editor reads the payload's `tableName` or the form's `tableName`, then loads that table's metadata. It writes to `queryFilterJson` and `columnsJson` by default; `filterFieldName` and `columnFieldName`/`columnsFieldName` override those keys. `hidePreview` controls the read-only summary preview. Starting in RC5, the editing dialog retains its query grid even when that summary is hidden, matching Material behavior; RC4 incorrectly hides both. Payload flags `hideColumns`, `hideSortBy`, `allowVariables`, `isApiVersioned`, `apiName`/`apiPath`/`apiVersion`, `filterDefaultFieldNames`, `filterDefaultFieldNameSourceFieldNames`, `omitExposedJoins`, `editButtonLabel`, and `modalHeader` affect the editor. It waits for a selected table and required default-field values. The preview supports server-side paging, sorting, column resizing, Refresh, column configuration, and column-menu actions (filter, hide, pin, copy page/full-query values, and statistics when permitted). The editor also offers density, Basic quick filters, Advanced conditions, and selection of an existing saved view. Selecting a view imports its filter and columns into the draft; it does not modify that saved view. Column widths and pins survive host Save. Unresolved variables suppress the preview; assign variables in Advanced mode. Interactions in a read-only record preview are temporary and never update the saved report. Starting in RC4, `omitExposedJoins` limits new filter/column choices in both the read-only preview and editor. Existing saved joined columns, criteria and sorts remain usable, and reordering available columns preserves omitted columns in place. Opening the editor and pressing OK preserves those saved fields; Basic/Advanced changes and host Save/reopen retain their values and widths. This is a choice restriction, not a backend permission boundary; existing joined data still uses the normal query permissions. WID-073 checks the configured and unconfigured cases against SQL, including local edits and reload. `OK` updates the host form's values, so the record must still be saved to persist. [WID-030 and WID-070](../acceptance/feature-matrix.md) cover the summary, preview, Cancel, OK, and persisted host save across five browser profiles.
|
||||
|
||||
In the report setup dialog, Left/Right Arrow switches between Filters and sort and Columns; Home/End selects the first/last available tab. Tab enters the selected panel's controls. Switching tabs preserves the draft, and metadata that hides Columns leaves only the filter tab. The tab controls expose `filter-editor-tab-<widget>-filters` and `filter-editor-tab-<widget>-columns` CSS hooks.
|
||||
|
||||
Selecting a saved view imports it into the report draft. **Reset Changes** reapplies that view; **Reset to New View** clears filters and custom quick fields, restores default columns, and sorts by the primary key descending when present. Both discard an open quick-filter edit. These actions leave the saved view unchanged. OK applies the draft to the host form; saving the form persists it. Cancel discards the dialog draft.
|
||||
|
||||
Complete conditions, including nested groups, mark the corresponding preview column header. Use that indicator to open the existing filter. It adds no condition; editing a read-only preview stays local, and editor changes remain a draft until OK and host Save. Very narrow columns retain the shared grid’s compact header treatment.
|
||||
|
||||
When a preview table declares `usesVariants`, select its backend variant before querying. The choice uses the same per-table browser storage as Record Query (`qqq.tableVariant.<table>`). Switching variants starts a fresh preview page; query/count, full-column copy and statistics use the selection. It does not alter the saved report definition. Application-API query/count require the backend fix in [QQQ PR #913](https://github.com/QRun-IO/qqq/pull/913), merged to QQQ develop as `cba758507dcc54cf282a6638ab50a7d3a1904c7c`; verification used the middleware JAR built from its reviewed source. This does not require or announce a QQQ 4.1 release.
|
||||
|
||||
|
||||
|
||||
### Demo and compatibility dispatch labels
|
||||
|
||||
The remaining dispatch labels mainly support local mock fixtures. They are implemented UI paths, but are not a promise that the production QQQ backend emits those shapes. Check [`src/mocks/fixtures/widgets`](../../src/mocks/fixtures/widgets) for demo payloads and the real widget fixtures and matrix for QQQ-backed examples.
|
||||
|
||||
| `type` | Use and important response fields |
|
||||
|---|---|
|
||||
| `recordGrid` | Demo record grid with `tableName`, `records`, and `columns` or full `fields`; optionally `totalCount`. |
|
||||
| `quickLinks` | Demo list of labeled navigation links; inspect the fixture's `links` payload before using it. |
|
||||
| `processSummary` | Demo process status/summary cards; inspect the fixture's status and counts before using it. |
|
||||
| `block` | Chooses QQQ composite blocks when the payload has `blockTypeName` or `blocks`; otherwise falls back to `BlockWidget`. A demo `blocks` list therefore enters the QQQ composite branch. |
|
||||
| `parent` | Demo `CompositeWidget` alias using `childWidgets`; canonical QQQ parent widgets use `parentWidget` and `childWidgetNameList`. |
|
||||
|
||||
Canonical chart types can also fall through to the demo chart components when the response is not a QQQ `chartData` payload.
|
||||
|
||||
### Widget configuration recipes
|
||||
|
||||
These examples are payload fragments emitted by an application's backend renderer, with illustrative names and values. Pair them with matching `QWidgetMetaData`; they are not standalone Java producer definitions. Every dispatch label is listed above; the details below cover the shared and specialized configuration that is easy to miss when copying a basic example.
|
||||
|
||||
**Responsive placement.** Metadata `defaultValues` accepts `gridCols:sizeClass:xs`, `sm`, `md`, `lg`, `xl`, and `xxl`. Their minimum viewport widths are 0, 576, 768, 992, 1200, and 1400 px. Values are column counts from 1 to 12; numeric strings are accepted. Once any override is set, `xs` defaults to 12 and `xxl` defaults to `gridColumns` or 12. Other declared spans apply until the next declared breakpoint. This example gives a half-width widget from 768 px upward, including 1400 px:
|
||||
|
||||
```json
|
||||
{"gridColumns":6,"defaultValues":{"gridCols:sizeClass:xs":12,"gridCols:sizeClass:md":6}}
|
||||
```
|
||||
|
||||
**Readable widths (Next UI).** Without per-breakpoint overrides, a widget's span follows the width of the dashboard grid itself (CSS container queries, so an open sidebar counts), not the viewport. Two `defaultValues` hints tune it, and neither needs a QQQ change:
|
||||
|
||||
- `gridMinWidth`: the narrowest the widget may be, in rem (a number, or `"28rem"` / `"448px"`). The widget shares a row only when it, and a neighbour of the same minimum, each get at least that much; otherwise it takes the full row. Values snap up to the tiers 16, 20, 24, 28, 32, 40, 48 or 64 rem; anything wider always takes the full row. Two `gridColumns: 6` widgets with `gridMinWidth: 28` pair from a 57.5rem (920 px) grid and go to thirds (`gridColumns: 4`) from 86.5rem.
|
||||
- `gridProfile`: `widgets` (charts, statistics: half from a 40rem grid), `cards` or `table` (tables: half from 66rem), or `fields` (small tiles: half from 30rem). The default is `cards` for table-type widgets and `widgets` for the rest.
|
||||
|
||||
`gridCols:sizeClass:*` wins over `gridMinWidth`, which wins over `gridProfile`.
|
||||
|
||||
```json
|
||||
{"type":"table","gridColumns":6,"defaultValues":{"gridMinWidth":28}}
|
||||
```
|
||||
|
||||
Table widgets (`table`, `multiTable`) stop growing at 96rem (1536 px) so columns stay readable on ultra-wide screens; set `defaultValues.tableMaxWidth` to another width (rem, or a CSS length in `rem`, `px`, `ch`, `em` or `%`) or to `"none"`. Where a table widget is narrower than 30rem, or than its declared column widths need (6rem per `fr` or unsized column plus the fixed widths), each row renders as a stacked card of `label: value` lines instead of a table that scrolls sideways.
|
||||
|
||||
To cap a whole dashboard page on ultra-wide screens, give the app a supplemental app metadata of type `dashboardLayout` (a backend `QSupplementalAppMetaData` subclass whose `getType()` returns `dashboardLayout` and that is included in frontend metadata) with `maxContentWidth` (rem, or a CSS length). The page then centres at that width: `{"supplementalAppMetaData":{"dashboardLayout":{"maxContentWidth":160}}}`.
|
||||
|
||||
See [`widgetColumnClasses`](../../src/components/widgets/widget-utils.ts). Parent grids use the same sizing. A `parentWidget` payload such as `{"childWidgetNameList":["totals","trend"],"layoutType":"TABS"}` resolves those names in the instance widget registry. Children inherit parent request parameters and dropdown selections and fetch their own data; tab selection is remembered locally. A child omitted from permitted metadata cannot be configured into existence by listing its name.
|
||||
|
||||
**Dropdown data.** Metadata and response lists are paired by position; keep their lengths and order aligned. For the `region` dropdown example above, a response fragment is:
|
||||
|
||||
```json
|
||||
{"dropdownNameList":["regions"],"dropdownLabelList":["Region"],"dropdownDataList":[[{"id":"east","label":"East"},{"id":"west","label":"West"}]],"dropdownDefaultValueList":["east"]}
|
||||
```
|
||||
|
||||
The request parameter is the payload's `dropdownNameList` entry (`regions` here). The backend convention uses the possible-value source name for choice controls and the declared name for date controls. A valid stored choice wins over the response default; unavailable stored choices are cleared. `labelForNullValue` adds an explicit empty option only when the payload has no default. `disableClearable` removes the clear button. To gate the body, return `dropdownNeedsSelectedText` until the required choice is supplied. Metadata `isRequired` and `defaultValue` alone are not consumed as client-side gating/default rules by [`ConnectedWidget`](../../src/components/widgets/ConnectedWidget.tsx). A date control sends local `YYYY-MM-DD` text; the special custom timeframe sends the UTC range described above.
|
||||
|
||||
**Charts and statistics.** All seven chart dispatch labels use the canonical shape below. `backgroundColors` and `urls` are arrays aligned with data points; `color` supplies the series color and `backgroundColor` is the stacked-series fallback. Top-level `chartData.urls` supplies category links. `height` is numeric; `isCurrency` or `isYAxisCurrency` enables the renderer's currency formatting.
|
||||
|
||||
```json
|
||||
{"type":"barChart","height":280,"isCurrency":true,"chartData":{"labels":["Open","Closed"],"datasets":[{"label":"Value","data":[1200,800],"backgroundColors":["info","success"],"urls":["/app/orders/","/app/archive/"]}]}}
|
||||
```
|
||||
|
||||
`chartSubheaderData` accepts `mainNumber`, `vsPreviousPercent`, `vsPreviousNumber`, `isUpVsPrevious`, `isGoodVsPrevious`, `vsDescription`, `mainNumberUrl`, and `previousNumberUrl`; use it for the optional comparison summary. See [`QqqChartPayload`](../../src/components/widgets/widget-types.ts). Statistics adds `countFontSize`, `countURL`, `percentageURL`, and `isCurrency` to the basic example; numeric currency counts format as USD, while string counts pass through. A zero or absent percentage hides the change row. Multi-statistics uses this nested shape:
|
||||
|
||||
```json
|
||||
{"type":"multiStatistics","statisticsGroupData":[{"header":"Orders","subheader":"Today","icon":"inventory_2","iconColor":"#0062ff","statisticList":[{"label":"Open","value":7,"url":"/app/orders/"}]}]}
|
||||
```
|
||||
|
||||
**Tables.** Each `table` payload (and each member of `multiTable.tableDataList`) uses the same configuration. `rowsPerPage` defaults to 10; `hidePaginationDropdown` hides its size selector, `fixedHeight` constrains the scroll area, and `fixedStickyLastRow` fixes the final row. `linkText`/`linkURL` add the table link. Columns accept `type`, `header`, `accessor`, `width`, `align`, and `verticalAlign`; widths can be fixed CSS lengths or `fr` shares.
|
||||
|
||||
```json
|
||||
{"type":"table","rowsPerPage":5,"columns":[{"header":"Name","accessor":"name","width":"2fr"},{"header":"Count","accessor":"count","width":"1fr"},{"accessor":"tooltip","type":"hidden"}],"rows":[{"name":"Open","count":7,"tooltip":"Current backlog","subRows":[{"name":"Priority","count":2}]}]}
|
||||
```
|
||||
|
||||
Cell `type` is `default`, `html`, `htmlAndTooltip`, `composite`, `block`, `image`, or `hidden`. An `htmlAndTooltip` cell reads the row's `tooltip`; `image` reads `imageUrl`, `imageLabel`, `imageTotal`, and `imageTotalType`. Composite/block cells read a block payload at the column accessor. Hidden helper columns do not display but remain available to cells and export. `subRows` provides expandable rows. See [`QqqTablePayload`](../../src/components/widgets/QqqTableWidget.tsx) and [`TableWidgetColumn`](../../src/components/widgets/table-widget-utils.ts).
|
||||
|
||||
**Display and editor details.** These keys supplement each catalog row; they are read by the linked implementations.
|
||||
|
||||
| Widget | Additional configuration |
|
||||
|---|---|
|
||||
| `stepper` | Each step accepts `label`, `linkText`, `linkURL`, `iconOverride`, and `colorOverride`; only the active step shows its action link. |
|
||||
| `fieldValueList` | Use `fieldLabelPrefixIconNames`, `fieldLabelPrefixIconColors`, and `fieldIndentLevels`, keyed by field name. Fields use the shared record formatter, including declared formats/adornments and possible-value record links. |
|
||||
| `alert` | `alertType` supports ERROR, WARNING, SUCCESS and INFO; absent/unknown values use INFO. `bulletList` contains sanitized HTML strings. Empty `html` renders no alert body. |
|
||||
| `usaMap` | `mapMarkerList` entries use `name`, `latitude`, and `longitude`; the UI includes the US basemap and an accessible location list. |
|
||||
| `html`, `generic`, `divider`, `location`, `quickSightChart` | Use the fields in the catalog and shared chrome; these types do not expose host-form editing. QuickSight's optional payload `label` names the iframe. |
|
||||
| `childRecordList` | `omitFieldNames` and `onlyIncludeFieldNames` control columns. `canAddChildRecord` enables Add; `disabledFieldsForNewChildRecords` locks named defaults, and `defaultValuesForNewChildRecordsFromParentFields` maps child field names to parent field names. `totalRows` supplies the total. A process host uses `isInProcess`, `allowRecordEdit`, and `allowRecordDelete` to control in-memory rows. |
|
||||
| `rowBuilder` | Metadata defaults include `frontendFields` (fallback `fields`), `associationName`/`outputFieldName`, `orderByFieldName`, `mayReorderRows`, `isForRecordViewAndEditScreen`, `isEditable`, `useModalEditor`, `inlineHeading`, `modalTitle`, and `defaultValuesForNewRowsFromParentRecord`; see the [agent editor guide](./next-ui-agent-guide.md#widget-editors-and-host-forms). |
|
||||
| `process` | `processMetaData.name` selects the process; payload `defaultValues` presets its inputs. Reloading the widget reinitializes the embedded run. |
|
||||
| `cronUI`, `dynamicForm`, `pivotTableSetup`, `filterAndColumnsSetup` | These edit host fields through their documented bindings above; changing the widget draft alone does not save a record. Use the shared form context and inspect each editor's validation before introducing a new field binding. |
|
||||
| `dataBagViewer`, `scriptViewer`, `ESB_OVERVIEW`, `customComponent` | These require their respective backend module/process or trusted bundle described above; ordinary display payloads cannot replace those dependencies. |
|
||||
| `recordGrid`, `quickLinks`, `processSummary`, `parent`, demo `block` | Use the [demo fixtures](../../src/mocks/fixtures/widgets) and matching renderer for the demo contract. These labels do not imply an equivalent canonical Java renderer. |
|
||||
|
||||
**Composite blocks.** `link` and `tooltip` apply to the block; `linkMap` and `tooltipMap` apply to named slots, such as `NUMBER`. Links carry `href` and optional `target`; tooltips carry `title`, `placement`, or nested `blockData`. Help slots can provide fallback tooltip text. `styles` configures the leaf block, while composites also accept `styleOverrides`, sanitized `overlayHtml`, and `overlayStyleOverrides`.
|
||||
|
||||
| `blockTypeName` | Values and styles to supply |
|
||||
|---|---|
|
||||
| `TEXT` | `values.text`, optional `startIcon`/`endIcon`; `styles.color`, `format`, `size`, `weight`. |
|
||||
| `NUMBER_ICON_BADGE` | `values.number`, `iconName`. |
|
||||
| `UP_OR_DOWN_NUMBER` | `values.number`, `context`, `isUp`, `isGood`; `styles.colorOverride`, `isStacked`. |
|
||||
| `TABLE_SUB_ROW_DETAIL_ROW` | `values.label`, `value`; `styles.labelColor`, `valueColor`. |
|
||||
| `PROGRESS_BAR` | `values.percent`, `heading`, `value`; `styles.barColor`. |
|
||||
| `BIG_NUMBER` | `values.heading`, `number`, `context`; `styles.width`, `numberColor`. |
|
||||
| `DIVIDER` | No values required. |
|
||||
| `IMAGE` | `values.path`, `alt`; `styles.width`, `height`, `bordered`. |
|
||||
| `AUDIO` | `values.path`, `showControls`, `autoPlay`; playback still follows browser policy. |
|
||||
| `ICON` | `values.name`; `styles.color`, `fontSize`. |
|
||||
| `BUTTON` | `values.label`, `actionCode` or `controlCode`, optional `startIcon`/`endIcon`; `styles.format` is filled, outlined or text. |
|
||||
| `INPUT_FIELD` | `values.fieldMetaData`, `value`, `placeholder`, `autoFocus`, `submitOnEnter`; host behavior is detailed above. |
|
||||
| `COMPOSITE` | Nested `blocks`, one of the documented `layout` values, and optional `blockId`/`modalMode`. |
|
||||
|
||||
A host evaluates `conditional` as the truthiness of the named host value and interpolates `${name}` in text; it is not a JavaScript expression. Dashboard composites without a process host do not evaluate those conditions. Modal buttons use `showModal:<blockId>`, `hideModal:<blockId>`, or `toggleModal:<blockId>` control codes. A nonempty `modalMode` marks the target composite; a host value with that block ID set to literal `true` opens it initially. This local dashboard example needs no process submission callback:
|
||||
|
||||
```json
|
||||
{"type":"composite","blocks":[{"blockTypeName":"BUTTON","values":{"label":"Details","controlCode":"showModal:details"}},{"blockTypeName":"COMPOSITE","blockId":"details","modalMode":"dialog","blocks":[{"blockTypeName":"TEXT","values":{"text":"Details appear here."}}]}]}
|
||||
```
|
||||
|
||||
See [`QqqComposite`](../../src/components/widgets/blocks/QqqComposite.tsx), [`QqqBlocks`](../../src/components/widgets/blocks/QqqBlocks.tsx), and [`BlockSlot`](../../src/components/widgets/blocks/BlockSlot.tsx). RC6 icon compatibility preserves existing Lucide mappings and adds a locally served glyph for valid unmapped names from the pinned 2,234-name legacy Material Icons inventory. Examples include `3d_rotation`, `account_balance_wallet`, `battery_6_bar`, and `60fps`. Unknown names retain their fallback; this does not include every Material Symbols name or distinct outlined/rounded font variants. Metadata image paths still take precedence. The font and Apache license ship with the static export/JAR; no Google Fonts request is needed at runtime. See the [font provenance](../../public/fonts/material-icons/README.md). This behavior first ships in RC6. The parity ledger also retains open INPUT_FIELD configuration and visual review work.
|
||||
|
||||
## 8. Theme, branding, and customization
|
||||
|
||||
`QInstance.branding` supplies app/company name, logo/icon, accent colors, banners and optional `gravatarDefault`. A company footer appears when both `companyName` and a safe HTTP(S) `companyUrl` are present. The active application theme is `QInstance.supplementalInstanceMetaData.materialDashboardTheme`, read by [`material-theme.ts`](../../src/lib/theme/material-theme.ts); do not configure `QInstance.theme` and expect these styles to apply. Its optional fields cover palette, fonts, typography, radii, density, logo/icon/favicon, icon style, branded header, app bar, sidebar, table colors, and `customCss`. Invalid theme values are ignored. For example:
|
||||
|
||||
```json
|
||||
{"supplementalInstanceMetaData":{"materialDashboardTheme":{"primaryColor":"#245ca6","fontFamily":"Inter, sans-serif","borderRadiusCard":"12px","brandedHeaderEnabled":true,"brandedHeaderTagline":"Operations","customCss":"[data-qqq-id='sidenav-root'] { border-right: 1px solid #ccc; }"}}}
|
||||
```
|
||||
|
||||
[`apply-branding.ts`](../../src/lib/theme/apply-branding.ts), [`material-theme.ts`](../../src/lib/theme/material-theme.ts), [`tokens.ts`](../../src/lib/theme/tokens.ts), and [`qqq-theme.css`](../../src/styles/qqq-theme.css) define the CSS mapping, including supported Material `--qqq-*` variables. Scope custom CSS to observed `data-qqq-id` attributes, and inspect actual rendering and contrast before migrating a Material stylesheet. A declared application theme sets the app look and keeps light mode active. Without an application theme, the local display-mode preference applies; a fresh profile starts in light mode regardless of the operating system preference. Density is available through theme metadata and local preferences. Open the sidebar user menu → Preferences → Appearance to choose Light or Dark. Changes apply immediately and persist in `localStorage` as `qqq-dark-mode` for this browser profile and origin (scheme, host and port), not the signed-in account; there is no cross-device sync or operating-system mode. Clearing site data removes the preference. When an application theme is active, the choices are disabled with an explanation and the saved preference is retained. Reset to Defaults resets appearance to light along with table and record defaults.
|
||||
|
||||
See the [Material CSS hook guide](../CSS-HOOKS.md) for the ID sanitizer, selector patterns, named hooks, and phone/tablet visibility rules.
|
||||
|
||||
Analytics is configured by the backend's `QInstance.environmentValues` ([`src/lib/analytics`](../../src/lib/analytics)). It stays off when no analytics values are published. `ANALYTICS_PROVIDERS` can name Google, PostHog, or registered plugins; provider-specific IDs and enable flags are also required. The default privacy behavior omits record IDs, labels and query strings and does not identify users. Only publish `ANALYTICS_INCLUDE_RECORD_DATA=true` or `ANALYTICS_IDENTIFY_USERS=true` after deciding those disclosures are appropriate for the application.
|
||||
|
||||
### Theme property reference
|
||||
|
||||
All keys below belong to `supplementalInstanceMetaData.materialDashboardTheme`. Unset properties retain the Next defaults. The executable allow-list and value validation are [`THEME_PROPERTY_KINDS`](../../src/lib/theme/material-theme.ts); [`QThemeMetaData`](../../src/types/metadata.ts) defines their types.
|
||||
|
||||
| Area | Keys or key pattern |
|
||||
|---|---|
|
||||
| Palette | `primaryColor`, `secondaryColor`, `backgroundColor`, `surfaceColor`, `textPrimary`, `textSecondary`, `errorColor`, `warningColor`, `successColor`, `infoColor`. |
|
||||
| Action color | `preferInfoColorToPrimaryColor` chooses the info color for the controls that honor that option. |
|
||||
| Fonts | `fontFamily`, `headerFontFamily`, `monoFontFamily`, `fontSizeBase`, `fontWeightLight`, `fontWeightRegular`, `fontWeightMedium`, `fontWeightBold`. |
|
||||
| Typography | `typography{variant}{property}`, where variant is `H1`–`H6`, `Body1`, `Body2`, `Button`, or `Caption`, and property is `FontSize`, `FontWeight`, `LineHeight`, `LetterSpacing`, or `TextTransform`. Example: `typographyH2FontSize: "1.5rem"`. |
|
||||
| Radii | `borderRadiusGlobal`, `borderRadiusScale`, and `borderRadius{component}` for `Button`, `Card`, `Chip`, `Dialog`, `OutlinedInput`, `LinearProgress`, `MenuPaper`, `PaperRounded`, `PopoverPaper`, and `Tooltip`. A component radius overrides the global radius; the scale applies when no global radius is set. |
|
||||
| Density and icons | `density`: `compact`, `normal`, `comfortable`; `iconStyle`: `filled`, `outlined`, `rounded`, `sharp`, `two-tone`. Unsupported icon names can still fall back to the default glyph. |
|
||||
| Images and CSS | `logoPath`, `iconPath`, `faviconPath`, `customCss`. |
|
||||
| Branded header | `brandedHeaderEnabled`, `brandedHeaderBackgroundColor`, `brandedHeaderTextColor`, `brandedHeaderLogoPath`, `brandedHeaderLogoAltText`, `brandedHeaderHeight`, `brandedHeaderTagline`. |
|
||||
| App bar | `appBarBackgroundColor`, `appBarTextColor`. |
|
||||
| Sidebar | `sidebarBackgroundColor`, `sidebarTextColor`, `sidebarIconColor`, `sidebarSelectedBackgroundColor`, `sidebarSelectedTextColor`, `sidebarHoverBackgroundColor`, `sidebarDividerColor`. |
|
||||
| Tables | `tableHeaderBackgroundColor`, `tableHeaderTextColor`, `tableRowHoverColor`, `tableRowSelectedColor`, `tableBorderColor`. |
|
||||
| Borders | `dividerColor`, `borderColor`, `cardBorderColor`. |
|
||||
|
||||
Lengths use CSS length strings; colors use supported named, hex, RGB or HSL values. Font weights must be integers from 1 through 1000; radius scale is 0 through 10. Text transforms are `none`, `uppercase`, `lowercase`, or `capitalize`. Invalid values are ignored rather than inserted into CSS. Application CSS and external assets remain trusted configuration, and their actual appearance/CSP must be checked on the host.
|
||||
|
||||
### Help content configuration
|
||||
|
||||
Fields and sections expose `helpContents`; widgets expose a slot map in `helpContent`. Entries supply `content`, `format` (`TEXT`, `HTML`, or `MARKDOWN`), and optional `roles`. The shared selector prefers the first entry matching the most specific screen role, then an entry without roles. View roles are `VIEW_SCREEN`, `READ_SCREENS`, `ALL_SCREENS`; query substitutes `QUERY_SCREEN`; edit uses `EDIT_SCREEN`, `WRITE_SCREENS`, `ALL_SCREENS`; create/copy substitutes `INSERT_SCREEN`; processes use `PROCESS_SCREEN`, `ALL_SCREENS`.
|
||||
|
||||
For example, widget help can use `{"helpContent":{"label":[{"content":"Current totals","format":"TEXT"}],"columnHeader=count":[{"content":"Matching records","format":"TEXT"}]}}`. Table widget headers use `columnHeader=<accessor>` slots; composite slots use `<blockId>,<slot>` keys. Turn on `?helpHelp` to inspect the exact key at the target control before defining content. See [`help-utils.ts`](../../src/lib/utils/help-utils.ts) and [`widgetSlotHelp`](../../src/components/widgets/widget-utils.ts).
|
||||
|
||||
### Analytics configuration example
|
||||
|
||||
Publish these values through `QInstance.environmentValues`, not frontend build variables. Google uses `GOOGLE_ANALYTICS_ENABLED="true"` and `GOOGLE_ANALYTICS_TRACKING_ID` (a valid GA4 tracking ID). PostHog uses `POSTHOG_ENABLED="true"`, `POSTHOG_API_KEY` (alias `POSTHOG_PROJECT_API_KEY`), and optional `POSTHOG_HOST`, which defaults to `https://us.i.posthog.com`.
|
||||
|
||||
```json
|
||||
{"environmentValues":{"ANALYTICS_PROVIDERS":"google,posthog","GOOGLE_ANALYTICS_ENABLED":"true","GOOGLE_ANALYTICS_TRACKING_ID":"G-EXAMPLE123","POSTHOG_ENABLED":"true","POSTHOG_PROJECT_API_KEY":"<public-project-key>","ANALYTICS_INCLUDE_RECORD_DATA":"false","ANALYTICS_IDENTIFY_USERS":"false"}}
|
||||
```
|
||||
|
||||
Replace the illustrative IDs with the application's public provider identifiers. `ANALYTICS_PROVIDERS` accepts comma-, semicolon-, or newline-separated names; names are lowercased. If omitted, Google and PostHog are considered, but each still requires its own enable/configuration values. `ANALYTICS_PLUGIN_SCRIPTS` (alias `ANALYTICS_PLUGIN_SCRIPT_URLS`) loads configured provider scripts; those register through `window.QQQAnalytics`. Use the [provider registry](../../src/lib/analytics/registry.ts) and [provider interface](../../src/lib/analytics/types.ts), and configure host CSP for each script/endpoint. Privacy defaults and opt-ins are described above; never place server-side provider secrets in environment values exposed to browsers.
|
||||
|
||||
## 9. Accessibility, testing, and known boundaries
|
||||
|
||||
Components use labeled fields, keyboard navigation, focus-managed dialogs and drawers, inline errors, contained widget failures, and touch layouts. Below 768 px, queries become record cards and filters/actions use sheets; tablet testing uses a touch WebKit project. Test with keyboard and assistive technology in addition to axe. See [browser matrix](../acceptance/browser-matrix.md) and [accessibility rows](../acceptance/feature-matrix.md).
|
||||
|
||||
Run `pnpm typecheck`, `pnpm lint`, `pnpm test`, `pnpm test:e2e`, `pnpm build:export`, and `pnpm perf:budget` for the relevant change. For RC1, prioritize complete Chromium workflows and the full Chromium gate before expanding browser/viewport coverage; verify defaults, validation, save/reopen and persisted results. The final release acceptance gate is `QQQ_ACCEPTANCE_BROWSERS=chromium,firefox,webkit,mobile,tablet pnpm test:acceptance` with `QQQ_SAMPLE_JAR` pointing at a compatible real sample build. It verifies SQL/backend behavior as well as browser behavior; mocked tests are not a substitute. See [test setup](../../tests/acceptance/README.md), [performance budget](../acceptance/performance.md), and [API error handling](../API-ERROR-HANDLING.md).
|
||||
|
||||
Current limits to account for: the Material parity ledger has open Partial/Missing rows beyond the narrower feature matrix; the report filter/column editor supports variables and API-versioned metadata but still needs complete embedded-query equivalence review; a built-in QFMD bridge does not prove every third-party Material bundle works; root-path static hosting is the documented deployment; and Google Picker requires a backend with the #704 metadata/CSP changes (backend PR #908 merged to develop as `bac663044`). Real Auth0 tenant, Google account, and AWS QuickSight service tests have approved external-service exclusions in the feature matrix. Validate any additional feature against the current branch, backend contract, and matrix before describing it as production-compatible.
|
||||
|
||||
|
||||
### Google Drive process configuration
|
||||
|
||||
This implementation is part of work after the immutable RC1 release. RC1 itself retains the disabled picker placeholder; the final 1.0 implementation requires the matching backend changes below.
|
||||
|
||||
Set the public browser OAuth client ID and browser API key as `GOOGLE_APP_CLIENT_ID`
|
||||
and `GOOGLE_APP_API_KEY` in the QInstance environment (normally `QQQ_ENV_` environment
|
||||
variables). Enable the Google Picker and Drive APIs and configure the application's
|
||||
JavaScript origin in Google Cloud. Restrict the API key to the required APIs and website
|
||||
origins, including `https://docs.google.com/*` as required by the
|
||||
[Google Picker setup guide](https://developers.google.com/workspace/drive/picker/guides/web-picker).
|
||||
Client secrets are not browser configuration and are not published in v1 metadata.
|
||||
|
||||
The Next UI middleware must include the #704 changes (backend PR #908, merged to develop as `bac663044`) that publish these two settings
|
||||
and permit Google's SDK and picker origins in CSP only when both are configured.
|
||||
Those conditional headers apply to the Javalin-hosted static export. The standalone
|
||||
Node/container host also derives them when the backend publishes `dashboardCspSources`
|
||||
in authentication metadata. With an older backend that omits that optional field, supply
|
||||
`QQQ_DASHBOARD_CSP_SOURCES` at runtime. Merge these sources with any other deployment
|
||||
additions and start through [`standalone/qqq-server.mjs`](../../standalone/qqq-server.mjs):
|
||||
|
||||
```bash
|
||||
export QQQ_DASHBOARD_CSP_SOURCES='script-src https://accounts.google.com/gsi/client https://apis.google.com; connect-src https://accounts.google.com/gsi/; frame-src https://accounts.google.com/gsi/ https://docs.google.com; style-src https://accounts.google.com/gsi/style'
|
||||
```
|
||||
|
||||
This matches the #704 backend directive additions. The standalone parser adds these to
|
||||
its base policy; custom reverse proxies/hosts must supply equivalent allowances. See
|
||||
[`security-headers.mjs`](../../standalone/security-headers.mjs). Keep the metadata client
|
||||
ID/API key configured too: CSP permissions alone do not enable the picker.
|
||||
|
||||
The component preserves Material's
|
||||
shared-drive folder view and `drive` OAuth scope. It loads the official SDKs before the
|
||||
button becomes ready, supports retry after loading or authorization errors, rejects files,
|
||||
and requires selection again if authorization expires before submission. Tokens remain
|
||||
in screen memory and are submitted to the backend process; they are not persisted in
|
||||
localStorage or sessionStorage. Without configuration the picker stays disabled while
|
||||
other inputs remain usable. PRC-065 verifies the browser/backend contract with controlled
|
||||
Google SDK responses; PRC-039 retains the approved real-account testing exclusion.
|
||||
|
||||
|
||||
### Post-RC4 table appearance correction
|
||||
|
||||
RC5 restores the original Next uniform row background; RC4 has alternating row shading. Selection highlighting and opaque pinned cells are retained. Applications can target `data-row-parity="odd"` or `"even"` through their configured CSS if they explicitly want alternating colors.
|
||||
|
||||
### API playground theme integration (RC7)
|
||||
|
||||
The embedded RapiDoc viewer uses its own shadow DOM. Next applies existing dashboard color tokens to its dark accent/inverse pair and light contact/status colors, preserving the surrounding layout. Preferences theme changes apply without reloading the page. Focused Chromium desktop/mobile checks cover the fixture's visible text, hovered controls and live theme changes; expanded endpoint states, arbitrary custom palettes and narrow-screen inner layout still require review. This correction is not included in RC6.
|
||||
|
||||
### TIME block correction (RC7)
|
||||
|
||||
Plain process and standalone TIME blocks now use a one-second native input step, matching regular/shared form fields. This permits second-level clock values without a minute-step validation error and makes native stepping advance one second. TIME values remain local clock strings, with no timezone conversion. This correction is published in RC7 at `f65cf6b`; earlier RCs remain unchanged.
|
||||
|
||||
|
||||
### Narrow API controls and LONG validation (RC8)
|
||||
|
||||
The API specification Download and View actions fit narrow panels, including 320px phones, and wrap their labels when necessary. Their download/new-tab behavior is preserved. On wide panels, buttons grow to fit the rendered font instead of clipping labels at a fixed width. This correction is not included in RC7.
|
||||
|
||||
When the frontend receives LONG range bounds, invalid integer text produces a field error before range comparisons run. Integer-string bounds retain exact precision beyond JavaScript's safe-number range; optional blanks and required-field messages retain their existing behavior. This describes the frontend schema's supported inputs, not a new backend metadata contract. RC8 includes this correction; RC7 does not.
|
||||
@@ -0,0 +1,237 @@
|
||||
# Project Overview: QQQ Admin UI Modernization
|
||||
|
||||
## Technology Stack
|
||||
|
||||
- **Framework:** Next.js 15+ (App Router) with React 19
|
||||
- **Language:** TypeScript 5.x (strict mode)
|
||||
- **Styling:** Tailwind CSS 4.x with CSS custom properties for QQQ theme tokens
|
||||
- **Component Library:** shadcn/ui (Radix primitives + Tailwind styling)
|
||||
- **Data Grid:** TanStack Table v8 with virtual scrolling (replaces MUI X DataGrid Pro)
|
||||
- **Forms:** React Hook Form v7 + Zod validation (replaces Formik + Yup)
|
||||
- **Data Fetching:** TanStack Query v5 (React Query) for server state
|
||||
- **Charts:** Recharts (replaces Chart.js) — React-native, composable, SSR-compatible
|
||||
- **Testing:** Vitest (unit/component), Playwright (E2E), axe-core (accessibility)
|
||||
- **Build:** Turbopack (dev), Webpack (production via Next.js)
|
||||
- **Linting:** ESLint + Prettier with project-standard config
|
||||
- **Package Manager:** pnpm
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
qqq-frontend-next/
|
||||
├── src/
|
||||
│ ├── app/ # Next.js App Router
|
||||
│ │ ├── (auth)/ # Auth route group (no layout)
|
||||
│ │ │ ├── login/page.tsx # Login page
|
||||
│ │ │ └── callback/page.tsx # OAuth callback handler
|
||||
│ │ ├── (dashboard)/ # Authenticated route group
|
||||
│ │ │ ├── layout.tsx # Dashboard layout (sidebar + nav + banners)
|
||||
│ │ │ └── app/ # All app routes
|
||||
│ │ │ ├── [appName]/
|
||||
│ │ │ │ └── page.tsx # AppHome (dashboard/widgets)
|
||||
│ │ │ ├── [tableName]/
|
||||
│ │ │ │ ├── page.tsx # RecordQuery (list)
|
||||
│ │ │ │ ├── savedView/[viewId]/page.tsx # Saved view query
|
||||
│ │ │ │ ├── create/page.tsx # Create record
|
||||
│ │ │ │ ├── key/page.tsx # View by unique key
|
||||
│ │ │ │ ├── dev/page.tsx # Table developer view
|
||||
│ │ │ │ └── [recordId]/
|
||||
│ │ │ │ ├── page.tsx # RecordView
|
||||
│ │ │ │ ├── edit/page.tsx # Edit record
|
||||
│ │ │ │ ├── copy/page.tsx # Copy record
|
||||
│ │ │ │ ├── dev/page.tsx # Record developer view
|
||||
│ │ │ │ └── createChild/[childTable]/page.tsx
|
||||
│ │ │ └── [processName]/
|
||||
│ │ │ └── page.tsx # ProcessRun (standalone)
|
||||
│ │ ├── layout.tsx # Root layout (providers, global styles)
|
||||
│ │ ├── not-found.tsx # 404 page
|
||||
│ │ └── error.tsx # Global error boundary
|
||||
│ ├── components/
|
||||
│ │ ├── ui/ # shadcn/ui primitives (Button, Input, Dialog, etc.)
|
||||
│ │ ├── layout/ # Sidebar, Navbar, Breadcrumbs, BrandedHeader, Footer
|
||||
│ │ ├── records/ # RecordView, FieldValue, RecordSidebar
|
||||
│ │ ├── forms/ # DynamicForm, EntityForm, field-type components
|
||||
│ │ ├── query/ # DataGrid, FilterBuilder, ColumnConfig, Pagination, Export
|
||||
│ │ ├── process/ # StepWizard, ValidationReview, BulkLoad, ProcessResults
|
||||
│ │ ├── widgets/ # Widget, DashboardWidgets, all chart/block/grid widgets
|
||||
│ │ └── feedback/ # Alerts, Toasts, Modals, HelpContent, CommandMenu
|
||||
│ ├── lib/
|
||||
│ │ ├── api/ # Typed API client modules
|
||||
│ │ │ ├── client.ts # Base axios/fetch client with auth interceptor
|
||||
│ │ │ ├── auth.ts # getAuthenticationMetaData, manageSession, logout
|
||||
│ │ │ ├── metadata.ts # loadMetaData, loadTableMetaData, loadProcessMetaData
|
||||
│ │ │ ├── tables.ts # queryRecords, countRecords, getRecord, insertRecord, updateRecord, deleteRecord
|
||||
│ │ │ ├── processes.ts # processInit, processStep, processStatus, processRecords, processCancel
|
||||
│ │ │ ├── widgets.ts # fetchWidgetData
|
||||
│ │ │ ├── possible-values.ts # fetchPossibleValues (table, process, standalone contexts)
|
||||
│ │ │ └── developer.ts # developer mode, scripts, logs
|
||||
│ │ ├── auth/ # Auth providers (OAuth2, Auth0, Anonymous)
|
||||
│ │ │ ├── auth-provider.tsx # AuthContext + provider component
|
||||
│ │ │ ├── use-auth.ts # useAuth() hook
|
||||
│ │ │ ├── oauth2.ts # OAuth2/OIDC flow
|
||||
│ │ │ ├── auth0.ts # Auth0 integration
|
||||
│ │ │ └── anonymous.ts # Anonymous/mock auth
|
||||
│ │ ├── hooks/ # Shared React hooks
|
||||
│ │ │ ├── use-metadata.ts # useMetaData(), useTableMetaData(), useProcessMetaData()
|
||||
│ │ │ ├── use-query-records.ts # useQueryRecords() with TanStack Query
|
||||
│ │ │ ├── use-record.ts # useRecord() single record fetch
|
||||
│ │ │ ├── use-process.ts # useProcess() step machine
|
||||
│ │ │ ├── use-widget.ts # useWidget() with abort/cancel
|
||||
│ │ │ ├── use-possible-values.ts # usePossibleValues() with debounce
|
||||
│ │ │ ├── use-saved-views.ts # useSavedViews() CRUD
|
||||
│ │ │ └── use-local-storage.ts # useLocalStorage() typed wrapper
|
||||
│ │ ├── utils/ # Pure utility functions
|
||||
│ │ │ ├── filter-utils.ts # QQueryFilter builders and validators
|
||||
│ │ │ ├── value-utils.ts # Display value formatting per field type
|
||||
│ │ │ ├── table-utils.ts # Table section/field helpers
|
||||
│ │ │ ├── process-utils.ts # Process navigation helpers
|
||||
│ │ │ ├── export-utils.ts # CSV/Excel export
|
||||
│ │ │ └── responsive-utils.ts # Breakpoint helpers
|
||||
│ │ └── theme/ # Theming system
|
||||
│ │ ├── theme-provider.tsx # ThemeContext (light/dark, accent colors)
|
||||
│ │ ├── tokens.ts # CSS custom property definitions (60+ tokens)
|
||||
│ │ └── tailwind-plugin.ts # Tailwind plugin for QQQ theme tokens
|
||||
│ ├── types/ # TypeScript type definitions
|
||||
│ │ ├── index.ts # Re-exports all types
|
||||
│ │ ├── metadata.ts # QInstance, QTableMetaData, QFieldMetaData, QProcessMetaData, etc.
|
||||
│ │ ├── records.ts # QRecord, QPossibleValue
|
||||
│ │ ├── query.ts # QQueryFilter, QFilterCriteria, QCriteriaOperator, etc.
|
||||
│ │ ├── processes.ts # QJobStarted, QJobRunning, QJobComplete, QJobError
|
||||
│ │ ├── widgets.ts # Widget data types
|
||||
│ │ └── enums.ts # QFieldType, QComponentType, AdornmentType, Capability, QAppNodeType
|
||||
│ └── styles/
|
||||
│ ├── globals.css # Tailwind directives + CSS custom properties
|
||||
│ └── qqq-theme.css # QQQ-specific theme tokens
|
||||
├── public/ # Static assets
|
||||
├── .storybook/ # Storybook configuration
|
||||
├── tests/
|
||||
│ ├── e2e/ # Playwright E2E tests
|
||||
│ └── setup.ts # Vitest setup
|
||||
├── next.config.ts # Next.js configuration
|
||||
├── tailwind.config.ts # Tailwind configuration
|
||||
├── tsconfig.json # TypeScript config (strict: true)
|
||||
├── vitest.config.ts # Vitest configuration
|
||||
├── playwright.config.ts # Playwright configuration
|
||||
└── package.json
|
||||
```
|
||||
|
||||
## Dependency Graph
|
||||
|
||||
```
|
||||
Package 1 (Scaffold + Auth + Layout Shell)
|
||||
│
|
||||
├──→ Package 2 (Record Query)
|
||||
│ │
|
||||
│ ├──→ Package 4 (Process Execution) ←── Package 3 (Record View + CRUD)
|
||||
│ │ │
|
||||
│ └──→ Package 5 (Dashboard + Widgets) ←──────┘
|
||||
│
|
||||
└──→ Package 3 (Record View + CRUD)
|
||||
│
|
||||
└──→ Package 6 (Polish + Parity) [depends on ALL above]
|
||||
```
|
||||
|
||||
Packages 2 and 3 can execute in parallel after Package 1 completes.
|
||||
Package 4 requires form components from Package 3.
|
||||
Package 5 requires the data grid from Package 2 and form components from Package 3.
|
||||
Package 6 requires everything.
|
||||
|
||||
## Work Package Summary
|
||||
|
||||
| Package | Name | Est. Duration | Prerequisites | Key Deliverables |
|
||||
|---------|------|---------------|---------------|------------------|
|
||||
| 1 | Scaffold + Auth + Layout Shell | 4 weeks | None | Project setup, types, API client, auth flow, sidebar/nav, theming, placeholder routes |
|
||||
| 2 | Record Query | 6 weeks | Package 1 | Data grid, filters (basic/advanced), column config, pagination, saved views, export |
|
||||
| 3 | Record View + CRUD | 6 weeks | Package 1 | Record detail view, entity form (all 12 field types), create/edit/copy/delete |
|
||||
| 4 | Process Execution | 5 weeks | Packages 1, 3 | Step wizard, async job polling, bulk load, validation review, process results |
|
||||
| 5 | Dashboard + Widgets | 4 weeks | Packages 1, 2, 3 | App home, widget system, charts, statistics, record grid widget, block types |
|
||||
| 6 | Polish + Parity | 5 weeks | All above | Responsive audit, a11y, performance, Storybook, E2E tests, dev tools, command palette |
|
||||
|
||||
## Shared Conventions
|
||||
|
||||
### Metadata-Driven Rendering
|
||||
|
||||
- ALL components that render from metadata MUST accept the metadata type as a prop — never fetch it internally
|
||||
- No hardcoded table names, field names, or process names anywhere in the UI code
|
||||
- All user-facing text from metadata uses the `label` property, never `name`
|
||||
- UI structure (navigation, pages, forms, columns) derives entirely from backend metadata
|
||||
|
||||
### API Integration
|
||||
|
||||
- All API calls go through typed client functions in `src/lib/api/` — never raw `fetch` or `axios`
|
||||
- TanStack Query manages all server state caching, deduplication, and revalidation
|
||||
- Query keys follow convention: `['metadata']`, `['tableMetadata', tableName]`, `['records', tableName, filterHash]`
|
||||
- Mutations use `useMutation` with `onSuccess` invalidation of affected queries
|
||||
- 401 responses intercepted globally → redirect to login preserving current URL
|
||||
|
||||
### Component Patterns
|
||||
|
||||
- Server Components used for: root layouts, metadata-fetching route pages
|
||||
- Client Components used for: interactive UI (forms, grids, filters, modals)
|
||||
- Mark client components with `"use client"` directive at file top
|
||||
- Every interactive component must have a `data-qqq-id` attribute for CSS customization hooks
|
||||
- Naming convention: `data-qqq-id="{component-type}-{identifier}"` (e.g., `data-qqq-id="button-save"`, `data-qqq-id="field-firstName"`)
|
||||
|
||||
### State Management
|
||||
|
||||
- Server state: TanStack Query (data from API)
|
||||
- UI state: React context (theme, sidebar open/closed, modal stack, current user)
|
||||
- Form state: React Hook Form (form values, validation, dirty tracking)
|
||||
- URL state: Next.js searchParams (filters, pagination, sort — shareable URLs)
|
||||
- Local persistence: localStorage (density, column widths, saved view selections, widget dropdown choices)
|
||||
|
||||
### Error Handling
|
||||
|
||||
- Error boundaries at: route level (Next.js error.tsx), widget level (per-widget ErrorBoundary)
|
||||
- API errors: caught in TanStack Query `onError` → displayed via toast notification
|
||||
- Form validation: Zod schemas generated from field metadata (required, maxLength, type)
|
||||
- Loading states: Skeleton components matching final layout shape
|
||||
- Empty states: Descriptive messages with action buttons ("No records found. Create one?")
|
||||
|
||||
### Accessibility (WCAG 2.1 AA)
|
||||
|
||||
- All form inputs: `<label>` element with `htmlFor` or wrapping input, `aria-required`, `aria-invalid`
|
||||
- All buttons: descriptive text or `aria-label`
|
||||
- Focus management: trap focus in modals, restore on close
|
||||
- Keyboard navigation: all interactive elements reachable via Tab, actions via Enter/Space
|
||||
- Color contrast: minimum 4.5:1 for normal text, 3:1 for large text
|
||||
- Screen reader: `aria-live` regions for dynamic content updates (toasts, loading states)
|
||||
|
||||
### CSS Customization
|
||||
|
||||
- 60+ CSS custom properties (tokens) for theming — defined in `src/styles/qqq-theme.css`
|
||||
- `data-qqq-id` attributes on all interactive elements enable `customCss` injection
|
||||
- Light/dark mode via `prefers-color-scheme` media query + manual toggle stored in localStorage
|
||||
- Tailwind classes for all layout/spacing; CSS custom properties for brand colors only
|
||||
|
||||
### File Naming Conventions
|
||||
|
||||
- Components: PascalCase (`RecordQuery.tsx`, `FilterBuilder.tsx`)
|
||||
- Hooks: camelCase with `use-` prefix (`use-metadata.ts`, `use-query-records.ts`)
|
||||
- Utilities: kebab-case (`filter-utils.ts`, `value-utils.ts`)
|
||||
- Types: kebab-case (`metadata.ts`, `records.ts`)
|
||||
- API modules: kebab-case (`possible-values.ts`, `auth.ts`)
|
||||
- Tests: `*.test.ts(x)` co-located with source or in `tests/` directory
|
||||
|
||||
### Import Organization
|
||||
|
||||
```typescript
|
||||
// 1. React/Next.js
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
||||
// 2. External libraries
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useForm } from 'react-hook-form'
|
||||
|
||||
// 3. Internal: types
|
||||
import type { QTableMetaData, QRecord } from '@/types'
|
||||
|
||||
// 4. Internal: lib (API, hooks, utils)
|
||||
import { queryRecords } from '@/lib/api/tables'
|
||||
import { useMetaData } from '@/lib/hooks/use-metadata'
|
||||
|
||||
// 5. Internal: components
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { DataGrid } from '@/components/query/DataGrid'
|
||||
```
|
||||
@@ -0,0 +1,565 @@
|
||||
# Requirements Coverage Cross-Check Report
|
||||
|
||||
**Document Version:** 1.0
|
||||
**Date:** 2026-02-25
|
||||
**Status:** COMPLETE COVERAGE VERIFIED
|
||||
**Reviewed Sections:** Section 3 (API Contract) and Section 5 (New Admin UI Requirements)
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
This report validates that **EVERY subsection** of Section 5 (New Admin UI Requirements) and Section 3 (API Contract) from the QQQ Admin UI Modernization Requirements document appears in at least one of the six work packages.
|
||||
|
||||
**Result:** ✅ **100% COVERAGE** - All requirements are mapped to work packages with clear ownership and no gaps.
|
||||
|
||||
---
|
||||
|
||||
## Section 3: Semantic API Contract v1.0 Coverage
|
||||
|
||||
### Authentication Endpoints
|
||||
|
||||
| Requirement | Section | Details | Covered By | Status |
|
||||
|---|---|---|---|---|
|
||||
| Get Authentication Metadata | 3.2.1 | `GET /qqq/v1/metaData/authentication` (unsecured) | Package 1 (Auth Provider) | ✅ |
|
||||
| Create/Manage Session | 3.2.2 | `POST /qqq/v1/manageSession` with accessToken | Package 1 (Auth Provider) | ✅ |
|
||||
| Logout | 3.2.3 | `POST /qqq/v1/logout` clears session cookie | Package 1 (Auth Provider) | ✅ |
|
||||
| OIDC Back-Channel Logout | 3.2.4 | `POST /qqq/v1/oidc/backchannel-logout` | Package 1 (Auth Provider) | ✅ |
|
||||
|
||||
**Implementation Status:** Package 1 Step 3-4 provides complete auth interceptor, session management, and logout handling.
|
||||
|
||||
---
|
||||
|
||||
### Metadata Endpoints
|
||||
|
||||
| Requirement | Section | Details | Covered By | Status |
|
||||
|---|---|---|---|---|
|
||||
| Application Metadata | 3.3.1 | `GET /qqq/v1/metaData` returns QInstance | Package 1 (API Client) | ✅ |
|
||||
| Table Metadata | 3.3.2 | `GET /qqq/v1/metaData/table/{tableName}` returns QTableMetaData | Package 1 (API Client), Package 2 | ✅ |
|
||||
| Process Metadata | 3.3.3 | `GET /qqq/v1/metaData/process/{processName}` returns QProcessMetaData | Package 1 (API Client), Package 4 | ✅ |
|
||||
|
||||
**Implementation Status:** Package 1 Step 3 and later steps in Packages 2/4 handle all metadata loading.
|
||||
|
||||
---
|
||||
|
||||
### Table Data Endpoints
|
||||
|
||||
| Requirement | Section | Details | Covered By | Status |
|
||||
|---|---|---|---|---|
|
||||
| Query Records | 3.4.1 | `POST /qqq/v1/table/{tableName}/query` with QQueryFilter | Package 2 (Step 2) | ✅ |
|
||||
| Count Records | 3.4.2 | `POST /qqq/v1/table/{tableName}/count` with optional includeDistinct | Package 2 (Step 2) | ✅ |
|
||||
| Get Single Record | 3.4.3 | `GET /qqq/v1/table/{tableName}/{primaryKey}` with tableVariant, includeAssociations | Package 3 (Step 1) | ✅ |
|
||||
| Insert Record | 3.4.4 | `POST /qqq/v1/table/{tableName}` with multipart/form-data | Package 3 (Step 1) | ✅ |
|
||||
| Update Record | 3.4.5 | `PUT /qqq/v1/table/{tableName}/{primaryKey}` with multipart/form-data | Package 3 (Step 1) | ✅ |
|
||||
| Delete Record | 3.4.6 | `DELETE /qqq/v1/table/{tableName}/{primaryKey}` | Package 3 (Step 1) | ✅ |
|
||||
|
||||
**Implementation Status:** Package 2 handles query/count, Package 3 handles get/create/update/delete with full form integration.
|
||||
|
||||
---
|
||||
|
||||
### Process Endpoints
|
||||
|
||||
| Requirement | Section | Details | Covered By | Status |
|
||||
|---|---|---|---|---|
|
||||
| Initialize Process | 3.5.1 | `POST /qqq/v1/processes/{processName}/init` with values, recordsParam, file | Package 4 (Step 1) | ✅ |
|
||||
| Process Step | 3.5.2 | `POST /qqq/v1/processes/{processName}/{processUUID}/step/{stepName}` | Package 4 (Step 1) | ✅ |
|
||||
| Process Status (Poll) | 3.5.3 | `GET /qqq/v1/processes/{processName}/{processUUID}/status/{jobUUID}` | Package 4 (Step 1) | ✅ |
|
||||
| Process Records | 3.5.4 | `GET /qqq/v1/processes/{processName}/{processUUID}/records` with skip/limit | Package 4 (Step 1) | ✅ |
|
||||
| Cancel Process | 3.5.5 | `GET /qqq/v1/processes/{processName}/{processUUID}/cancel` | Package 4 (Step 1) | ✅ |
|
||||
|
||||
**Implementation Status:** Package 4 Step 1 implements complete process API client with all endpoints and job polling.
|
||||
|
||||
---
|
||||
|
||||
### Additional Endpoints
|
||||
|
||||
| Requirement | Section | Details | Covered By | Status |
|
||||
|---|---|---|---|---|
|
||||
| Widget Endpoint | 3.6 | `GET /qqq/v1/widget/{widgetName}` with dynamic query params | Package 5 (Step 2) | ✅ |
|
||||
| Possible Values Endpoints | 3.7 | `POST /qqq/v1/table/{t}/possibleValues/{f}`, `/processes/{p}/possibleValues/{f}`, `/possibleValues/{f}` | Package 2 (Step 5), Package 3 (Step 1), Package 4 | ✅ |
|
||||
| Error Response Shape | 3.8 | Consistent error format with HTTP status codes | All packages (API client) | ✅ |
|
||||
| Pagination Contract | 3.9 | offset/limit based with skip and limit | Package 2 (Step 6) | ✅ |
|
||||
| Filtering Contract | 3.10 | Tree-structured QQueryFilter with boolean operators, subFilters, expressions | Package 2 (Step 4-5) | ✅ |
|
||||
| Backwards Compatibility Policy | 3.11 | SemVer with PATCH/MINOR/MAJOR deprecation cycles | Package 6 (documentation) | ✅ |
|
||||
|
||||
**Implementation Status:** Complete coverage across all packages.
|
||||
|
||||
---
|
||||
|
||||
## Section 5: New Admin UI Requirements Coverage
|
||||
|
||||
### 5.1: Core Principles
|
||||
|
||||
| Requirement | Details | Covered By | Status |
|
||||
|---|---|---|---|---|
|
||||
| 100% feature parity | Every page, component, interaction in qfmd | All Packages 1-6 | ✅ |
|
||||
| Responsive design | Fully functional at mobile/tablet/desktop | Packages 1, 2, 3, 4, 5, 6 | ✅ |
|
||||
| Metadata-driven rendering | No hardcoded tables, fields, pages | All Packages | ✅ |
|
||||
| WCAG 2.1 AA accessible | Keyboard navigation, ARIA, color contrast | Package 6 (Steps 7-13) | ✅ |
|
||||
| Themeable | Light/dark mode, custom brand colors, CSS variables | Package 1 (Step 9), Package 6 (Step 26) | ✅ |
|
||||
|
||||
---
|
||||
|
||||
### 5.2: Page Inventory
|
||||
|
||||
#### 5.2.1 Authentication Pages
|
||||
|
||||
| Page | Route | Covered By | Status |
|
||||
|---|---|---|---|
|
||||
| Login | `/login` | Package 1 (Step 4, Step 11) | ✅ |
|
||||
| No-Auth Screen | `/no-auth-screen` | Package 1 (Step 11) | ✅ |
|
||||
|
||||
**Implementation Status:** Package 1 Step 11 creates login page with error handling.
|
||||
|
||||
---
|
||||
|
||||
#### 5.2.2 App Home Pages
|
||||
|
||||
| Page | Route | Components | Covered By | Status |
|
||||
|---|---|---|---|---|
|
||||
| App Dashboard/Home | `/app/{appName}` | Dashboard widgets, record counts, navigation | Package 5 (Step 12) | ✅ |
|
||||
|
||||
**Implementation Status:** Package 5 Step 12 implements App Home page with full widget orchestration.
|
||||
|
||||
---
|
||||
|
||||
#### 5.2.3 Record Pages
|
||||
|
||||
| Page | Route | Key Elements | Covered By | Status |
|
||||
|---|---|---|---|---|
|
||||
| Record Query/List | `/app/{table}` | DataGrid, filters, pagination, toolbar | Package 2 (Steps 1-11) | ✅ |
|
||||
| Saved View | `/app/{table}/savedView/:id` | Pre-filtered query with saved config | Package 2 (Step 10) | ✅ |
|
||||
| Create Record | `/app/{table}/create` | Multi-section form, file upload, validation | Package 3 (Steps 5-6) | ✅ |
|
||||
| View Single Record | `/app/{table}/:id` | Record header, sections, widgets, audit trail | Package 3 (Step 6) | ✅ |
|
||||
| Edit Record | `/app/{table}/:id/edit` | Form with pre-populated values | Package 3 (Steps 5-6) | ✅ |
|
||||
| Copy Record | `/app/{table}/:id/copy` | Form with copied values (no ID) | Package 3 (Steps 5-6) | ✅ |
|
||||
| View by Key | `/app/{table}/key` | Record lookup by unique key | Package 3 (Step 6) | ✅ |
|
||||
| Table Developer View | `/app/{table}/dev` | Metadata inspector | Package 6 (Step 17) | ✅ |
|
||||
| Record Developer View | `/app/{table}/:id/dev` | Script editor, logs, test execution | Package 6 (Step 18) | ✅ |
|
||||
|
||||
**Implementation Status:** Package 2 handles query/list (Steps 1-11), Package 3 handles create/view/edit/copy (Steps 5-6), Package 6 handles developer views (Steps 17-18).
|
||||
|
||||
---
|
||||
|
||||
#### 5.2.4 Process Pages
|
||||
|
||||
| Page | Route | Components | Covered By | Status |
|
||||
|---|---|---|---|---|
|
||||
| Standalone Process | `/app/{processName}` | Step wizard, forms, validation, results | Package 4 (Steps 2-8) | ✅ |
|
||||
| Table-Scoped Process | `/app/{table}/{processName}` | Same as standalone with record context | Package 4 (Steps 2-8) | ✅ |
|
||||
|
||||
**Implementation Status:** Package 4 Steps 2-8 implement ProcessRun component for both standalone and embedded contexts.
|
||||
|
||||
---
|
||||
|
||||
#### 5.2.5 Report Pages
|
||||
|
||||
| Page | Route | Components | Covered By | Status |
|
||||
|---|---|---|---|---|
|
||||
| Report Execution | `/app/{reportName}` | Report config form, output display | Package 6 (Step 19) | ✅ |
|
||||
|
||||
**Implementation Status:** Package 6 Step 19 reuses ProcessRun for report execution.
|
||||
|
||||
---
|
||||
|
||||
### 5.3: Component Inventory
|
||||
|
||||
#### 5.3.1 Layout Components
|
||||
|
||||
| Component | Required Behavior | Covered By | Status |
|
||||
|---|---|---|---|
|
||||
| Side Navigation | Hierarchical from appTree, collapsible, mini-mode, keyboard nav | Package 1 (Steps 7-8) | ✅ |
|
||||
| Top Navigation | Breadcrumbs, user menu, sidebar toggle, search | Package 1 (Steps 7-8), Package 6 (Step 14) | ✅ |
|
||||
| Header Banner | Custom branding, environment banners, dismissible | Package 1 (Step 11) | ✅ |
|
||||
| Footer | Minimal, optional | Package 1 (Step 7) | ✅ |
|
||||
|
||||
**Implementation Status:** Package 1 Steps 7-8 implement Sidebar, Header, Breadcrumbs. Package 6 Step 14 adds Command Palette.
|
||||
|
||||
---
|
||||
|
||||
#### 5.3.2 Data Display Components
|
||||
|
||||
| Component | Required Behavior | Covered By | Status |
|
||||
|---|---|---|---|
|
||||
| Data Grid | Server-side pagination, sorting, filtering, column reordering, row selection, density control, export | Package 2 (Steps 1-11) | ✅ |
|
||||
| Record Detail View | Section-organized fields, field adornments, audit trail, associated records | Package 3 (Step 6) | ✅ |
|
||||
| Statistics Cards | Number cards with icons, color coding, click-through | Package 5 (Step 8) | ✅ |
|
||||
| Charts | Line, bar, pie, stacked, doughnut with interactivity | Package 5 (Step 7) | ✅ |
|
||||
| Data Bags | JSON/structured data with syntax highlighting | Package 5 (Step 6) | ✅ |
|
||||
|
||||
**Implementation Status:** Package 2 handles DataGrid (Steps 1-11), Package 3 handles RecordView (Step 6), Package 5 handles all widgets (Steps 6-8).
|
||||
|
||||
---
|
||||
|
||||
#### 5.3.3 Form Components
|
||||
|
||||
| Component | Required Behavior | Covered By | Status |
|
||||
|---|---|---|---|
|
||||
| Entity Form | Multi-section, Zod validation, all 12 field types, conditional visibility | Package 3 (Steps 2-6) | ✅ |
|
||||
| Autocomplete/Select | Possible-values dropdown with server search, hierarchical | Package 3 (Step 4) | ✅ |
|
||||
| Boolean Fields | Toggle switch, tri-state | Package 3 (Step 4) | ✅ |
|
||||
| Chip Input | Multi-value with tags | Package 3 (Step 4) | ✅ |
|
||||
| File Upload | Single/multi with preview, drag-drop | Package 3 (Step 4) | ✅ |
|
||||
| Date/Time Fields | Date picker, time picker, range selection | Package 3 (Step 4) | ✅ |
|
||||
| Code Editor | Syntax-highlighted editing | Package 3 (Step 4), Package 6 (Step 18) | ✅ |
|
||||
|
||||
**Implementation Status:** Package 3 Steps 2-6 implement all form components. Package 6 Step 18 adds developer code editor.
|
||||
|
||||
---
|
||||
|
||||
#### 5.3.4 Process Components
|
||||
|
||||
| Component | Required Behavior | Covered By | Status |
|
||||
|---|---|---|---|
|
||||
| Step Wizard | Linear stepper, forward/back navigation, progress | Package 4 (Steps 2-3) | ✅ |
|
||||
| Validation Review | Pre-commit warnings/errors, accept/reject/modify | Package 4 (Step 5) | ✅ |
|
||||
| Bulk Load | CSV mapping, value mapping, profile save/load | Package 4 (Step 6) | ✅ |
|
||||
| Process Results | Summary, record counts, error lists, downloads | Package 4 (Step 8) | ✅ |
|
||||
| Progress Display | Progress bar, message, current/total, cancel | Package 4 (Step 3) | ✅ |
|
||||
| Google Drive | Folder picker wrapper | Package 4 (Step 9) | ✅ |
|
||||
|
||||
**Implementation Status:** Package 4 Steps 2-9 implement all process components.
|
||||
|
||||
---
|
||||
|
||||
#### 5.3.5 Query/Filter Components
|
||||
|
||||
| Component | Required Behavior | Covered By | Status |
|
||||
|---|---|---|---|
|
||||
| Filter Builder | Basic + advanced modes, 20 operators, AND/OR logic, sub-filters | Package 2 (Steps 4-5) | ✅ |
|
||||
| Quick Filter | Text search across columns | Package 2 (Step 4) | ✅ |
|
||||
| Column Configuration | Show/hide, reorder, persist to localStorage | Package 2 (Step 8) | ✅ |
|
||||
| Pagination | Page size selector, page navigation, total count | Package 2 (Step 6) | ✅ |
|
||||
| Saved Views | Save/restore filter + column + sort config | Package 2 (Step 10) | ✅ |
|
||||
| Export | CSV, PDF, Excel (extensible) | Package 2 (Step 11) | ✅ |
|
||||
| Table Variants | Select alternate views | Package 2 (Step 7) | ✅ |
|
||||
|
||||
**Implementation Status:** Package 2 Steps 4-11 implement all query/filter components.
|
||||
|
||||
---
|
||||
|
||||
#### 5.3.6 Widget Components
|
||||
|
||||
| Component | Required Behavior | Covered By | Status |
|
||||
|---|---|---|---|
|
||||
| Widget Container | Label, dropdown, reload, export, help | Package 5 (Step 4) | ✅ |
|
||||
| Block Types (11 types) | Text, BigNumber, UpOrDown, IconBadge, Progress, Button, Icon, Image, Audio, Divider, InputField | Package 5 (Step 6) | ✅ |
|
||||
| Record Grid Widget | Embedded grid, add/edit/delete | Package 5 (Step 9) | ✅ |
|
||||
| Specialized Widgets (10 types) | Composite, Parent, DynamicForm, PivotTable, FilterColumns, Stepper, Cron, USMap, ScriptViewer, Custom | Package 5 (Step 10) | ✅ |
|
||||
|
||||
**Implementation Status:** Package 5 Steps 4-11 implement all widget components.
|
||||
|
||||
---
|
||||
|
||||
#### 5.3.7 Feedback Components
|
||||
|
||||
| Component | Required Behavior | Covered By | Status |
|
||||
|---|---|---|---|
|
||||
| Alerts | Inline success/error/warning/info | Package 1 (Step 11), All packages | ✅ |
|
||||
| Modals/Dialogs | Modal stack, confirmation, focus trap | Package 1 (Step 6), Package 6 (Step 8) | ✅ |
|
||||
| Tooltips/Help | Contextual help, rich HTML content | Package 3 (Step 3), All packages | ✅ |
|
||||
| Command Menu | Cmd+K quick navigation | Package 6 (Step 14) | ✅ |
|
||||
|
||||
**Implementation Status:** All feedback components covered across packages, with full keyboard/focus support in Package 6.
|
||||
|
||||
---
|
||||
|
||||
### 5.4: Responsive Design Requirements
|
||||
|
||||
#### 5.4.1 Breakpoints
|
||||
|
||||
| Breakpoint | Width | Layout | Covered By | Status |
|
||||
|---|---|---|---|---|
|
||||
| Mobile | < 768px | Single-column, off-canvas nav, bottom navigation | Packages 1, 2, 3, 4, 5, 6 | ✅ |
|
||||
| Tablet | 768px–1024px | Adaptive layout, mini-sidebar, two-column | Packages 1, 2, 3, 4, 5, 6 | ✅ |
|
||||
| Desktop | > 1024px | Full layout, persistent sidebar, multi-panel | Packages 1, 2, 3, 4, 5, 6 | ✅ |
|
||||
|
||||
**Implementation Status:** Package 6 Steps 1-6 comprehensively audit and implement responsive design across all pages.
|
||||
|
||||
---
|
||||
|
||||
#### 5.4.2 Page-Specific Responsive Treatment
|
||||
|
||||
| Page | Mobile Treatment | Covered By | Status |
|
||||
|---|---|---|---|
|
||||
| Record Query | Card-based list, simplified filter, 2-3 columns | Package 6 (Step 1) | ✅ |
|
||||
| Record View | Single-column stacked, collapsible sections | Package 6 (Step 2) | ✅ |
|
||||
| Entity Form | Single-column, sticky buttons | Package 6 (Step 2) | ✅ |
|
||||
| Process Execution | Full-screen steps, bottom navigation | Package 6 (Step 5) | ✅ |
|
||||
| App Home/Dashboard | Stacked widgets, horizontal scroll stats | Package 6 (Step 5) | ✅ |
|
||||
|
||||
**Implementation Status:** Package 6 Steps 1-6 implement comprehensive responsive treatment for all page types.
|
||||
|
||||
---
|
||||
|
||||
### 5.5: Authentication and Authorization
|
||||
|
||||
#### 5.5.1 Authentication Flows
|
||||
|
||||
| Flow | Implementation | Covered By | Status |
|
||||
|---|---|---|---|
|
||||
| OAuth2/OIDC | Redirect, token exchange, session creation | Package 1 (Steps 3-4) | ✅ |
|
||||
| Auth0 | Auth0-specific configuration | Package 1 (Step 3) | ✅ |
|
||||
| Anonymous/Mock | For development and test instances | Package 1 (Steps 3-4) | ✅ |
|
||||
| Session Management | sessionUUID cookie-based | Package 1 (Step 3) | ✅ |
|
||||
| Back-Channel Logout | Server-side session invalidation | Package 1 (Step 4) | ✅ |
|
||||
| Explicit Logout | POST /logout clears session | Package 1 (Step 4) | ✅ |
|
||||
|
||||
**Implementation Status:** Package 1 Steps 3-4 implement complete auth system.
|
||||
|
||||
---
|
||||
|
||||
#### 5.5.2 Authorization (Role-Based UI)
|
||||
|
||||
| Control | Implementation | Covered By | Status |
|
||||
|---|---|---|---|
|
||||
| Table-level permissions | readPermission, insertPermission, editPermission, deletePermission control visibility | Packages 2, 3, 6 | ✅ |
|
||||
| Process-level permissions | hasPermission controls launch buttons | Package 4 | ✅ |
|
||||
| Capability-based toggling | TABLE_QUERY, TABLE_GET, TABLE_COUNT, etc. hide unavailable operations | Packages 2, 3 | ✅ |
|
||||
| Widget-level permissions | hasPermission controls visibility | Package 5 | ✅ |
|
||||
| Help content role filtering | Roles control help visibility | All packages | ✅ |
|
||||
|
||||
**Implementation Status:** Authorization checks integrated throughout all packages.
|
||||
|
||||
---
|
||||
|
||||
#### 5.5.3 Session Timeout
|
||||
|
||||
| Feature | Implementation | Covered By | Status |
|
||||
|---|---|---|---|
|
||||
| 401 Detection | Global axios interceptor | Package 1 (Step 3) | ✅ |
|
||||
| Re-auth Modal | Non-disruptive modal prompt | Package 6 (Step 29) | ✅ |
|
||||
| State Preservation | Unsaved form data, scroll, filter state | Package 6 (Step 29) | ✅ |
|
||||
| Timeout Warning | Configurable warning (5 min before expiry) | Package 6 (Step 29) | ✅ |
|
||||
|
||||
**Implementation Status:** Package 1 Step 3 handles 401 globally, Package 6 Step 29 implements full session timeout UX.
|
||||
|
||||
---
|
||||
|
||||
### 5.6: Performance Requirements
|
||||
|
||||
#### 5.6.1 Load Time Targets
|
||||
|
||||
| Metric | Target | Implementation | Covered By | Status |
|
||||
|---|---|---|---|
|
||||
| First Contentful Paint | < 1.5s | Server Components, code splitting | Package 1, Package 6 (Step 23) | ✅ |
|
||||
| Time to Interactive | < 3.0s | Lazy loading, optimizations | Package 6 (Steps 22-25) | ✅ |
|
||||
| Largest Contentful Paint | < 2.5s | Image optimization | Package 6 (Step 23) | ✅ |
|
||||
| Cumulative Layout Shift | < 0.1 | Skeleton loaders, reserved space | All packages | ✅ |
|
||||
| First Input Delay | < 100ms | Efficient event handlers | All packages | ✅ |
|
||||
|
||||
**Implementation Status:** Package 6 Steps 22-25 implement comprehensive performance optimization.
|
||||
|
||||
---
|
||||
|
||||
#### 5.6.2 Caching Strategy
|
||||
|
||||
| Endpoint | Strategy | TTL | Covered By | Status |
|
||||
|---|---|---|---|
|
||||
| GET /metaData | Memoize in memory | Session | Package 1 (Step 3) | ✅ |
|
||||
| GET /metaData/table/{t} | Memoize per table | Session | Package 2 (Step 2) | ✅ |
|
||||
| GET /metaData/process/{p} | Memoize per process | Session | Package 4 (Step 1) | ✅ |
|
||||
| GET /metaData/authentication | localStorage TTL | 1 hour | Package 1 (Step 3) | ✅ |
|
||||
| POST /table/{t}/query | No cache | N/A | Package 2 (Step 2) | ✅ |
|
||||
| POST /table/{t}/count | Short-lived cache | 30 seconds | Package 2 (Step 2) | ✅ |
|
||||
| GET /widget/{w} | Request dedup | Cancel-replace | Package 5 (Step 2) | ✅ |
|
||||
| POST /possibleValues/{f} | Debounced | 300ms | Packages 3, 4 | ✅ |
|
||||
|
||||
**Implementation Status:** Caching strategy implemented across all packages with TanStack Query.
|
||||
|
||||
---
|
||||
|
||||
#### 5.6.3 Optimization Strategies
|
||||
|
||||
| Strategy | Implementation | Covered By | Status |
|
||||
|---|---|---|---|
|
||||
| Route-based code splitting | Next.js automatic | Package 1 (Step 1) | ✅ |
|
||||
| Lazy loading | Charts, code editor, file upload, rich text | Package 6 (Step 24) | ✅ |
|
||||
| Optimistic UI | Create/update/delete with immediate feedback | Packages 3, 4 | ✅ |
|
||||
| Virtual scrolling | DataGrid >100 rows, dropdowns >50 items | Package 6 (Step 24) | ✅ |
|
||||
| Server-side metadata | Server Components processing | Package 1 (Step 1) | ✅ |
|
||||
|
||||
**Implementation Status:** All optimization strategies covered across packages.
|
||||
|
||||
---
|
||||
|
||||
### 5.7: Developer Experience Requirements
|
||||
|
||||
#### 5.7.1 Component Documentation
|
||||
|
||||
| Requirement | Implementation | Covered By | Status |
|
||||
|---|---|---|---|
|
||||
| Storybook setup | All shared components documented | Package 6 (Step 20) | ✅ |
|
||||
| Component stories | Loading, empty, error, populated, disabled states | Package 6 (Step 20) | ✅ |
|
||||
| Interactive controls | Prop exploration in Storybook | Package 6 (Step 20) | ✅ |
|
||||
| Accessibility audit | axe-core integration in stories | Package 6 (Step 20) | ✅ |
|
||||
|
||||
**Implementation Status:** Package 6 Step 20 implements comprehensive Storybook documentation.
|
||||
|
||||
---
|
||||
|
||||
#### 5.7.2 Testing
|
||||
|
||||
| Test Type | Implementation | Covered By | Status |
|
||||
|---|---|---|---|
|
||||
| E2E Testing | Playwright for critical flows | Package 6 (Step 21) | ✅ |
|
||||
| Component Testing | Vitest + Testing Library | All packages (unit tests) | ✅ |
|
||||
| Visual Regression | Screenshot comparison | Package 6 (Step 21) | ✅ |
|
||||
| Accessibility Testing | axe-core in CI | Package 6 (Step 21) | ✅ |
|
||||
| API Contract Testing | Type definitions validation | Package 1 (Step 2) | ✅ |
|
||||
|
||||
**Implementation Status:** Comprehensive testing covered across packages and Package 6.
|
||||
|
||||
---
|
||||
|
||||
#### 5.7.3 Build and CI/CD
|
||||
|
||||
| Requirement | Implementation | Covered By | Status |
|
||||
|---|---|---|---|
|
||||
| Gradle/Maven integration | Part of QQQ build pipeline | Package 1 (Step 1) | ✅ |
|
||||
| HMR | Vite-based or Turbopack | Package 1 (Step 1) | ✅ |
|
||||
| Production build | Minification, tree-shaking, source maps | Package 1 (Step 1) | ✅ |
|
||||
| Bundle size monitoring | CI threshold check | Package 6 (Step 22) | ✅ |
|
||||
| Lint + Format enforcement | ESLint + Prettier in CI | Package 1 (Step 1) | ✅ |
|
||||
|
||||
**Implementation Status:** Package 1 Step 1 establishes build pipeline, Package 6 Step 22 adds monitoring.
|
||||
|
||||
---
|
||||
|
||||
#### 5.7.4 Type Safety
|
||||
|
||||
| Requirement | Implementation | Covered By | Status |
|
||||
|---|---|---|---|
|
||||
| Generated API client | From OpenAPI spec or shared types | Package 1 (Step 2) | ✅ |
|
||||
| Strict TypeScript | strict: true in tsconfig | Package 1 (Step 1) | ✅ |
|
||||
| Runtime validation | Zod for API responses | Package 3 (Step 2) | ✅ |
|
||||
|
||||
**Implementation Status:** Type safety implemented from Package 1 foundations across all packages.
|
||||
|
||||
---
|
||||
|
||||
### 5.8: Migration Strategy
|
||||
|
||||
#### 5.8.1 Phased Approach
|
||||
|
||||
| Phase | Duration | Coverage | Covered By | Status |
|
||||
|---|---|---|---|
|
||||
| Phase 0 — Foundation | Weeks 1–4 | Project setup, auth, layout, routing | Package 1 | ✅ |
|
||||
| Phase 1 — Record Query | Weeks 5–10 | DataGrid with filtering, sorting, pagination | Package 2 | ✅ |
|
||||
| Phase 2 — Record CRUD | Weeks 11–16 | View, create, edit, delete forms | Package 3 | ✅ |
|
||||
| Phase 3 — Processes | Weeks 17–22 | Step wizard, bulk load, async execution | Package 4 | ✅ |
|
||||
| Phase 4 — Dashboard | Weeks 23–26 | Widgets, charts, statistics | Package 5 | ✅ |
|
||||
| Phase 5 — Polish | Weeks 27–32 | Responsive, a11y, testing, E2E | Package 6 | ✅ |
|
||||
|
||||
**Implementation Status:** All phases mapped to work packages 1-6.
|
||||
|
||||
---
|
||||
|
||||
#### 5.8.2 Coexistence Strategy
|
||||
|
||||
| Strategy | Implementation | Covered By | Status |
|
||||
|---|---|---|---|
|
||||
| Dual UI support | New UI at `/v2/`, legacy at `/` | Package 1 (Step 1) | ✅ |
|
||||
| Shared auth | Both UIs use same sessionUUID | Package 1 (Step 3) | ✅ |
|
||||
| Feature flag | User preference for default UI | Package 1 (Step 6) | ✅ |
|
||||
|
||||
**Implementation Status:** Coexistence architecture built into Package 1.
|
||||
|
||||
---
|
||||
|
||||
#### 5.8.3 First Milestone Deliverable
|
||||
|
||||
| Deliverable | Scope | Covered By | Status |
|
||||
|---|---|---|---|
|
||||
| Record Query page | Single table fully functional | Package 2 (Complete) | ✅ |
|
||||
| Validates | Metadata-driven rendering, API integration, grid performance, filters, auth, responsive, keyboard nav | Package 2 + Package 1 | ✅ |
|
||||
|
||||
**Implementation Status:** Package 2 complete record query implementation is production-ready first milestone.
|
||||
|
||||
---
|
||||
|
||||
## Gaps Found
|
||||
|
||||
**Result: ZERO GAPS**
|
||||
|
||||
All subsections of Section 5 and Section 3 are covered by at least one work package. No requirements are left unmapped.
|
||||
|
||||
---
|
||||
|
||||
## Overlap Analysis
|
||||
|
||||
### Intentional Overlaps (No Conflicts)
|
||||
|
||||
| Requirement | Package A | Package B | Nature | Resolution |
|
||||
|---|---|---|---|---|
|
||||
| API Client setup | Package 1 | All packages | Foundational | Package 1 provides base, others extend with specialized functions |
|
||||
| TanStack Query | Package 1 | Packages 2-5 | Foundational | Package 1 sets up QueryClient, others define query keys and hooks |
|
||||
| Form components | Package 3 | Package 4 | Reuse | Package 4 reuses EntityForm, DynamicFormField from Package 3 |
|
||||
| Metadata loading | Package 1 | Packages 2-5 | Foundational | Package 1 provides loadMetaData, others load specific table/process metadata |
|
||||
| Authorization checks | Package 1 | Packages 2-6 | Distributed | Package 1 establishes context, others gate features based on permissions |
|
||||
| Responsive design | Packages 1-6 | Package 6 | Progressive | Packages 1-5 implement responsive layouts, Package 6 audits and polishes |
|
||||
| Testing | All packages | Package 6 | Progressive | Packages 1-5 include unit/component tests, Package 6 adds E2E/Storybook |
|
||||
| Accessibility | All packages | Package 6 | Progressive | Packages 1-5 build accessible components, Package 6 audits and adds advanced features |
|
||||
|
||||
**Conclusion:** All overlaps are intentional, complementary, and properly sequenced. No ownership conflicts.
|
||||
|
||||
---
|
||||
|
||||
## Coverage Summary Table
|
||||
|
||||
### By Work Package
|
||||
|
||||
| Package | Section 5 Coverage | Section 3 Coverage | Total Subsections |
|
||||
|---|---|---|---|
|
||||
| Package 1: Scaffold & Auth | 5.1, 5.2.1, 5.3.1, 5.3.7, 5.4, 5.5.1, 5.5.2, 5.6.1-3, 5.7.3-4, 5.8 | 3.2 (all), 3.3.1, 3.8, 3.11 | 17 |
|
||||
| Package 2: Record Query | 5.2.3 (query), 5.3.2 (grid), 5.3.5 | 3.4.1, 3.4.2, 3.7, 3.9, 3.10 | 8 |
|
||||
| Package 3: Record CRUD | 5.2.3 (create/edit/view), 5.3.2 (detail), 5.3.3 | 3.4.3, 3.4.4, 3.4.5, 3.4.6, 3.7 | 8 |
|
||||
| Package 4: Processes | 5.2.4, 5.3.4, 5.3.6 (process widgets) | 3.5 (all) | 8 |
|
||||
| Package 5: Dashboard | 5.2.2, 5.3.2 (stats/charts), 5.3.6 (all widgets) | 3.6 | 5 |
|
||||
| Package 6: Polish | 5.3.7 (advanced), 5.4.2, 5.5.3, 5.6.1-3, 5.7.1-2, 5.8.3 | N/A | 7 |
|
||||
|
||||
**Total Unique Subsections:** 53/53 = **100% Coverage**
|
||||
|
||||
---
|
||||
|
||||
## Verification Methodology
|
||||
|
||||
This cross-check was performed by:
|
||||
|
||||
1. **Extracting all section headers** from Section 3 (API Contract) and Section 5 (New Admin UI)
|
||||
2. **Reading each work package** and identifying implemented subsections
|
||||
3. **Mapping each subsection** to responsible package(s) and specific implementation step(s)
|
||||
4. **Verifying coverage** — every subsection appears in at least one package
|
||||
5. **Checking for overlaps** — ensuring no conflicts, all overlaps are intentional
|
||||
6. **Validating completeness** — no gaps, no floating requirements
|
||||
|
||||
---
|
||||
|
||||
## Recommendations
|
||||
|
||||
1. **For Implementation Teams:**
|
||||
- Use this matrix as a checklist during development
|
||||
- Cross-reference work package steps with requirement subsections
|
||||
- Update this report when new requirements are added
|
||||
|
||||
2. **For Project Management:**
|
||||
- Track progress against this coverage map
|
||||
- Use as acceptance criteria for package completion
|
||||
- Monitor for any new requirements that might appear during implementation
|
||||
|
||||
3. **For Architecture Reviews:**
|
||||
- Reference this document when evaluating design decisions
|
||||
- Ensure API client changes don't break type contracts defined in Section 3
|
||||
- Validate responsive design completeness per Section 5.4
|
||||
|
||||
---
|
||||
|
||||
## Sign-Off
|
||||
|
||||
**Report Created By:** Cross-Check Analysis
|
||||
**Date:** 2026-02-25
|
||||
**Status:** COMPLETE AND VERIFIED
|
||||
|
||||
**Conclusion:** ✅ All requirements from Section 3 (Semantic API Contract v1.0) and Section 5 (New Admin UI Requirements) are fully covered by the six work packages with clear ownership, no gaps, and intentional complementary overlaps.
|
||||
|
||||
The implementation plan is ready for execution.
|
||||
@@ -0,0 +1,54 @@
|
||||
# Review Checklist: Package 1 — Scaffold, Auth, and Layout Shell
|
||||
|
||||
### Requirements Coverage
|
||||
- [ ] Section 3.2: Auth endpoints (getAuthenticationMetaData, manageSession, logout) implemented
|
||||
- [ ] Section 5.1: Metadata-driven rendering foundation established
|
||||
- [ ] Section 5.5: Authentication flows (AUTH_0, OAUTH2, FULLY_ANONYMOUS) working
|
||||
- [ ] Section 5.3.1: Layout components (sidebar, navbar, breadcrumbs, banner) rendering
|
||||
- [ ] Section 5.5.2: Permission fields present on metadata types (readPermission, insertPermission, etc.)
|
||||
|
||||
### Integration Check
|
||||
- [ ] `pnpm dev` starts without errors
|
||||
- [ ] `pnpm build` completes successfully
|
||||
- [ ] `pnpm tsc --noEmit` passes with no type errors
|
||||
- [ ] All type definitions from shared-context/type-definitions.md present in /src/types/
|
||||
- [ ] API client base URL configurable via NEXT_PUBLIC_API_BASE_URL
|
||||
- [ ] TanStack Query provider wraps the app
|
||||
- [ ] Auth provider wraps the app
|
||||
- [ ] Theme provider wraps the app
|
||||
|
||||
### API Contract Compliance
|
||||
- [ ] GET /qqq/v1/metaData/authentication — correct path, no auth required
|
||||
- [ ] POST /qqq/v1/manageSession — multipart/form-data, accessToken field
|
||||
- [ ] POST /qqq/v1/logout — session cookie sent
|
||||
- [ ] GET /qqq/v1/metaData — correct path, returns QInstance shape
|
||||
- [ ] GET /qqq/v1/metaData/table/{tableName} — returns QTableMetaData
|
||||
- [ ] GET /qqq/v1/metaData/process/{processName} — returns QProcessMetaData
|
||||
- [ ] 401 responses trigger unauthorized callback
|
||||
|
||||
### Metadata-Driven Check
|
||||
- [ ] Sidebar navigation built from appTree metadata (no hardcoded nav items)
|
||||
- [ ] Routes generated dynamically from app tree traversal
|
||||
- [ ] Breadcrumbs use pathToLabelMap from metadata
|
||||
- [ ] Branding (logo, appName, accentColor) from QBrandingMetaData
|
||||
- [ ] Banners from branding.banners metadata
|
||||
|
||||
### Responsive Check
|
||||
- [ ] Sidebar collapses to mini-mode on tablet
|
||||
- [ ] Sidebar becomes off-canvas drawer on mobile (to be finalized in Package 6)
|
||||
- [ ] Header adapts to screen width
|
||||
- [ ] Layout doesn't overflow horizontally at any breakpoint
|
||||
|
||||
### Accessibility Check
|
||||
- [ ] Sidebar has role="navigation"
|
||||
- [ ] All buttons have aria-label or text content
|
||||
- [ ] Breadcrumbs have aria-current="page" on last item
|
||||
- [ ] Banner has role="alert"
|
||||
- [ ] Focus visible on all interactive elements
|
||||
- [ ] Tab order is logical through sidebar → header → content
|
||||
|
||||
### Type Safety Check
|
||||
- [ ] No `any` types in component props (except values: Record<string, any> from API)
|
||||
- [ ] All API functions have typed return values
|
||||
- [ ] QueryClient configured with proper generic types
|
||||
- [ ] Auth context type is fully defined (no partial types)
|
||||
@@ -0,0 +1,66 @@
|
||||
# Review Checklist: Package 2 — Record Query
|
||||
|
||||
### Requirements Coverage
|
||||
- [ ] Section 5.2.3: Record query page at /app/{tableName} renders
|
||||
- [ ] Section 5.3.2: Data grid with server-side pagination, sorting, filtering
|
||||
- [ ] Section 5.3.5: Basic filter (quick search) and advanced filter builder
|
||||
- [ ] Section 5.3.5: All 20 QCriteriaOperator values supported in filter builder
|
||||
- [ ] Section 5.3.5: AND/OR boolean logic and sub-filters
|
||||
- [ ] Section 5.3.5: Column configuration (show/hide, reorder)
|
||||
- [ ] Section 5.3.5: Saved views (save/load/delete)
|
||||
- [ ] Section 5.3.5: Export to CSV
|
||||
- [ ] Section 5.3.5: Pagination with page size selector (10/25/50/100)
|
||||
- [ ] Section 5.3.5: Table variant selection
|
||||
- [ ] Section 5.4.2: Mobile card view alternative
|
||||
- [ ] Section 3.4.1: POST /table/{t}/query with correct filter shape
|
||||
- [ ] Section 3.4.2: POST /table/{t}/count for total count
|
||||
- [ ] Section 3.7: Possible values for filter field dropdowns
|
||||
- [ ] Section 3.9: Offset/limit pagination (skip = (page-1) * pageSize)
|
||||
- [ ] Section 3.10: Tree-structured filters with subFilters
|
||||
|
||||
### Integration Check
|
||||
- [ ] Imports from Package 1 resolve: types, API client, queryKeys, QContext, layout
|
||||
- [ ] Page renders within dashboard layout with sidebar and breadcrumbs
|
||||
- [ ] Row click navigates to /app/{tableName}/{recordId} (Package 3 route)
|
||||
- [ ] Create button navigates to /app/{tableName}/create (Package 3 route)
|
||||
- [ ] Process launcher invokes process routes (Package 4)
|
||||
- [ ] Toolbar integrates with QContext (setPageHeader, setTableMetaData)
|
||||
|
||||
### API Contract Compliance
|
||||
- [ ] POST /qqq/v1/table/{tableName}/query — JSON body with filter object
|
||||
- [ ] POST /qqq/v1/table/{tableName}/count — same filter structure
|
||||
- [ ] POST /qqq/v1/table/{tableName}/possibleValues/{fieldName} — correct form data
|
||||
- [ ] Response shapes match: { records: QRecord[] } and { count, distinctCount? }
|
||||
- [ ] Query deduplication prevents stale results from overwriting fresh
|
||||
- [ ] Joins passed when exposedJoins present on table metadata
|
||||
|
||||
### Metadata-Driven Check
|
||||
- [ ] Columns generated from QTableMetaData.fields (no hardcoded columns)
|
||||
- [ ] Cell renderers chosen by QFieldType and adornment type
|
||||
- [ ] Filter operators filtered per field type (e.g., no CONTAINS on INTEGER)
|
||||
- [ ] Create button shown only if insertPermission is true
|
||||
- [ ] Table label used in page header (not table name)
|
||||
- [ ] Hidden fields (isHidden) excluded from default column set
|
||||
|
||||
### Responsive Check
|
||||
- [ ] Desktop: full data grid with toolbar, filters, pagination
|
||||
- [ ] Tablet: horizontal scroll data grid, sticky first column
|
||||
- [ ] Mobile: card-based list view as alternative to grid
|
||||
- [ ] Filter panel adapts to screen size (bottom sheet on mobile)
|
||||
- [ ] Pagination controls usable at all breakpoints
|
||||
|
||||
### Accessibility Check
|
||||
- [ ] Data grid has role="grid" or role="table"
|
||||
- [ ] Column headers have scope="col"
|
||||
- [ ] Sort buttons have aria-sort attribute
|
||||
- [ ] Filter inputs have labels
|
||||
- [ ] Row selection checkboxes have aria-label
|
||||
- [ ] Keyboard navigation within grid (arrow keys, Tab)
|
||||
- [ ] Page size selector is a labeled select
|
||||
|
||||
### Type Safety Check
|
||||
- [ ] QQueryFilter type used for all filter construction
|
||||
- [ ] QRecord[] typed for query results
|
||||
- [ ] Column definitions typed as ColumnDef<QRecord>[]
|
||||
- [ ] No `any` in filter builder state
|
||||
- [ ] Export function parameter typed
|
||||
@@ -0,0 +1,73 @@
|
||||
# Review Checklist: Package 3 — Record View and CRUD
|
||||
|
||||
### Requirements Coverage
|
||||
- [ ] Section 5.2.3: Record view at /app/{tableName}/{recordId}
|
||||
- [ ] Section 5.2.3: Create at /app/{tableName}/create
|
||||
- [ ] Section 5.2.3: Edit at /app/{tableName}/{recordId}/edit
|
||||
- [ ] Section 5.2.3: Copy at /app/{tableName}/{recordId}/copy
|
||||
- [ ] Section 5.3.2: Section-organized field display (T1 and non-T1)
|
||||
- [ ] Section 5.3.2: Field value rendering for all 12 QFieldTypes
|
||||
- [ ] Section 5.3.2: All adornment types rendered (LINK, CHIP, SIZE, FILE_DOWNLOAD, etc.)
|
||||
- [ ] Section 5.3.3: Dynamic form generation from QFieldMetaData
|
||||
- [ ] Section 5.3.3: Possible values autocomplete with debounced search
|
||||
- [ ] Section 5.3.3: File upload fields
|
||||
- [ ] Section 5.3.3: Boolean switch (null/false/true)
|
||||
- [ ] Section 5.3.3: Date/time pickers
|
||||
- [ ] Section 5.3.3: Rich text editor for HTML fields
|
||||
- [ ] Section 5.4.2: Responsive view (single-column mobile, multi-column desktop)
|
||||
- [ ] Section 5.4.2: Responsive form (single-column mobile, gridColumns tablet+)
|
||||
- [ ] Section 3.4.3: GET /table/{t}/{id} for single record
|
||||
- [ ] Section 3.4.4: POST /table/{t} for insert
|
||||
- [ ] Section 3.4.5: PUT /table/{t}/{id} for update
|
||||
- [ ] Section 3.4.6: DELETE /table/{t}/{id} for delete
|
||||
- [ ] Section 3.7: Possible values endpoint integration
|
||||
|
||||
### Integration Check
|
||||
- [ ] Imports from Package 1: types, API client, queryKeys, QContext
|
||||
- [ ] Record View navigates back to Record Query (Package 2)
|
||||
- [ ] Action menu can launch processes (Package 4 route)
|
||||
- [ ] Widget sections delegate to Package 5 components (or stubs)
|
||||
- [ ] Delete navigates back to query page
|
||||
- [ ] Unsaved changes prompt before navigation
|
||||
|
||||
### API Contract Compliance
|
||||
- [ ] GET /qqq/v1/table/{tableName}/{primaryKey} — includes associations param
|
||||
- [ ] POST /qqq/v1/table/{tableName} — multipart/form-data with field values
|
||||
- [ ] PUT /qqq/v1/table/{tableName}/{primaryKey} — multipart/form-data
|
||||
- [ ] DELETE /qqq/v1/table/{tableName}/{primaryKey} — returns deletedCount
|
||||
- [ ] POST .../possibleValues/{fieldName} — searchTerm, debounced 300ms
|
||||
- [ ] Error responses (403, 404, 500) handled gracefully
|
||||
|
||||
### Metadata-Driven Check
|
||||
- [ ] Form fields generated from QTableMetaData.sections → fieldNames → QFieldMetaData
|
||||
- [ ] Section layout follows sections metadata (T1 at top)
|
||||
- [ ] gridColumns from section metadata controls form layout
|
||||
- [ ] Field labels from metadata (never field names)
|
||||
- [ ] isEditable respected (read-only fields in edit mode)
|
||||
- [ ] isRequired reflected in Zod schema and visual indicator
|
||||
- [ ] isHidden fields excluded from view and form
|
||||
- [ ] isHeavy fields lazy-loaded or excluded from initial fetch
|
||||
- [ ] Possible values sourced from possibleValueSourceName metadata
|
||||
|
||||
### Responsive Check
|
||||
- [ ] Record View: single-column stacked sections on mobile
|
||||
- [ ] Record View: collapsible sections on mobile
|
||||
- [ ] Entity Form: single-column on mobile, gridColumns on desktop
|
||||
- [ ] Action menu: bottom sheet on mobile, dropdown on desktop
|
||||
- [ ] Save/Cancel buttons: sticky at bottom on mobile
|
||||
|
||||
### Accessibility Check
|
||||
- [ ] All form inputs have associated <label> elements
|
||||
- [ ] Required fields marked with aria-required="true"
|
||||
- [ ] Invalid fields marked with aria-invalid="true" and aria-describedby
|
||||
- [ ] Delete confirmation dialog traps focus
|
||||
- [ ] Keyboard shortcuts documented and displayed (n, e, c, d, a)
|
||||
- [ ] Help tooltips accessible via keyboard (not just hover)
|
||||
- [ ] File upload has accessible label and drag-drop instructions
|
||||
|
||||
### Type Safety Check
|
||||
- [ ] Form state typed with Zod schema inference (z.infer<typeof schema>)
|
||||
- [ ] QRecord type used for all record data
|
||||
- [ ] Field rendering switches on QFieldType enum (exhaustive)
|
||||
- [ ] Adornment rendering switches on AdornmentType enum
|
||||
- [ ] No `any` in form submission handlers
|
||||
@@ -0,0 +1,63 @@
|
||||
# Review Checklist: Package 4 — Process Execution
|
||||
|
||||
### Requirements Coverage
|
||||
- [ ] Section 5.2.4: Standalone process at /app/{processName}
|
||||
- [ ] Section 5.2.4: Table-scoped process with recordIds
|
||||
- [ ] Section 5.2.4: Table-scoped process with filterJSON
|
||||
- [ ] Section 5.3.4: Step wizard with stepper UI
|
||||
- [ ] Section 5.3.4: All 14 component types rendered
|
||||
- [ ] Section 5.3.4: Validation review screen
|
||||
- [ ] Section 5.3.4: Bulk load workflows (file mapping, value mapping, profiles)
|
||||
- [ ] Section 5.3.4: Process results summary
|
||||
- [ ] Section 5.3.4: Async job progress display
|
||||
- [ ] Section 3.5.1: POST /processes/{p}/init
|
||||
- [ ] Section 3.5.2: POST /processes/{p}/{uuid}/step/{stepName}
|
||||
- [ ] Section 3.5.3: GET /processes/{p}/{uuid}/status/{jobUUID}
|
||||
- [ ] Section 3.5.4: GET /processes/{p}/{uuid}/records
|
||||
- [ ] Section 3.5.5: GET /processes/{p}/{uuid}/cancel
|
||||
|
||||
### Integration Check
|
||||
- [ ] Form components reused from Package 3 (EntityForm, DynamicFormField)
|
||||
- [ ] Possible values autocomplete from Package 3
|
||||
- [ ] Process can be launched from Record Query toolbar (Package 2)
|
||||
- [ ] Process can be launched from Record View action menu (Package 3)
|
||||
- [ ] Process results link back to affected records (Package 3 routes)
|
||||
- [ ] Widget component type delegates to Package 5
|
||||
|
||||
### API Contract Compliance
|
||||
- [ ] POST /processes/{p}/init — multipart/form-data with values, recordsParam, file
|
||||
- [ ] POST /processes/{p}/{uuid}/step/{s} — multipart/form-data with values, file
|
||||
- [ ] GET /processes/{p}/{uuid}/status/{j} — returns QJobRunning | QJobComplete | QJobError
|
||||
- [ ] GET /processes/{p}/{uuid}/records — query params skip, limit
|
||||
- [ ] GET /processes/{p}/{uuid}/cancel — returns boolean
|
||||
- [ ] Exponential backoff polling: 1.5s initial, 1.5x multiplier, 12s max
|
||||
|
||||
### Metadata-Driven Check
|
||||
- [ ] Steps rendered from QFrontendStepMetaData.components
|
||||
- [ ] Form fields from step.formFields metadata
|
||||
- [ ] View fields from step.viewFields metadata
|
||||
- [ ] Process label from metadata (not process name)
|
||||
- [ ] ProcessMetaDataAdjustment handled (step list and field updates)
|
||||
- [ ] minInputRecords/maxInputRecords enforced
|
||||
|
||||
### Responsive Check
|
||||
- [ ] Full-screen steps on mobile
|
||||
- [ ] Stepper horizontal scrollable on mobile
|
||||
- [ ] Bottom-anchored navigation buttons on mobile
|
||||
- [ ] File upload drop zone works on touch devices
|
||||
- [ ] Progress display readable at all breakpoints
|
||||
|
||||
### Accessibility Check
|
||||
- [ ] Stepper has aria-label and step indicators
|
||||
- [ ] Current step has aria-current="step"
|
||||
- [ ] Progress bar has role="progressbar" with aria-valuenow/min/max
|
||||
- [ ] Cancel button has confirmation dialog
|
||||
- [ ] Form steps use accessible form patterns from Package 3
|
||||
- [ ] Error messages linked to relevant fields via aria-describedby
|
||||
|
||||
### Type Safety Check
|
||||
- [ ] Process response union type properly discriminated (QJobStarted | QJobComplete | QJobError)
|
||||
- [ ] ProcessMetaDataAdjustment typed
|
||||
- [ ] Step component type discriminated on QComponentType enum
|
||||
- [ ] File upload typed with File/FormData
|
||||
- [ ] No `any` in process state management
|
||||
@@ -0,0 +1,57 @@
|
||||
# Review Checklist: Package 5 — Dashboard and Widgets
|
||||
|
||||
### Requirements Coverage
|
||||
- [ ] Section 5.2.2: App Home page with dashboard layout
|
||||
- [ ] Section 5.3.6: Widget container (label, reload, export, dropdown, help)
|
||||
- [ ] Section 5.3.6: All chart types (line, bar, horizontal bar, stacked bar, pie)
|
||||
- [ ] Section 5.3.6: Statistics cards (mini, multi, standard)
|
||||
- [ ] Section 5.3.6: All block types (text, bigNumber, upOrDown, progress, button, icon, image, audio, divider, input)
|
||||
- [ ] Section 5.3.6: Record grid widget with CRUD
|
||||
- [ ] Section 5.3.6: Composite and parent widgets
|
||||
- [ ] Section 5.3.6: Widget dropdown selections with possible values
|
||||
- [ ] Section 5.3.6: Specialized widgets (pivot table, CRON, script viewer, custom component)
|
||||
- [ ] Section 3.6: GET /widget/{widgetName} with dynamic params
|
||||
|
||||
### Integration Check
|
||||
- [ ] DataGrid reused from Package 2 for record grid widget
|
||||
- [ ] EntityForm reused from Package 3 for dynamic form widget
|
||||
- [ ] Widget sections on Record View (Package 3) can render these widgets
|
||||
- [ ] Process step type WIDGET delegates to widget container
|
||||
- [ ] App Home accessible via sidebar navigation (Package 1)
|
||||
|
||||
### API Contract Compliance
|
||||
- [ ] GET /qqq/v1/widget/{widgetName} — correct path with dynamic query params
|
||||
- [ ] Dropdown selection values passed as query params
|
||||
- [ ] Request deduplication: abort previous request on dropdown change
|
||||
- [ ] Widget data response handled polymorphically per widget type
|
||||
- [ ] Error responses show per-widget error (not crash entire page)
|
||||
|
||||
### Metadata-Driven Check
|
||||
- [ ] Widget list from QAppMetaData.widgets (no hardcoded widget names)
|
||||
- [ ] Widget grid layout from QWidgetMetaData.gridColumns
|
||||
- [ ] Dropdown options from QWidgetMetaData.dropdowns + possibleValues API
|
||||
- [ ] Widget label from metadata
|
||||
- [ ] showReloadButton/showExportButton from metadata
|
||||
- [ ] Help content from metadata
|
||||
|
||||
### Responsive Check
|
||||
- [ ] Widget grid: 1-column on mobile, 2-column on tablet, N-column on desktop
|
||||
- [ ] Charts resize responsively (ResponsiveContainer)
|
||||
- [ ] Statistics cards stack vertically on mobile
|
||||
- [ ] Record grid widget scrolls horizontally on small screens
|
||||
- [ ] Widget dropdowns usable on touch devices
|
||||
|
||||
### Accessibility Check
|
||||
- [ ] Charts have aria-label with data summary
|
||||
- [ ] Statistics cards have descriptive text (not just numbers)
|
||||
- [ ] Reload/export buttons have aria-label
|
||||
- [ ] Widget container has role="region" with aria-label
|
||||
- [ ] Audio player has accessible controls
|
||||
- [ ] Image blocks have alt text
|
||||
|
||||
### Type Safety Check
|
||||
- [ ] Widget data typed per widget type (no raw `any` for response)
|
||||
- [ ] Block type discriminated union
|
||||
- [ ] Chart dataset typed
|
||||
- [ ] Record grid widget uses QRecord[] type
|
||||
- [ ] Dropdown values typed as QPossibleValue[]
|
||||
@@ -0,0 +1,64 @@
|
||||
# Review Checklist: Package 6 — Polish, Parity, and Production Readiness
|
||||
|
||||
### Requirements Coverage
|
||||
- [ ] Section 5.4: Responsive design at all 3 breakpoints for all pages
|
||||
- [ ] Section 5.1.4: WCAG 2.1 AA accessibility compliance
|
||||
- [ ] Section 5.6.1: Performance targets (FCP <1.5s, TTI <3s, LCP <2.5s)
|
||||
- [ ] Section 5.6.2: Caching strategy implemented per endpoint
|
||||
- [ ] Section 5.6.3: Code splitting, lazy loading, virtual scrolling
|
||||
- [ ] Section 5.7.1: Storybook with all component stories
|
||||
- [ ] Section 5.7.2: Playwright E2E test suite for critical flows
|
||||
- [ ] Section 5.7.3: Build pipeline with lint/format enforcement
|
||||
- [ ] Section 5.7.4: Strict TypeScript, no `any` in props
|
||||
- [ ] Section 5.5.3: Session timeout UX with state preservation
|
||||
- [ ] Section 5.1.5: Theme tokens (60+ CSS custom properties)
|
||||
- [ ] Section 5.1.5: customCss injection via data-qqq-id selectors
|
||||
- [ ] Command palette (Cmd+K) implemented
|
||||
- [ ] Keyboard shortcuts for all pages
|
||||
- [ ] Audit trail on Record View
|
||||
- [ ] Developer tools pages (table dev, record dev, script editor)
|
||||
- [ ] Report execution pages
|
||||
|
||||
### Integration Check
|
||||
- [ ] All Package 1-5 components work together end-to-end
|
||||
- [ ] Command palette searches across all metadata (tables, processes, reports)
|
||||
- [ ] Keyboard shortcuts don't conflict across pages
|
||||
- [ ] Developer view routes render within dashboard layout
|
||||
- [ ] Report pages reuse ProcessRun from Package 4
|
||||
- [ ] Theme changes propagate to all components
|
||||
|
||||
### API Contract Compliance
|
||||
- [ ] Developer endpoints: GET /data/{t}/{id}/developer
|
||||
- [ ] Script endpoints: POST .../associatedScript/{f}, GET .../logs, POST .../test
|
||||
- [ ] Audit endpoints integrated (if available)
|
||||
- [ ] All API calls use correct v1 paths
|
||||
|
||||
### Metadata-Driven Check
|
||||
- [ ] Command palette items from metadata (not hardcoded)
|
||||
- [ ] Developer view shows metadata structure dynamically
|
||||
- [ ] Theme tokens from QThemeMetaData
|
||||
- [ ] customCss loaded from backend and injected
|
||||
|
||||
### Responsive Check
|
||||
- [ ] Mobile card view for Record Query verified functional
|
||||
- [ ] All forms single-column on mobile
|
||||
- [ ] Off-canvas sidebar drawer on mobile
|
||||
- [ ] Command palette fullscreen on mobile
|
||||
- [ ] Touch targets ≥ 44px verified across all pages
|
||||
- [ ] No horizontal overflow at any breakpoint
|
||||
|
||||
### Accessibility Check
|
||||
- [ ] axe-core CI integration with zero violations
|
||||
- [ ] Skip-to-content link present
|
||||
- [ ] Focus trap in all modals/dialogs
|
||||
- [ ] aria-live regions for toasts and loading states
|
||||
- [ ] Color contrast ≥ 4.5:1 (normal) and ≥ 3:1 (large)
|
||||
- [ ] All keyboard shortcuts have visible reference
|
||||
- [ ] Screen reader tested for critical flows
|
||||
|
||||
### Type Safety Check
|
||||
- [ ] Zero TypeScript errors in `pnpm tsc --noEmit`
|
||||
- [ ] No `any` types in any component props across all packages
|
||||
- [ ] All API responses validated at boundary (Zod or equivalent)
|
||||
- [ ] All event handlers properly typed
|
||||
- [ ] Exhaustive switch statements for all enum types
|
||||
@@ -0,0 +1,559 @@
|
||||
# Semantic API Contract v1.0
|
||||
|
||||
## 3.1 Overview
|
||||
|
||||
This section formalizes the implicit QQQ REST API into an explicit, versioned contract. Any frontend implementation (not just qfmd) can be built against this specification.
|
||||
|
||||
**Version:** `v1.0`
|
||||
**Base Path:** `/qqq/v1/`
|
||||
**Content-Type:** `application/json` (responses), `multipart/form-data` (process/CRUD requests with file uploads)
|
||||
**Authentication:** Cookie-based session (`sessionUUID` cookie) for all secured endpoints
|
||||
|
||||
## 3.2 Authentication Endpoints
|
||||
|
||||
### 3.2.1 Get Authentication Metadata
|
||||
|
||||
```
|
||||
GET /qqq/v1/metaData/authentication
|
||||
Authentication: None (public)
|
||||
```
|
||||
|
||||
**Response 200:**
|
||||
```json
|
||||
{
|
||||
"name": "string",
|
||||
"type": "AUTH_0 | OAUTH2 | FULLY_ANONYMOUS | MOCK",
|
||||
"values": {
|
||||
"clientId": "string",
|
||||
"baseUrl": "string",
|
||||
"audience": "string"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2.2 Create/Manage Session
|
||||
|
||||
```
|
||||
POST /qqq/v1/manageSession
|
||||
Authentication: None (public)
|
||||
Content-Type: multipart/form-data
|
||||
```
|
||||
|
||||
**Request Body:**
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `accessToken` | string | Yes | OAuth2/OIDC access token |
|
||||
|
||||
**Response 200:**
|
||||
```json
|
||||
{
|
||||
"uuid": "string",
|
||||
"values": { }
|
||||
}
|
||||
```
|
||||
Sets `sessionUUID` cookie with the session UUID.
|
||||
|
||||
### 3.2.3 Logout
|
||||
|
||||
```
|
||||
POST /qqq/v1/logout
|
||||
Authentication: Required (sessionUUID cookie)
|
||||
```
|
||||
|
||||
**Response 200:** Empty body. Removes `sessionUUID` cookie.
|
||||
|
||||
### 3.2.4 OIDC Back-Channel Logout
|
||||
|
||||
```
|
||||
POST /qqq/v1/oidc/backchannel-logout
|
||||
Authentication: None (JWT in body)
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
```
|
||||
|
||||
**Request Body:**
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `logout_token` | string | Yes | JWT from Identity Provider containing `sub` or `sid` claims |
|
||||
|
||||
**Response 200:** Empty body.
|
||||
|
||||
## 3.3 Metadata Endpoints
|
||||
|
||||
### 3.3.1 Application Metadata
|
||||
|
||||
```
|
||||
GET /qqq/v1/metaData
|
||||
Authentication: Required
|
||||
```
|
||||
|
||||
**Query Parameters:**
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `frontendName` | string | No | Frontend identifier (e.g., "qqq-frontend-material-dashboard") |
|
||||
| `frontendVersion` | string | No | Frontend version string |
|
||||
| `applicationName` | string | No | Application instance name |
|
||||
| `applicationVersion` | string | No | Application version |
|
||||
|
||||
**Response 200:**
|
||||
```json
|
||||
{
|
||||
"apps": {
|
||||
"{appName}": {
|
||||
"name": "string",
|
||||
"label": "string",
|
||||
"children": ["QAppTreeNode"],
|
||||
"iconName": "string",
|
||||
"widgets": ["string"],
|
||||
"sections": [{
|
||||
"name": "string",
|
||||
"label": "string",
|
||||
"icon": { "name": "string", "path": "string", "color": "string" },
|
||||
"tables": ["string"],
|
||||
"processes": ["string"],
|
||||
"reports": ["string"]
|
||||
}]
|
||||
}
|
||||
},
|
||||
"appTree": [{
|
||||
"name": "string",
|
||||
"label": "string",
|
||||
"type": "TABLE | PROCESS | REPORT | APP",
|
||||
"children": ["QAppTreeNode"],
|
||||
"iconName": "string",
|
||||
"icon": { "name": "string", "path": "string", "color": "string" }
|
||||
}],
|
||||
"tables": {
|
||||
"{tableName}": {
|
||||
"name": "string",
|
||||
"label": "string",
|
||||
"isHidden": "boolean",
|
||||
"primaryKeyField": "string",
|
||||
"iconName": "string",
|
||||
"capabilities": ["TABLE_QUERY", "TABLE_GET", "TABLE_COUNT", "TABLE_INSERT", "TABLE_UPDATE", "TABLE_DELETE"],
|
||||
"readPermission": "boolean",
|
||||
"insertPermission": "boolean",
|
||||
"editPermission": "boolean",
|
||||
"deletePermission": "boolean"
|
||||
}
|
||||
},
|
||||
"processes": {
|
||||
"{processName}": {
|
||||
"name": "string",
|
||||
"label": "string",
|
||||
"tableName": "string",
|
||||
"isHidden": "boolean",
|
||||
"iconName": "string",
|
||||
"hasPermission": "boolean"
|
||||
}
|
||||
},
|
||||
"reports": { },
|
||||
"widgets": { },
|
||||
"branding": {
|
||||
"companyName": "string",
|
||||
"companyUrl": "string",
|
||||
"appName": "string",
|
||||
"logo": "string",
|
||||
"icon": "string",
|
||||
"accentColor": "string",
|
||||
"banners": { }
|
||||
},
|
||||
"helpContents": { },
|
||||
"environmentValues": { }
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3.2 Table Metadata
|
||||
|
||||
```
|
||||
GET /qqq/v1/metaData/table/{tableName}
|
||||
Authentication: Required + READ permission on table
|
||||
```
|
||||
|
||||
**Response 200:**
|
||||
```json
|
||||
{
|
||||
"name": "string",
|
||||
"label": "string",
|
||||
"isHidden": "boolean",
|
||||
"primaryKeyField": "string",
|
||||
"fields": {
|
||||
"{fieldName}": {
|
||||
"name": "string",
|
||||
"label": "string",
|
||||
"type": "STRING | INTEGER | LONG | DECIMAL | BOOLEAN | DATE | TIME | DATE_TIME | TEXT | HTML | PASSWORD | BLOB",
|
||||
"isRequired": "boolean",
|
||||
"isEditable": "boolean",
|
||||
"isHeavy": "boolean",
|
||||
"isHidden": "boolean",
|
||||
"defaultValue": "any",
|
||||
"possibleValueSourceName": "string",
|
||||
"displayFormat": "string",
|
||||
"maxLength": "number",
|
||||
"gridColumns": "number",
|
||||
"adornments": [{
|
||||
"type": "LINK | CHIP | SIZE | ERROR | RENDER_HTML | REVEAL | CODE_EDITOR | FILE_DOWNLOAD | FILE_UPLOAD | TOOLTIP",
|
||||
"values": { }
|
||||
}],
|
||||
"helpContents": [],
|
||||
"behaviors": []
|
||||
}
|
||||
},
|
||||
"sections": [{
|
||||
"name": "string",
|
||||
"label": "string",
|
||||
"tier": "string",
|
||||
"iconName": "string",
|
||||
"fieldNames": ["string"],
|
||||
"widgetName": "string",
|
||||
"isHidden": "boolean",
|
||||
"gridColumns": "number"
|
||||
}],
|
||||
"exposedJoins": [{
|
||||
"label": "string",
|
||||
"isMany": "boolean",
|
||||
"joinTable": "QTableMetaData",
|
||||
"joinPath": [{ "name": "string", "type": "ONE_TO_ONE | ONE_TO_MANY | MANY_TO_ONE", "leftTable": "string", "rightTable": "string" }]
|
||||
}],
|
||||
"capabilities": ["string"],
|
||||
"readPermission": "boolean",
|
||||
"insertPermission": "boolean",
|
||||
"editPermission": "boolean",
|
||||
"deletePermission": "boolean",
|
||||
"usesVariants": "boolean",
|
||||
"variantTableLabel": "string",
|
||||
"helpContent": { },
|
||||
"supplementalTableMetaData": { },
|
||||
"shareableTableMetaData": { }
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3.3 Process Metadata
|
||||
|
||||
```
|
||||
GET /qqq/v1/metaData/process/{processName}
|
||||
Authentication: Required
|
||||
```
|
||||
|
||||
**Response 200:**
|
||||
```json
|
||||
{
|
||||
"name": "string",
|
||||
"label": "string",
|
||||
"tableName": "string",
|
||||
"isHidden": "boolean",
|
||||
"iconName": "string",
|
||||
"hasPermission": "boolean",
|
||||
"stepFlow": "LINEAR",
|
||||
"minInputRecords": "number",
|
||||
"maxInputRecords": "number",
|
||||
"frontendSteps": [{
|
||||
"name": "string",
|
||||
"label": "string",
|
||||
"format": "string",
|
||||
"components": [{
|
||||
"type": "HELP_TEXT | BULK_EDIT_FORM | BULK_LOAD_FILE_MAPPING_FORM | BULK_LOAD_VALUE_MAPPING_FORM | BULK_LOAD_PROFILE_FORM | VALIDATION_REVIEW_SCREEN | EDIT_FORM | VIEW_FORM | DOWNLOAD_FORM | RECORD_LIST | PROCESS_SUMMARY_RESULTS | GOOGLE_DRIVE_SELECT_FOLDER | WIDGET | HTML",
|
||||
"values": { }
|
||||
}],
|
||||
"formFields": ["QFieldMetaData"],
|
||||
"viewFields": ["QFieldMetaData"],
|
||||
"recordListFields": ["QFieldMetaData"],
|
||||
"helpContents": []
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
## 3.4 Table Data Endpoints
|
||||
|
||||
### 3.4.1 Query Records
|
||||
|
||||
```
|
||||
POST /qqq/v1/table/{tableName}/query
|
||||
Authentication: Required + READ permission
|
||||
```
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"filter": {
|
||||
"criteria": [{
|
||||
"fieldName": "string",
|
||||
"operator": "EQUALS | NOT_EQUALS | NOT_EQUALS_OR_IS_NULL | IN | NOT_IN | STARTS_WITH | ENDS_WITH | CONTAINS | NOT_STARTS_WITH | NOT_ENDS_WITH | NOT_CONTAINS | LESS_THAN | LESS_THAN_OR_EQUALS | GREATER_THAN | GREATER_THAN_OR_EQUALS | IS_BLANK | IS_NOT_BLANK | BETWEEN | NOT_BETWEEN",
|
||||
"values": ["any"],
|
||||
"otherFieldName": "string"
|
||||
}],
|
||||
"orderBys": [{
|
||||
"fieldName": "string",
|
||||
"isAscending": "boolean"
|
||||
}],
|
||||
"subFilters": ["QQueryFilter"],
|
||||
"booleanOperator": "AND | OR",
|
||||
"skip": "number",
|
||||
"limit": "number"
|
||||
},
|
||||
"joins": [{
|
||||
"joinTable": "string",
|
||||
"select": "boolean",
|
||||
"type": "INNER | LEFT | RIGHT | FULL",
|
||||
"baseTableOrAlias": "string",
|
||||
"alias": "string",
|
||||
"joinName": "string"
|
||||
}],
|
||||
"tableVariant": "string"
|
||||
}
|
||||
```
|
||||
|
||||
**Response 200:**
|
||||
```json
|
||||
{
|
||||
"records": [{
|
||||
"tableName": "string",
|
||||
"recordLabel": "string",
|
||||
"values": { "{fieldName}": "any" },
|
||||
"displayValues": { "{fieldName}": "string" },
|
||||
"errors": ["string"],
|
||||
"warnings": ["string"]
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
### 3.4.2 Count Records
|
||||
|
||||
```
|
||||
POST /qqq/v1/table/{tableName}/count
|
||||
Authentication: Required + READ permission
|
||||
```
|
||||
|
||||
**Query Parameters:**
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `includeDistinct` | boolean | No | Whether to include distinct count |
|
||||
|
||||
**Request Body:** Same filter/joins structure as query.
|
||||
|
||||
**Response 200:**
|
||||
```json
|
||||
{
|
||||
"count": "number",
|
||||
"distinctCount": "number"
|
||||
}
|
||||
```
|
||||
|
||||
### 3.4.3 Get Single Record (Not Yet in v1 Specs)
|
||||
|
||||
```
|
||||
GET /qqq/v1/table/{tableName}/{primaryKey}
|
||||
Authentication: Required + READ permission
|
||||
```
|
||||
|
||||
**Query Parameters:**
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `tableVariant` | string | No | Table variant identifier |
|
||||
| `includeAssociations` | boolean | No | Include associated records |
|
||||
| `queryJoins` | string (JSON) | No | Join specifications |
|
||||
|
||||
**Response 200:** `QRecord`
|
||||
|
||||
### 3.4.4 Insert Record (Not Yet in v1 Specs)
|
||||
|
||||
```
|
||||
POST /qqq/v1/table/{tableName}
|
||||
Authentication: Required + INSERT permission
|
||||
Content-Type: multipart/form-data
|
||||
```
|
||||
|
||||
**Request Body:** Form fields matching table field names.
|
||||
|
||||
**Response 200:** `QRecord` (inserted record with generated ID)
|
||||
|
||||
### 3.4.5 Update Record (Not Yet in v1 Specs)
|
||||
|
||||
```
|
||||
PUT /qqq/v1/table/{tableName}/{primaryKey}
|
||||
Authentication: Required + EDIT permission
|
||||
Content-Type: multipart/form-data
|
||||
```
|
||||
|
||||
**Request Body:** Form fields with updated values.
|
||||
|
||||
**Response 200:** `QRecord` (updated record)
|
||||
|
||||
### 3.4.6 Delete Record (Not Yet in v1 Specs)
|
||||
|
||||
```
|
||||
DELETE /qqq/v1/table/{tableName}/{primaryKey}
|
||||
Authentication: Required + DELETE permission
|
||||
```
|
||||
|
||||
**Response 200:**
|
||||
```json
|
||||
{
|
||||
"deletedCount": "number"
|
||||
}
|
||||
```
|
||||
|
||||
## 3.5 Process Endpoints
|
||||
|
||||
### 3.5.1 Initialize Process
|
||||
|
||||
```
|
||||
POST /qqq/v1/processes/{processName}/init
|
||||
Authentication: Required
|
||||
Content-Type: multipart/form-data
|
||||
```
|
||||
|
||||
**Request Body:**
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `values` | string (JSON) | No | Initial process values |
|
||||
| `recordsParam` | string | No | One of: `recordIds`, `filterJSON`, `filterId` |
|
||||
| `recordIds` | string | Conditional | Comma-separated primary keys (when recordsParam=recordIds) |
|
||||
| `filterJSON` | string | Conditional | QQueryFilter JSON (when recordsParam=filterJSON) |
|
||||
| `stepTimeoutMillis` | number | No | Async timeout in ms (default: 3000) |
|
||||
| `file` | binary | No | File upload |
|
||||
|
||||
**Response 200:** `QJobStarted | QJobComplete | QJobError` (see union type in Section 2.1)
|
||||
|
||||
### 3.5.2 Process Step
|
||||
|
||||
```
|
||||
POST /qqq/v1/processes/{processName}/{processUUID}/step/{stepName}
|
||||
Authentication: Required
|
||||
Content-Type: multipart/form-data
|
||||
```
|
||||
|
||||
**Request Body:**
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `values` | string (JSON) | No | Step form values |
|
||||
| `stepTimeoutMillis` | number | No | Async timeout in ms (default: 3000) |
|
||||
| `file` | binary | No | File upload for this step |
|
||||
|
||||
**Response 200:** `QJobStarted | QJobComplete | QJobError`
|
||||
|
||||
### 3.5.3 Process Status (Poll)
|
||||
|
||||
```
|
||||
GET /qqq/v1/processes/{processName}/{processUUID}/status/{jobUUID}
|
||||
Authentication: Required
|
||||
```
|
||||
|
||||
**Response 200:** `QJobRunning | QJobComplete | QJobError`
|
||||
|
||||
### 3.5.4 Process Records (Not Yet in v1 Specs)
|
||||
|
||||
```
|
||||
GET /qqq/v1/processes/{processName}/{processUUID}/records
|
||||
Authentication: Required
|
||||
```
|
||||
|
||||
**Query Parameters:**
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `skip` | number | No | Records to skip |
|
||||
| `limit` | number | No | Max records to return |
|
||||
|
||||
**Response 200:**
|
||||
```json
|
||||
{
|
||||
"totalRecords": "number",
|
||||
"records": ["QRecord"]
|
||||
}
|
||||
```
|
||||
|
||||
### 3.5.5 Cancel Process (Not Yet in v1 Specs)
|
||||
|
||||
```
|
||||
GET /qqq/v1/processes/{processName}/{processUUID}/cancel
|
||||
Authentication: Required
|
||||
```
|
||||
|
||||
**Response 200:** `boolean`
|
||||
|
||||
## 3.6 Widget Endpoints (Not Yet in v1 Specs)
|
||||
|
||||
```
|
||||
GET /qqq/v1/widget/{widgetName}
|
||||
Authentication: Required
|
||||
```
|
||||
|
||||
**Query Parameters:** Dynamic, widget-specific key-value pairs.
|
||||
|
||||
**Response 200:** Widget-specific JSON payload. Shape varies by widget type (chart data, record grids, statistics, HTML content, etc.).
|
||||
|
||||
## 3.7 Possible Values Endpoints (Not Yet in v1 Specs)
|
||||
|
||||
```
|
||||
POST /qqq/v1/table/{tableName}/possibleValues/{fieldName}
|
||||
POST /qqq/v1/processes/{processName}/possibleValues/{fieldName}
|
||||
POST /qqq/v1/possibleValues/{fieldName}
|
||||
Authentication: Required
|
||||
```
|
||||
|
||||
**Request Body:**
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `searchTerm` | string | No | Text search filter |
|
||||
| `ids` | string | No | Specific IDs to look up |
|
||||
| `labels` | string | No | Specific labels to look up |
|
||||
| `values` | string | No | Pre-selected values |
|
||||
| `useCase` | string | No | Context hint for filtering |
|
||||
|
||||
**Response 200:**
|
||||
```json
|
||||
[{
|
||||
"id": "number | string",
|
||||
"label": "string"
|
||||
}]
|
||||
```
|
||||
|
||||
## 3.8 Error Response Shape
|
||||
|
||||
All error responses follow a consistent shape:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "string"
|
||||
}
|
||||
```
|
||||
|
||||
**HTTP Status Codes:**
|
||||
| Code | Meaning |
|
||||
|------|---------|
|
||||
| 200 | Success |
|
||||
| 401 | Unauthorized (no valid session) |
|
||||
| 403 | Forbidden (insufficient permissions) |
|
||||
| 404 | Not Found (table, process, or record does not exist) |
|
||||
| 500 | Internal Server Error |
|
||||
|
||||
## 3.9 Pagination Contract
|
||||
|
||||
Based on analysis of `QQueryFilter` and `TableQuerySpecV1`:
|
||||
|
||||
The pagination model is **offset/limit** based:
|
||||
- `filter.skip` (number): Number of records to skip (offset). Default: 0.
|
||||
- `filter.limit` (number): Maximum number of records to return. No enforced maximum from the API; the frontend typically defaults to 10, 25, 50, or 100.
|
||||
|
||||
There is no cursor-based pagination. The frontend manages page state by computing `skip = (pageNumber - 1) * pageSize` and sending it with each query. Total count is obtained via a separate `/count` call.
|
||||
|
||||
## 3.10 Filtering Contract
|
||||
|
||||
Based on analysis of `QQueryFilter`, `QFilterCriteria`, and `QCriteriaOperator`:
|
||||
|
||||
Filters are tree-structured with boolean operators:
|
||||
- Top-level `QQueryFilter` contains `criteria[]`, `orderBys[]`, and optional `subFilters[]`
|
||||
- `booleanOperator` (`AND` | `OR`) controls how criteria are combined
|
||||
- `subFilters` allow nested boolean logic (e.g., `(A AND B) OR (C AND D)`)
|
||||
- Each `QFilterCriteria` specifies `fieldName`, `operator`, and `values[]`
|
||||
- Special expression types are supported in values: `FilterVariableExpression`, `NowExpression`, `NowWithOffsetExpression`, `ThisOrLastPeriodExpression`
|
||||
|
||||
## 3.11 Backwards Compatibility Policy
|
||||
|
||||
Per the QQQ Semantic Versioning Policy (from wiki):
|
||||
|
||||
- **Public API surfaces** subject to SemVer include REST endpoints, request/response shapes, query parameter names, and HTTP status codes.
|
||||
- **PATCH versions** may include bug fixes, security patches, and performance optimizations without changing the API contract.
|
||||
- **MINOR versions** may add new optional fields to responses, new optional query parameters, new endpoints, and new enum values — all backward-compatible additions.
|
||||
- **MAJOR versions** may include breaking changes: removing endpoints, changing required parameters, altering response shapes, or removing enum values.
|
||||
- **Deprecation policy**: Two-version deprecation cycle. Deprecated features are annotated and kept available for at least one major version before removal.
|
||||
- **Recommendation**: The v1 API contract defined in this document should be frozen once formalized. New capabilities should be added as backward-compatible extensions (new optional fields, new endpoints). Breaking changes require a v2 contract.
|
||||
@@ -0,0 +1,879 @@
|
||||
# QQQ Admin UI Coding Conventions
|
||||
|
||||
This document establishes the foundational coding standards for the QQQ Admin UI modernization. All implementation agents must follow these conventions strictly to ensure consistency, maintainability, and compatibility across the codebase.
|
||||
|
||||
---
|
||||
|
||||
## Server Components vs Client Components
|
||||
|
||||
**Context:** The new UI will use Next.js App Router, which supports both Server Components (default) and Client Components.
|
||||
|
||||
### Server Components (Default in App Router)
|
||||
|
||||
Use Server Components for:
|
||||
- Route pages (`page.tsx` files) that fetch metadata, layout wrappers, and static content
|
||||
- Components that access server-side resources (databases, private APIs, files)
|
||||
- Authentication checks and permission gating
|
||||
- Data that doesn't require client-side interactivity
|
||||
|
||||
**Characteristics:**
|
||||
- No JavaScript shipped to client for these components
|
||||
- Can directly query databases, call private APIs, and access secrets
|
||||
- Cannot use hooks (useState, useEffect, useContext)
|
||||
- Cannot use browser APIs (window, localStorage, DOM events)
|
||||
- Reduces initial bundle size and improves Time to Interactive
|
||||
|
||||
**Example:**
|
||||
```typescript
|
||||
// app/records/page.tsx - Server Component
|
||||
import { fetchMetadata, fetchRecords } from "@/lib/api";
|
||||
import RecordGrid from "@/components/RecordGrid";
|
||||
|
||||
export default async function RecordsPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: { table: string };
|
||||
}) {
|
||||
const tableMetadata = await fetchMetadata(searchParams.table);
|
||||
const records = await fetchRecords(searchParams.table);
|
||||
|
||||
return <RecordGrid metadata={tableMetadata} records={records} />;
|
||||
}
|
||||
```
|
||||
|
||||
### Client Components (`"use client"`)
|
||||
|
||||
Use Client Components for:
|
||||
- Any component with `useState`, `useEffect`, `useReducer`, or other hooks
|
||||
- Any component with `onClick`, `onChange`, form handlers, or user events
|
||||
- Components using `useContext`, `useQuery`, `useMutation`, or custom hooks
|
||||
- Data grids, forms, modals, dropdowns—any interactive UI
|
||||
|
||||
**Characteristics:**
|
||||
- Include `"use client"` directive at top of file
|
||||
- Ship JavaScript to client (included in bundle)
|
||||
- Can use React hooks and browser APIs
|
||||
- Can subscribe to context and use custom hooks
|
||||
- Most QQQ components will be Client Components
|
||||
|
||||
**Boundary Pattern:**
|
||||
```typescript
|
||||
// page.tsx - Server Component fetches data
|
||||
export default async function Page({ params }) {
|
||||
const data = await fetchServerData(params.id);
|
||||
return <ClientComponent initialData={data} />;
|
||||
}
|
||||
|
||||
// components/ClientComponent.tsx - Client Component handles interaction
|
||||
"use client";
|
||||
import { useState } from "react";
|
||||
|
||||
export default function ClientComponent({ initialData }) {
|
||||
const [state, setState] = useState(initialData);
|
||||
return <div onClick={() => setState(...)}>{state}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
**Decision Tree:**
|
||||
- Does this component use hooks (useState, useEffect, useContext)? → Client Component
|
||||
- Does this component have onClick, onChange, or other event handlers? → Client Component
|
||||
- Is this a layout wrapper or data-fetching shell? → Consider Server Component
|
||||
- Does this component render other Client Components? → Can be Server Component
|
||||
- Does this component need browser APIs? → Client Component
|
||||
|
||||
---
|
||||
|
||||
## State Management Approach
|
||||
|
||||
QQQ's state management uses multiple complementary patterns depending on scope and lifecycle.
|
||||
|
||||
### TanStack Query for Server State
|
||||
|
||||
Use **TanStack Query** (`react-query` or `@tanstack/react-query`) for all server-side data:
|
||||
- API responses (metadata, records, counts)
|
||||
- Asynchronous data fetching with loading/error states
|
||||
- Automatic caching and invalidation
|
||||
- Optimistic updates
|
||||
- Background refetching
|
||||
|
||||
**Read Data Pattern (useQuery):**
|
||||
```typescript
|
||||
"use client";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { queryClient } from "@/lib/queryClient";
|
||||
|
||||
export default function RecordList({ tableName }: { tableName: string }) {
|
||||
const { data: records, isLoading, error } = useQuery({
|
||||
queryKey: ["table", tableName, "query"],
|
||||
queryFn: () => api.query(tableName, filter),
|
||||
staleTime: 30 * 1000, // 30 seconds
|
||||
});
|
||||
|
||||
if (isLoading) return <Loading />;
|
||||
if (error) return <Error error={error} />;
|
||||
return <Grid records={records} />;
|
||||
}
|
||||
```
|
||||
|
||||
**Mutate Data Pattern (useMutation):**
|
||||
```typescript
|
||||
"use client";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { queryClient } from "@/lib/queryClient";
|
||||
|
||||
export default function CreateButton() {
|
||||
const createMutation = useMutation({
|
||||
mutationFn: (values) => api.create(tableName, values),
|
||||
onSuccess: (newRecord) => {
|
||||
// Invalidate list to trigger refetch
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["table", tableName, "query"],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<button onClick={() => createMutation.mutate(formValues)}>
|
||||
{createMutation.isPending ? "Saving..." : "Create"}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Configuration:**
|
||||
```typescript
|
||||
// lib/queryClient.ts
|
||||
import { QueryClient } from "@tanstack/react-query";
|
||||
|
||||
export const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
gcTime: 1000 * 60 * 10, // 10 minutes (formerly cacheTime)
|
||||
retry: 1,
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
mutations: {
|
||||
retry: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### React Context for Global UI State
|
||||
|
||||
Use **React Context** for application-wide UI state that doesn't need server persistence:
|
||||
- Current theme (light/dark mode)
|
||||
- Sidebar open/closed state
|
||||
- Modal stack (which modals are open)
|
||||
- Authenticated user info
|
||||
- UI layout preferences (density, sidebar width)
|
||||
|
||||
**Example:**
|
||||
```typescript
|
||||
// context/ThemeContext.tsx
|
||||
"use client";
|
||||
import { createContext, useContext, useState } from "react";
|
||||
|
||||
const ThemeContext = createContext<{
|
||||
theme: "light" | "dark";
|
||||
setTheme: (t: "light" | "dark") => void;
|
||||
} | null>(null);
|
||||
|
||||
export function ThemeProvider({ children }) {
|
||||
const [theme, setTheme] = useState<"light" | "dark">("light");
|
||||
return (
|
||||
<ThemeContext.Provider value={{ theme, setTheme }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useTheme() {
|
||||
const ctx = useContext(ThemeContext);
|
||||
if (!ctx) throw new Error("useTheme must be used within ThemeProvider");
|
||||
return ctx;
|
||||
}
|
||||
```
|
||||
|
||||
### React Hook Form for Form State
|
||||
|
||||
Use **React Hook Form** for all form state management:
|
||||
- Field values, dirty tracking, validation state
|
||||
- Integrates with Zod schemas for validation
|
||||
- Minimal re-renders
|
||||
- Efficient field-level updates
|
||||
- Built-in submit handling
|
||||
|
||||
**Example:**
|
||||
```typescript
|
||||
"use client";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().min(1, "Name required"),
|
||||
email: z.string().email(),
|
||||
});
|
||||
|
||||
export default function RecordForm({ initialValues }) {
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors, isDirty },
|
||||
watch,
|
||||
} = useForm({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: initialValues,
|
||||
});
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<input {...register("name")} />
|
||||
{errors.name && <span>{errors.name.message}</span>}
|
||||
<button disabled={!isDirty}>Save</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### URL SearchParams for Shareable State
|
||||
|
||||
Use **URL search parameters** for state that should be shareable and bookmarkable:
|
||||
- Table filters, sort order, pagination
|
||||
- View/variant selection
|
||||
- Record ID (current record being viewed)
|
||||
- Tab/section selection
|
||||
|
||||
**Example:**
|
||||
```typescript
|
||||
"use client";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
|
||||
export default function RecordTable() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const page = parseInt(searchParams.get("page") ?? "1");
|
||||
const filter = searchParams.get("filter");
|
||||
|
||||
const handleSort = (field: string) => {
|
||||
const params = new URLSearchParams(searchParams);
|
||||
params.set("sort", field);
|
||||
params.set("page", "1"); // Reset to page 1
|
||||
router.push(`?${params.toString()}`);
|
||||
};
|
||||
|
||||
return <Grid onSort={handleSort} />;
|
||||
}
|
||||
```
|
||||
|
||||
### localStorage for User Preferences
|
||||
|
||||
Use **localStorage** for persistent user preferences (not synced to server):
|
||||
- Column widths and ordering
|
||||
- UI density (compact/normal/spacious)
|
||||
- Sidebar width
|
||||
- Last selected view per table
|
||||
- Expanded/collapsed section preferences
|
||||
|
||||
**Example:**
|
||||
```typescript
|
||||
"use client";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const STORAGE_KEY = "qqq-table-column-widths";
|
||||
|
||||
export function useColumnWidths(tableId: string) {
|
||||
const [widths, setWidths] = useState<Record<string, number>>({});
|
||||
|
||||
useEffect(() => {
|
||||
// Hydrate from localStorage
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
if (stored) {
|
||||
const all = JSON.parse(stored);
|
||||
setWidths(all[tableId] ?? {});
|
||||
}
|
||||
}, [tableId]);
|
||||
|
||||
const updateWidth = (fieldName: string, width: number) => {
|
||||
const updated = { ...widths, [fieldName]: width };
|
||||
setWidths(updated);
|
||||
|
||||
// Persist to localStorage
|
||||
const all = JSON.parse(localStorage.getItem(STORAGE_KEY) ?? "{}");
|
||||
all[tableId] = updated;
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(all));
|
||||
};
|
||||
|
||||
return { widths, updateWidth };
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling Patterns
|
||||
|
||||
### API Errors: TanStack Query Error States
|
||||
|
||||
Errors from API calls are automatically caught by TanStack Query. Always display feedback:
|
||||
|
||||
```typescript
|
||||
"use client";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
export default function RecordList({ tableName }) {
|
||||
const { data, isLoading, error, isError } = useQuery({
|
||||
queryKey: ["table", tableName],
|
||||
queryFn: () => api.query(tableName, {}),
|
||||
});
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<Alert severity="error">
|
||||
Failed to load records: {error instanceof Error ? error.message : "Unknown error"}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading) return <Skeleton />;
|
||||
return <Grid records={data} />;
|
||||
}
|
||||
```
|
||||
|
||||
### Form Validation: Zod + React Hook Form
|
||||
|
||||
Validation errors are tied to fields and displayed inline:
|
||||
|
||||
```typescript
|
||||
"use client";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
const schema = z.object({
|
||||
name: z.string().min(1, "Name is required"),
|
||||
email: z.string().email("Invalid email"),
|
||||
age: z.number().min(18, "Must be 18+"),
|
||||
});
|
||||
|
||||
export default function Form() {
|
||||
const {
|
||||
register,
|
||||
formState: { errors },
|
||||
handleSubmit,
|
||||
} = useForm({ resolver: zodResolver(schema) });
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Input
|
||||
{...register("name")}
|
||||
placeholder="Name"
|
||||
error={!!errors.name}
|
||||
helperText={errors.name?.message}
|
||||
/>
|
||||
<Input
|
||||
{...register("email")}
|
||||
placeholder="Email"
|
||||
error={!!errors.email}
|
||||
helperText={errors.email?.message}
|
||||
/>
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Route Errors: Next.js error.tsx Boundary
|
||||
|
||||
Each route segment can have an `error.tsx` file that creates an error boundary:
|
||||
|
||||
```typescript
|
||||
// app/records/[id]/error.tsx
|
||||
"use client";
|
||||
|
||||
export default function RecordError({
|
||||
error,
|
||||
reset,
|
||||
}: {
|
||||
error: Error;
|
||||
reset: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<h2 className="text-lg font-bold mb-4">Error Loading Record</h2>
|
||||
<p className="text-red-600 mb-4">{error.message}</p>
|
||||
<button onClick={reset} className="px-4 py-2 bg-blue-500 text-white">
|
||||
Try Again
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Widget Errors: ErrorBoundary Component
|
||||
|
||||
Wrap interactive components that may fail in an error boundary:
|
||||
|
||||
```typescript
|
||||
"use client";
|
||||
import { ErrorBoundary } from "react-error-boundary";
|
||||
|
||||
function ErrorFallback({ error, resetErrorBoundary }) {
|
||||
return (
|
||||
<div className="p-4 bg-red-50 border border-red-200 rounded">
|
||||
<p className="font-bold">Failed to load widget</p>
|
||||
<p className="text-sm text-gray-600">{error.message}</p>
|
||||
<button onClick={resetErrorBoundary} className="mt-2 text-blue-600 underline">
|
||||
Reload
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function WidgetContainer() {
|
||||
return (
|
||||
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
||||
<MyWidget />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 401 Handling: Axios Interceptor
|
||||
|
||||
Set up a global interceptor to catch 401 responses and redirect to login:
|
||||
|
||||
```typescript
|
||||
// lib/axiosInstance.ts
|
||||
import axios from "axios";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export function setupAxios(router: ReturnType<typeof useRouter>) {
|
||||
axios.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
if (error.response?.status === 401) {
|
||||
const returnTo = encodeURIComponent(window.location.pathname);
|
||||
router.push(`/login?returnTo=${returnTo}`);
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### General Rule
|
||||
|
||||
Never swallow errors silently. Always surface them to the user via one of: Alert component, toast notification, field-level error, or error page.
|
||||
|
||||
---
|
||||
|
||||
## Tailwind Class Ordering Convention
|
||||
|
||||
Follow the official **Tailwind CSS IntelliSense** convention for class ordering:
|
||||
`Position → Display → Sizing → Spacing → Typography → Visual → Animation`
|
||||
|
||||
**Example:**
|
||||
```html
|
||||
<div class="relative flex w-full p-4 text-sm font-medium text-gray-900 bg-white border rounded-lg shadow-sm hover:bg-gray-50 transition-colors">
|
||||
Content
|
||||
</div>
|
||||
```
|
||||
|
||||
**Breakdown:**
|
||||
- `relative` — Position
|
||||
- `flex` — Display
|
||||
- `w-full` — Sizing
|
||||
- `p-4` — Spacing
|
||||
- `text-sm font-medium text-gray-900` — Typography
|
||||
- `bg-white border rounded-lg shadow-sm` — Visual
|
||||
- `hover:bg-gray-50 transition-colors` — Animation/Interaction
|
||||
|
||||
**Multi-line (for long class lists):**
|
||||
```tsx
|
||||
<button
|
||||
className={cn(
|
||||
// Position & Display
|
||||
"relative inline-flex items-center justify-center",
|
||||
// Sizing
|
||||
"w-full px-4 py-2",
|
||||
// Typography
|
||||
"text-sm font-medium text-white",
|
||||
// Visual
|
||||
"bg-blue-600 border border-blue-700 rounded-lg shadow-sm",
|
||||
// Interaction
|
||||
"hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500",
|
||||
"disabled:opacity-50 disabled:cursor-not-allowed",
|
||||
// Animation
|
||||
"transition-colors duration-200"
|
||||
)}
|
||||
>
|
||||
Click Me
|
||||
</button>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Accessibility Requirements
|
||||
|
||||
All QQQ components must meet **WCAG 2.1 AA** standards minimum:
|
||||
|
||||
### Form Inputs and Labels
|
||||
|
||||
- Every form input must have an associated label
|
||||
- Use `htmlFor` to link labels to inputs by ID
|
||||
- For icon-only buttons, use `aria-label`
|
||||
|
||||
```typescript
|
||||
<label htmlFor="name-input">Name:</label>
|
||||
<input id="name-input" type="text" />
|
||||
|
||||
<button aria-label="Delete record" title="Delete record">
|
||||
<TrashIcon />
|
||||
</button>
|
||||
```
|
||||
|
||||
### Keyboard Navigation
|
||||
|
||||
- All interactive elements (buttons, links, inputs) must be reachable via Tab key
|
||||
- Tab order should follow visual left-to-right, top-to-bottom flow
|
||||
- Escape key should close modals
|
||||
- Enter/Space should activate buttons
|
||||
|
||||
```typescript
|
||||
<input type="text" onKeyDown={(e) => e.key === "Enter" && handleSubmit()} />
|
||||
<Modal onEscapePress={closeModal} />
|
||||
```
|
||||
|
||||
### Focus Visibility
|
||||
|
||||
- All interactive elements must have visible focus rings
|
||||
- Use Tailwind's `focus:ring-2 focus:ring-offset-2 focus:ring-blue-500` pattern
|
||||
- Never remove focus rings without providing an alternative
|
||||
|
||||
```html
|
||||
<button class="focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
Click
|
||||
</button>
|
||||
```
|
||||
|
||||
### ARIA Attributes
|
||||
|
||||
- Use `aria-label` on icon buttons (e.g., `<button aria-label="Close">×</button>`)
|
||||
- Use `aria-live="polite"` for dynamic content updates
|
||||
- Use `aria-expanded` for collapsible sections
|
||||
- Use `aria-disabled` for disabled elements
|
||||
- Use `aria-hidden="true"` for decorative icons
|
||||
|
||||
```typescript
|
||||
<button
|
||||
aria-label="Toggle sidebar"
|
||||
aria-expanded={sidebarOpen}
|
||||
onClick={toggleSidebar}
|
||||
>
|
||||
<MenuIcon />
|
||||
</button>
|
||||
|
||||
<div aria-live="polite" aria-atomic="true">
|
||||
{message}
|
||||
</div>
|
||||
```
|
||||
|
||||
### Color Contrast
|
||||
|
||||
- Minimum 4.5:1 contrast ratio for normal text (14px or smaller)
|
||||
- Minimum 3:1 contrast ratio for large text (18px+ or 14px+ bold)
|
||||
- Never rely on color alone to convey meaning (always add icon/text)
|
||||
|
||||
Use tools like [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/) to verify.
|
||||
|
||||
### Images and Icons
|
||||
|
||||
- All images must have meaningful alt text or `aria-hidden="true"` if decorative
|
||||
- SVG icons should have a title element or aria-label on parent
|
||||
|
||||
```tsx
|
||||
<img src="avatar.jpg" alt="User: John Doe" />
|
||||
<svg aria-label="Star" className="w-5 h-5">
|
||||
<title>Star icon</title>
|
||||
<path d="..." />
|
||||
</svg>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## data-qqq-id Naming Convention
|
||||
|
||||
All QQQ components must include `data-qqq-id` attributes for:
|
||||
- Automated testing (selector stability)
|
||||
- Custom CSS injection via `customCss` metadata
|
||||
- Accessibility tooling integration
|
||||
|
||||
**Pattern:** `{component-type}-{metadata-name}`
|
||||
|
||||
Where `{metadata-name}` is the `name` property from the metadata object (not the `label`).
|
||||
|
||||
### Examples by Component Type
|
||||
|
||||
**Navigation:**
|
||||
```html
|
||||
<nav data-qqq-id="sidebar-nav">
|
||||
<button data-qqq-id="sidebar-item-employees">Employees</button>
|
||||
<button data-qqq-id="sidebar-item-projects">Projects</button>
|
||||
</nav>
|
||||
```
|
||||
|
||||
**Fields (Forms and Grids):**
|
||||
```html
|
||||
<label htmlFor="field-email" data-qqq-id="field-label-email">Email</label>
|
||||
<input id="field-email" data-qqq-id="field-email" type="email" />
|
||||
|
||||
<th data-qqq-id="grid-header-email">Email</th>
|
||||
<td data-qqq-id="grid-cell-email">user@example.com</td>
|
||||
```
|
||||
|
||||
**Buttons:**
|
||||
```html
|
||||
<button data-qqq-id="button-create">Create Record</button>
|
||||
<button data-qqq-id="button-save">Save</button>
|
||||
<button data-qqq-id="button-delete">Delete</button>
|
||||
<button data-qqq-id="button-cancel">Cancel</button>
|
||||
```
|
||||
|
||||
**Filters:**
|
||||
```html
|
||||
<div data-qqq-id="filter-row-0">
|
||||
<select data-qqq-id="filter-field-0">Field</select>
|
||||
<select data-qqq-id="filter-operator-0">Operator</select>
|
||||
<input data-qqq-id="filter-value-0" type="text" />
|
||||
</div>
|
||||
```
|
||||
|
||||
**Widgets:**
|
||||
```html
|
||||
<section data-qqq-id="widget-salesChart">
|
||||
<h3 data-qqq-id="widget-label-salesChart">Sales Chart</h3>
|
||||
<canvas data-qqq-id="widget-content-salesChart"></canvas>
|
||||
</section>
|
||||
```
|
||||
|
||||
**General Rule:** If metadata has a `name: "employeeId"`, use `data-qqq-id="field-employeeId"` or `data-qqq-id="grid-header-employeeId"`, never `data-qqq-id="field-Employee ID"` or `data-qqq-id="field-employee_id"`.
|
||||
|
||||
---
|
||||
|
||||
## QQQ Theme Token System
|
||||
|
||||
The application defines **60+ CSS custom properties** in `src/styles/qqq-theme.css` for theming and customization.
|
||||
|
||||
### Core Color Tokens
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* Primary & Accent */
|
||||
--qqq-primary-color: #1976d2;
|
||||
--qqq-secondary-color: #9c27b0;
|
||||
--qqq-accent-color: var(--qqq-primary-color); /* From QBrandingMetaData.accentColor */
|
||||
--qqq-success-color: #4caf50;
|
||||
--qqq-warning-color: #ff9800;
|
||||
--qqq-error-color: #f44336;
|
||||
--qqq-info-color: #2196f3;
|
||||
}
|
||||
```
|
||||
|
||||
### Sidebar Tokens
|
||||
|
||||
```css
|
||||
:root {
|
||||
--qqq-sidebar-background: #1e293b; /* Dark slate */
|
||||
--qqq-sidebar-text: #e2e8f0; /* Light text */
|
||||
--qqq-sidebar-hover: rgba(255, 255, 255, 0.1);
|
||||
--qqq-sidebar-active: var(--qqq-accent-color);
|
||||
--qqq-sidebar-border: #334155;
|
||||
}
|
||||
```
|
||||
|
||||
### Table/Grid Tokens
|
||||
|
||||
```css
|
||||
:root {
|
||||
--qqq-grid-header-bg: #f8fafc; /* Light gray header */
|
||||
--qqq-grid-row-hover: #f1f5f9;
|
||||
--qqq-grid-border: #e2e8f0;
|
||||
--qqq-grid-text: #1e293b;
|
||||
--qqq-grid-text-muted: #64748b;
|
||||
}
|
||||
```
|
||||
|
||||
### Dark Mode Overrides
|
||||
|
||||
```css
|
||||
[data-theme="dark"] {
|
||||
--qqq-grid-header-bg: #1e293b;
|
||||
--qqq-grid-row-hover: #334155;
|
||||
--qqq-grid-border: #475569;
|
||||
--qqq-grid-text: #f1f5f9;
|
||||
--qqq-grid-text-muted: #cbd5e1;
|
||||
}
|
||||
```
|
||||
|
||||
### Usage in Components
|
||||
|
||||
Reference these tokens via CSS custom properties in Tailwind or inline styles:
|
||||
|
||||
```tsx
|
||||
// In Tailwind (configure in tailwind.config.ts)
|
||||
<div className="bg-[var(--qqq-primary-color)] text-[var(--qqq-sidebar-text)]">
|
||||
Content
|
||||
</div>
|
||||
|
||||
// Or in inline styles
|
||||
<div style={{ color: "var(--qqq-accent-color)" }}>
|
||||
Content
|
||||
</div>
|
||||
```
|
||||
|
||||
**Tailwind Config Integration:**
|
||||
```typescript
|
||||
// tailwind.config.ts
|
||||
export default {
|
||||
theme: {
|
||||
colors: {
|
||||
qqq: {
|
||||
primary: "var(--qqq-primary-color)",
|
||||
secondary: "var(--qqq-secondary-color)",
|
||||
accent: "var(--qqq-accent-color)",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## How to Handle QQQ customCss
|
||||
|
||||
QQQ allows backend to inject custom CSS via the `customCss` metadata field. The mechanism:
|
||||
|
||||
1. **Backend provides CSS:** Custom CSS rules are defined targeting `[data-qqq-id="..."]` selectors
|
||||
2. **Frontend injects dynamically:** Create a `<style>` tag in the document head and insert the CSS
|
||||
3. **All components must have data-qqq-id:** Without these identifiers, custom selectors won't match
|
||||
|
||||
### Implementation
|
||||
|
||||
```typescript
|
||||
// hooks/useCustomCSS.ts
|
||||
"use client";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export function useCustomCSS(customCss: string | undefined) {
|
||||
useEffect(() => {
|
||||
if (!customCss) return;
|
||||
|
||||
// Create or update style tag
|
||||
let styleTag = document.getElementById("qqq-custom-css");
|
||||
if (!styleTag) {
|
||||
styleTag = document.createElement("style");
|
||||
styleTag.id = "qqq-custom-css";
|
||||
document.head.appendChild(styleTag);
|
||||
}
|
||||
|
||||
styleTag.textContent = customCss;
|
||||
|
||||
return () => {
|
||||
// Keep style tag in DOM (don't remove on unmount)
|
||||
// Custom CSS should persist for the session
|
||||
};
|
||||
}, [customCss]);
|
||||
}
|
||||
```
|
||||
|
||||
### Example Backend-Provided CSS
|
||||
|
||||
```css
|
||||
/* From backend customCss field */
|
||||
[data-qqq-id="field-salary"] {
|
||||
font-weight: bold;
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
[data-qqq-id="button-delete"] {
|
||||
background-color: #d32f2f !important;
|
||||
}
|
||||
|
||||
[data-qqq-id="grid-header-status"] {
|
||||
background-color: #1976d2 !important;
|
||||
color: white;
|
||||
}
|
||||
```
|
||||
|
||||
### Best Practices
|
||||
|
||||
- Always include `!important` in custom CSS (to override component styles)
|
||||
- Selectors should use exact `data-qqq-id` matches, not partial/complex selectors
|
||||
- Never allow inline `style` attributes from backend (XSS vulnerability)
|
||||
- Validate custom CSS syntax and length before injection
|
||||
- Log any CSS injection errors to console for debugging
|
||||
|
||||
---
|
||||
|
||||
## Mock API Layer (MSW)
|
||||
|
||||
A full **MSW (Mock Service Worker) v2** mock is implemented at `src/mocks/`. It intercepts all `/qqq/v1/*` requests and returns realistic fixture data, enabling frontend development with no backend required.
|
||||
|
||||
### Activating the Mock
|
||||
|
||||
```bash
|
||||
cp .env.mock .env.local && pnpm dev
|
||||
# OR
|
||||
NEXT_PUBLIC_MOCK_API=true pnpm dev
|
||||
```
|
||||
|
||||
### Mock Data Available
|
||||
|
||||
| App | Table | Records |
|
||||
|-----|-------|---------|
|
||||
| CRM | `person` (People) | 25 |
|
||||
| CRM | `company` (Companies) | 10 |
|
||||
| CRM | `order` (Orders) | 20 |
|
||||
| Inventory | `product` (Products) | 15 |
|
||||
| Inventory | `supplier` (Suppliers) | 8 |
|
||||
|
||||
Auth type is `FULLY_ANONYMOUS` — the app auto-sessions with no login interaction.
|
||||
|
||||
Dashboard widgets: 4 statistics tiles, bar chart, line chart, record grid.
|
||||
|
||||
### Adding Handlers for New Packages
|
||||
|
||||
When a new package introduces new API endpoints (e.g., a new process, a new widget type), **add a corresponding handler** in `src/mocks/handlers/`. The existing handlers show the pattern:
|
||||
|
||||
```
|
||||
src/mocks/handlers/
|
||||
├── auth.ts # /metaData/authentication, /manageSession, /logout
|
||||
├── metadata.ts # /metaData, /metaData/table/:name, /metaData/process/:name
|
||||
├── tables.ts # /table/:name/query|count|:pk (full CRUD, in-memory store)
|
||||
├── processes.ts # /processes/:name/init|step|status|cancel
|
||||
├── widgets.ts # /widget/:widgetName
|
||||
├── possible-values.ts
|
||||
└── index.ts # combines all handlers
|
||||
```
|
||||
|
||||
In-memory mutations (POST/PUT/DELETE) persist within the browser session and reset on reload.
|
||||
|
||||
### Vitest Integration
|
||||
|
||||
The MSW node server is wired into the Vitest setup (`tests/setup.ts`). Unit tests automatically intercept API calls — no real network requests are made.
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
These conventions ensure:
|
||||
- **Consistency:** All code follows the same patterns
|
||||
- **Type Safety:** Full TypeScript coverage with strict mode
|
||||
- **Maintainability:** Shared understanding of architecture and patterns
|
||||
- **Accessibility:** WCAG 2.1 AA compliance across all components
|
||||
- **Customizability:** Custom CSS injection, theming, and metadata-driven UI
|
||||
- **Performance:** Strategic use of Server Components, TanStack Query caching, and code splitting
|
||||
|
||||
Every implementation agent must adhere to these standards. Deviations require explicit justification and team consensus.
|
||||
@@ -0,0 +1,563 @@
|
||||
# QQQ TypeScript Type Definitions
|
||||
|
||||
> These types are ported from `qqq-frontend-core/src/model/`. Every implementation agent must use these types exactly as defined.
|
||||
|
||||
## Core Data Types
|
||||
|
||||
### QRecord
|
||||
Single database record with values, display representations, and metadata.
|
||||
```typescript
|
||||
export interface QRecord {
|
||||
tableName: string; // Name of the source table
|
||||
recordLabel: string; // Display-friendly record identifier
|
||||
values: Record<string, any>; // Raw field values keyed by field name
|
||||
displayValues: Record<string, string>; // Formatted/display string representations
|
||||
associatedRecords?: Record<string, QRecord[]>; // Related records from joins
|
||||
errors?: string[]; // Validation errors encountered
|
||||
warnings?: string[]; // Non-fatal warnings
|
||||
}
|
||||
```
|
||||
|
||||
### QPossibleValue
|
||||
Represents a selectable option from a dropdown or autocomplete field.
|
||||
```typescript
|
||||
export interface QPossibleValue {
|
||||
id: number | string; // Unique identifier for the value
|
||||
label: string; // Display label shown to user
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Metadata Types
|
||||
|
||||
### QInstance
|
||||
Top-level metadata container describing the entire application instance.
|
||||
```typescript
|
||||
export interface QInstance {
|
||||
apps: Record<string, QAppMetaData>; // All defined apps keyed by name
|
||||
appTree: QAppTreeNode[]; // Hierarchical navigation tree
|
||||
tables: Record<string, QTableMetaData>; // All tables keyed by name
|
||||
processes: Record<string, QProcessMetaData>; // All processes keyed by name
|
||||
reports: Record<string, any>; // All reports keyed by name
|
||||
widgets: Record<string, QWidgetMetaData>; // All widgets keyed by name
|
||||
branding: QBrandingMetaData; // Application branding and theme
|
||||
helpContents: Record<string, QHelpContent>; // Context-specific help
|
||||
environmentValues: Record<string, string>; // Environment configuration
|
||||
}
|
||||
```
|
||||
|
||||
### QTableMetaData
|
||||
Complete definition of a table, including fields, sections, permissions, and UI hints.
|
||||
```typescript
|
||||
export interface QTableMetaData {
|
||||
name: string; // Unique table identifier
|
||||
label: string; // User-facing table name
|
||||
isHidden: boolean; // Hide from navigation if true
|
||||
primaryKeyField: string; // Name of the primary key field
|
||||
fields: Record<string, QFieldMetaData>; // Field definitions
|
||||
sections: QTableSection[]; // Form/view section organization
|
||||
exposedJoins: QExposedJoin[]; // Relationships to other tables
|
||||
capabilities: Capability[]; // Operations enabled (QUERY, GET, INSERT, UPDATE, DELETE, COUNT)
|
||||
readPermission: boolean; // User can read records
|
||||
insertPermission: boolean; // User can create records
|
||||
editPermission: boolean; // User can modify records
|
||||
deletePermission: boolean; // User can delete records
|
||||
usesVariants: boolean; // Table has multiple views
|
||||
variantTableLabel: string; // Label for variant selector
|
||||
helpContent?: QHelpContent; // Table-level help
|
||||
supplementalTableMetaData?: Record<string, any>; // Additional metadata
|
||||
shareableTableMetaData?: Record<string, any>; // Shareable configuration
|
||||
}
|
||||
```
|
||||
|
||||
### QFieldMetaData
|
||||
Defines a single field within a table, including type, constraints, and display behavior.
|
||||
```typescript
|
||||
export interface QFieldMetaData {
|
||||
name: string; // Field identifier
|
||||
label: string; // User-facing field name
|
||||
type: QFieldType; // Data type (STRING, INTEGER, BOOLEAN, DATE, etc.)
|
||||
isRequired: boolean; // Must have a value
|
||||
isEditable: boolean; // User can modify this field
|
||||
isHeavy: boolean; // Large data; load on demand
|
||||
isHidden: boolean; // Don't display in UI
|
||||
defaultValue?: any; // Initial value for new records
|
||||
possibleValueSourceName?: string; // Reference to dropdown options endpoint
|
||||
displayFormat?: string; // Formatting pattern for display (e.g., "currency", "percentage")
|
||||
maxLength?: number; // Maximum string/text length
|
||||
gridColumns?: number; // Width hint for responsive layouts
|
||||
adornments: FieldAdornment[]; // Visual/interactive enhancements (icons, links, file uploads)
|
||||
helpContents?: QHelpContent[]; // Field-level help
|
||||
behaviors?: string[]; // Special behaviors (e.g., "readonly_after_create")
|
||||
}
|
||||
```
|
||||
|
||||
### QProcessMetaData
|
||||
Definition of a process or workflow with its steps and configuration.
|
||||
```typescript
|
||||
export interface QProcessMetaData {
|
||||
name: string; // Process identifier
|
||||
label: string; // User-facing process name
|
||||
tableName: string; // Associated table name
|
||||
isHidden: boolean; // Hide from navigation
|
||||
iconName: string; // Icon reference for UI
|
||||
hasPermission: boolean; // User can execute this process
|
||||
stepFlow: "LINEAR"; // Step flow type (only LINEAR currently documented)
|
||||
minInputRecords: number; // Minimum records required as input
|
||||
maxInputRecords: number; // Maximum records allowed as input
|
||||
frontendSteps: QFrontendStepMetaData[]; // Step definitions
|
||||
}
|
||||
```
|
||||
|
||||
### QFrontendStepMetaData
|
||||
Describes a single step within a process workflow.
|
||||
```typescript
|
||||
export interface QFrontendStepMetaData {
|
||||
name: string; // Step identifier
|
||||
label: string; // Display name
|
||||
format?: string; // Layout format hint
|
||||
components: QFrontendComponent[]; // UI components to render
|
||||
formFields?: QFieldMetaData[]; // Fields for input forms
|
||||
viewFields?: QFieldMetaData[]; // Fields for display-only views
|
||||
recordListFields?: QFieldMetaData[]; // Fields for record list display
|
||||
helpContents?: QHelpContent[]; // Step-level help
|
||||
}
|
||||
```
|
||||
|
||||
### QAppMetaData
|
||||
Defines an application or dashboard with its sections and widgets.
|
||||
```typescript
|
||||
export interface QAppMetaData {
|
||||
name: string; // App identifier
|
||||
label: string; // Display name
|
||||
children: QAppTreeNode[]; // Child apps or items
|
||||
iconName: string; // Icon reference
|
||||
widgets: string[]; // Dashboard widget names
|
||||
sections: QAppSection[]; // Logical sections within app
|
||||
}
|
||||
```
|
||||
|
||||
### QAppTreeNode
|
||||
Represents a single node in the hierarchical application navigation tree.
|
||||
```typescript
|
||||
export interface QAppTreeNode {
|
||||
name: string; // Node identifier
|
||||
label: string; // Display name
|
||||
type: QAppNodeType; // Node type (TABLE, PROCESS, REPORT, APP)
|
||||
children?: QAppTreeNode[]; // Nested children
|
||||
iconName?: string; // Icon reference
|
||||
icon?: QIcon; // Detailed icon configuration
|
||||
}
|
||||
```
|
||||
|
||||
### QAppNodeType
|
||||
Type discriminator for navigation tree nodes.
|
||||
```typescript
|
||||
export type QAppNodeType = "TABLE" | "PROCESS" | "REPORT" | "APP";
|
||||
```
|
||||
|
||||
### QAppSection
|
||||
Represents a section within an app (e.g., "Users", "Settings") containing tables/processes/reports.
|
||||
```typescript
|
||||
export interface QAppSection {
|
||||
name: string; // Section identifier
|
||||
label: string; // Display name
|
||||
icon?: QIcon; // Section icon
|
||||
tables: string[]; // Table names in section
|
||||
processes: string[]; // Process names in section
|
||||
reports: string[]; // Report names in section
|
||||
}
|
||||
```
|
||||
|
||||
### QWidgetMetaData
|
||||
Configuration for a dashboard widget.
|
||||
```typescript
|
||||
export interface QWidgetMetaData {
|
||||
name: string; // Widget identifier
|
||||
label: string; // Display name
|
||||
type?: string; // Widget type (chart, grid, statistic, etc.)
|
||||
hasPermission: boolean; // User can view this widget
|
||||
dropdowns?: Record<string, any>; // Dropdown selections for widget
|
||||
helpContent?: QHelpContent; // Widget-level help
|
||||
}
|
||||
```
|
||||
|
||||
### QAuthenticationMetaData
|
||||
Authentication system configuration.
|
||||
```typescript
|
||||
export interface QAuthenticationMetaData {
|
||||
name: string; // Authentication provider name
|
||||
type: "AUTH_0" | "OAUTH2" | "FULLY_ANONYMOUS" | "MOCK"; // Auth type
|
||||
values: {
|
||||
clientId?: string; // OAuth client ID
|
||||
baseUrl?: string; // Provider base URL
|
||||
audience?: string; // OAuth audience claim
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### QBrandingMetaData
|
||||
Application branding, theming, and custom styling.
|
||||
```typescript
|
||||
export interface QBrandingMetaData {
|
||||
companyName: string; // Company/organization name
|
||||
companyUrl: string; // Company website URL
|
||||
appName: string; // Application name
|
||||
logo?: string; // Company logo URL or base64
|
||||
icon?: string; // Application icon URL or base64
|
||||
accentColor?: string; // Primary accent color (hex code)
|
||||
banners?: Record<string, Banner>; // Environment/status banners
|
||||
}
|
||||
```
|
||||
|
||||
### QTableSection
|
||||
Organizes fields within a table form or view into logical groupings.
|
||||
```typescript
|
||||
export interface QTableSection {
|
||||
name: string; // Section identifier
|
||||
label: string; // Display name
|
||||
tier?: string; // Priority tier (e.g., "basic", "advanced")
|
||||
iconName?: string; // Icon reference
|
||||
fieldNames: string[]; // Field names in this section
|
||||
widgetName?: string; // Optional embedded widget
|
||||
isHidden: boolean; // Hide entire section
|
||||
gridColumns?: number; // Column count for responsive layout
|
||||
}
|
||||
```
|
||||
|
||||
### QExposedJoin
|
||||
Represents a relationship between tables.
|
||||
```typescript
|
||||
export interface QExposedJoin {
|
||||
label: string; // Relationship display name
|
||||
isMany: boolean; // One-to-many (true) or one-to-one (false)
|
||||
joinTable?: QTableMetaData; // Target table metadata
|
||||
joinPath?: QJoinMetaData[]; // Path steps through intermediate tables
|
||||
}
|
||||
```
|
||||
|
||||
### QJoinMetaData
|
||||
Single step in a join path between tables.
|
||||
```typescript
|
||||
export interface QJoinMetaData {
|
||||
name: string; // Join identifier
|
||||
type: "ONE_TO_ONE" | "ONE_TO_MANY" | "MANY_TO_ONE"; // Relationship type
|
||||
leftTable: string; // Source table name
|
||||
rightTable: string; // Target table name
|
||||
}
|
||||
```
|
||||
|
||||
### QReportMetaData
|
||||
Definition of a report (typically backed by a process).
|
||||
```typescript
|
||||
export interface QReportMetaData {
|
||||
name: string; // Report identifier
|
||||
label: string; // Display name
|
||||
isHidden: boolean; // Hide from navigation
|
||||
hasPermission: boolean; // User can access report
|
||||
}
|
||||
```
|
||||
|
||||
### QHelpContent
|
||||
Contextual help text and rich content for UI elements.
|
||||
```typescript
|
||||
export interface QHelpContent {
|
||||
title?: string; // Help section title
|
||||
content?: string; // Help text or HTML
|
||||
links?: Array<{ label: string; url: string }>; // Related links
|
||||
roles?: string[]; // Show only to specific user roles
|
||||
}
|
||||
```
|
||||
|
||||
### QIcon
|
||||
Icon configuration with name, path, and styling.
|
||||
```typescript
|
||||
export interface QIcon {
|
||||
name: string; // Icon identifier
|
||||
path?: string; // SVG path or icon URL
|
||||
color?: string; // Icon color (hex or CSS color)
|
||||
}
|
||||
```
|
||||
|
||||
### QTableVariant
|
||||
Alternative view/configuration of a table.
|
||||
```typescript
|
||||
export interface QTableVariant {
|
||||
name: string; // Variant identifier
|
||||
label: string; // Display name
|
||||
description?: string; // Variant description
|
||||
}
|
||||
```
|
||||
|
||||
### Banner
|
||||
Status or environment banner configuration.
|
||||
```typescript
|
||||
export interface Banner {
|
||||
text: string; // Banner text
|
||||
severity: "info" | "warning" | "error"; // Visual severity level
|
||||
color?: string; // Custom color (hex)
|
||||
dismissible: boolean; // User can close banner
|
||||
}
|
||||
```
|
||||
|
||||
### FieldAdornment
|
||||
Visual or interactive enhancement applied to a field (links, chips, file uploads, etc.).
|
||||
```typescript
|
||||
export interface FieldAdornment {
|
||||
type: AdornmentType; // Adornment category
|
||||
values?: Record<string, any>; // Configuration-specific values
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Enum Types
|
||||
|
||||
### QFieldType
|
||||
Enumeration of all supported field data types.
|
||||
```typescript
|
||||
export type QFieldType =
|
||||
| "STRING" // Short text
|
||||
| "INTEGER" // Whole number
|
||||
| "LONG" // Large whole number
|
||||
| "DECIMAL" // Floating-point number
|
||||
| "BOOLEAN" // True/false
|
||||
| "DATE" // Date (no time)
|
||||
| "TIME" // Time (no date)
|
||||
| "DATE_TIME" // Date and time
|
||||
| "TEXT" // Long text
|
||||
| "HTML" // HTML content
|
||||
| "PASSWORD" // Sensitive text (masked in UI)
|
||||
| "BLOB"; // Binary large object
|
||||
```
|
||||
|
||||
### QComponentType
|
||||
Component types available within process steps.
|
||||
```typescript
|
||||
export type QComponentType =
|
||||
| "HELP_TEXT" // Static help text
|
||||
| "BULK_EDIT_FORM" // Multi-record edit form
|
||||
| "BULK_LOAD_FILE_MAPPING_FORM" // CSV column mapping
|
||||
| "BULK_LOAD_VALUE_MAPPING_FORM" // Source-to-DB value mapping
|
||||
| "BULK_LOAD_PROFILE_FORM" // Saved bulk load profile
|
||||
| "VALIDATION_REVIEW_SCREEN" // Review validation errors
|
||||
| "EDIT_FORM" // Record edit form
|
||||
| "VIEW_FORM" // Record display form
|
||||
| "DOWNLOAD_FORM" // File download form
|
||||
| "RECORD_LIST" // Records display
|
||||
| "PROCESS_SUMMARY_RESULTS" // Process completion summary
|
||||
| "GOOGLE_DRIVE_SELECT_FOLDER" // Google Drive folder picker
|
||||
| "WIDGET" // Custom widget
|
||||
| "HTML"; // Custom HTML content
|
||||
```
|
||||
|
||||
### AdornmentType
|
||||
Types of field enhancements.
|
||||
```typescript
|
||||
export type AdornmentType =
|
||||
| "LINK" // Clickable hyperlink
|
||||
| "CHIP" // Tag/chip display
|
||||
| "SIZE" // Bytesize formatter
|
||||
| "ERROR" // Error state indicator
|
||||
| "RENDER_HTML" // Render as HTML
|
||||
| "REVEAL" // Show/hide toggle
|
||||
| "CODE_EDITOR" // Code editing interface
|
||||
| "FILE_DOWNLOAD" // Download link
|
||||
| "FILE_UPLOAD" // File upload control
|
||||
| "TOOLTIP"; // Hover tooltip
|
||||
```
|
||||
|
||||
### Capability
|
||||
Feature toggle enumerations for table operations.
|
||||
```typescript
|
||||
export type Capability =
|
||||
| "TABLE_QUERY" // List/query records
|
||||
| "TABLE_GET" // Fetch single record
|
||||
| "TABLE_COUNT" // Count records
|
||||
| "TABLE_INSERT" // Create new record
|
||||
| "TABLE_UPDATE" // Edit record
|
||||
| "TABLE_DELETE"; // Delete record
|
||||
```
|
||||
|
||||
### QCriteriaOperator
|
||||
Filter operator enumerations for building query criteria.
|
||||
```typescript
|
||||
export type QCriteriaOperator =
|
||||
| "EQUALS" // Exact match
|
||||
| "NOT_EQUALS" // Not equal
|
||||
| "NOT_EQUALS_OR_IS_NULL" // Not equal or missing
|
||||
| "IN" // Within list
|
||||
| "NOT_IN" // Not in list
|
||||
| "STARTS_WITH" // String prefix
|
||||
| "ENDS_WITH" // String suffix
|
||||
| "CONTAINS" // Substring match
|
||||
| "NOT_STARTS_WITH" // Exclude prefix
|
||||
| "NOT_ENDS_WITH" // Exclude suffix
|
||||
| "NOT_CONTAINS" // Exclude substring
|
||||
| "LESS_THAN" // Numeric/date <
|
||||
| "LESS_THAN_OR_EQUALS" // Numeric/date <=
|
||||
| "GREATER_THAN" // Numeric/date >
|
||||
| "GREATER_THAN_OR_EQUALS" // Numeric/date >=
|
||||
| "IS_BLANK" // Empty/null
|
||||
| "IS_NOT_BLANK" // Non-empty/not null
|
||||
| "BETWEEN" // Within range (inclusive)
|
||||
| "NOT_BETWEEN"; // Outside range
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Query Types
|
||||
|
||||
### QQueryFilter
|
||||
Tree-structured query definition with criteria, ordering, and pagination.
|
||||
```typescript
|
||||
export interface QQueryFilter {
|
||||
criteria: QFilterCriteria[]; // Filter conditions
|
||||
orderBys?: QFilterOrderBy[]; // Sort specifications
|
||||
subFilters?: QQueryFilter[]; // Nested boolean logic
|
||||
booleanOperator: "AND" | "OR"; // How criteria/subFilters combine
|
||||
skip: number; // Offset for pagination
|
||||
limit: number; // Max records to return
|
||||
}
|
||||
```
|
||||
|
||||
### QFilterCriteria
|
||||
Single filter condition within a QQueryFilter.
|
||||
```typescript
|
||||
export interface QFilterCriteria {
|
||||
fieldName: string; // Field being filtered
|
||||
operator: QCriteriaOperator; // Comparison operator
|
||||
values: (string | number | boolean | FilterVariableExpression | NowExpression | NowWithOffsetExpression | ThisOrLastPeriodExpression)[]; // Values to compare
|
||||
otherFieldName?: string; // For field-to-field comparisons
|
||||
}
|
||||
```
|
||||
|
||||
### QFilterOrderBy
|
||||
Sort specification within a query.
|
||||
```typescript
|
||||
export interface QFilterOrderBy {
|
||||
fieldName: string; // Field to sort by
|
||||
isAscending: boolean; // Sort direction
|
||||
}
|
||||
```
|
||||
|
||||
### QueryJoin
|
||||
Join specification for including related tables in a query.
|
||||
```typescript
|
||||
export interface QueryJoin {
|
||||
joinTable: string; // Table name to join
|
||||
select: boolean; // Include in results
|
||||
type: "INNER" | "LEFT" | "RIGHT" | "FULL"; // Join type
|
||||
baseTableOrAlias?: string; // Base table or alias
|
||||
alias?: string; // Join alias
|
||||
joinName?: string; // Named join identifier
|
||||
}
|
||||
```
|
||||
|
||||
### FilterVariableExpression
|
||||
Placeholder for dynamic filter variable substitution.
|
||||
```typescript
|
||||
export interface FilterVariableExpression {
|
||||
type: "FILTER_VARIABLE"; // Expression type marker
|
||||
variableName: string; // Variable name to substitute
|
||||
}
|
||||
```
|
||||
|
||||
### NowExpression
|
||||
Represents the current date/time in filter expressions.
|
||||
```typescript
|
||||
export interface NowExpression {
|
||||
type: "NOW"; // Expression type marker
|
||||
}
|
||||
```
|
||||
|
||||
### NowWithOffsetExpression
|
||||
Represents current date/time with offset (e.g., +7 days, -1 month).
|
||||
```typescript
|
||||
export interface NowWithOffsetExpression {
|
||||
type: "NOW_WITH_OFFSET"; // Expression type marker
|
||||
offsetValue: number; // Offset magnitude
|
||||
offsetUnit: "DAY" | "WEEK" | "MONTH" | "YEAR"; // Offset unit
|
||||
isNegativeOffset: boolean; // Add (false) or subtract (true)
|
||||
}
|
||||
```
|
||||
|
||||
### ThisOrLastPeriodExpression
|
||||
Represents "this period" or "last period" relative expressions (week, month, quarter, year).
|
||||
```typescript
|
||||
export interface ThisOrLastPeriodExpression {
|
||||
type: "THIS_OR_LAST_PERIOD"; // Expression type marker
|
||||
period: "WEEK" | "MONTH" | "QUARTER" | "YEAR"; // Time period
|
||||
isLast: boolean; // This period (false) or last period (true)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Process Job Types
|
||||
|
||||
### QJobStarted
|
||||
Indicates async job has been initiated; frontend should poll for status.
|
||||
```typescript
|
||||
export interface QJobStarted {
|
||||
processUUID: string; // Process execution ID
|
||||
jobUUID: string; // Async job ID for polling
|
||||
}
|
||||
```
|
||||
|
||||
### QJobRunning
|
||||
Indicates job is in progress with optional progress information.
|
||||
```typescript
|
||||
export interface QJobRunning {
|
||||
processUUID: string; // Process execution ID
|
||||
message: string; // Status message
|
||||
current?: number; // Current progress count
|
||||
total?: number; // Total progress count
|
||||
}
|
||||
```
|
||||
|
||||
### QJobComplete
|
||||
Indicates job completed successfully with next step information.
|
||||
```typescript
|
||||
export interface QJobComplete {
|
||||
processUUID: string; // Process execution ID
|
||||
values: Record<string, any>; // Step output values
|
||||
nextStep?: string; // Name of next step to execute
|
||||
backStep?: string; // Name of previous step (for back navigation)
|
||||
processMetaDataAdjustment?: ProcessMetaDataAdjustment; // Dynamic step/field changes
|
||||
}
|
||||
```
|
||||
|
||||
### QJobError
|
||||
Indicates job failed with error details.
|
||||
```typescript
|
||||
export interface QJobError {
|
||||
processUUID: string; // Process execution ID
|
||||
error: string; // Technical error details
|
||||
userFacingError?: string; // User-friendly error message
|
||||
}
|
||||
```
|
||||
|
||||
### ProcessMetaDataAdjustment
|
||||
Dynamic modifications to process metadata after a step completes.
|
||||
```typescript
|
||||
export interface ProcessMetaDataAdjustment {
|
||||
addedSteps?: QFrontendStepMetaData[]; // Steps to insert
|
||||
removedSteps?: string[]; // Step names to remove
|
||||
modifiedFields?: Record<string, Partial<QFieldMetaData>>; // Field changes
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Usage Notes
|
||||
|
||||
- All type names prefixed with `Q` follow the QQQ naming convention
|
||||
- Use `interface` (not `class`) as these represent data contracts
|
||||
- Import and use these types in all frontend implementations to ensure API contract alignment
|
||||
- Never create custom type aliases; always reference these canonical definitions
|
||||
- Mark fields as optional using `?` only when explicitly documented as optional
|
||||
- All `Record<string, X>` mappings use string keys (field/table/process/widget names)
|
||||
- Process job types form a discriminated union; use the `processUUID` and type presence to determine state
|
||||
- Filter expressions support runtime variable substitution via `FilterVariableExpression` types
|
||||
@@ -0,0 +1,451 @@
|
||||
# Master Architect Agent Prompt
|
||||
|
||||
## Your Role
|
||||
|
||||
You are the **Master Architect Agent** for the QQQ Admin UI Modernization project. Your job is to take the requirements specification and produce a set of **detailed implementation plans** — one per work package — that implementation agents can execute independently to build the new admin UI.
|
||||
|
||||
You do NOT write application code yourself. You produce plans that are precise enough that an implementation agent (an AI coding agent with access to the codebase) can follow them and produce correct, integrating code on the first pass.
|
||||
|
||||
---
|
||||
|
||||
## Inputs Available to You
|
||||
|
||||
You have access to the full codebase. The following files are critical:
|
||||
|
||||
### Requirements Specification
|
||||
- **`docs/qqq-admin-modernization-requirements.md`** — The complete requirements document. Read this FIRST, in its entirety, before doing anything else.
|
||||
|
||||
### Existing Source Code (Reference Implementation)
|
||||
These files represent the current system you are replacing. Read them to understand existing patterns, edge cases, and integration points:
|
||||
|
||||
- **API Client Layer:**
|
||||
- `qqq-frontend-core/src/controllers/QController.ts` (~1169 lines) — Current API client, all HTTP calls
|
||||
- `qqq-frontend-core/src/controllers/QControllerV1.ts` (~1117 lines) — V1 API client
|
||||
- `qqq-frontend-core/src/model/**/*.ts` (~42 files) — All TypeScript domain models
|
||||
|
||||
- **Current Frontend UI:**
|
||||
- `qqq-frontend-material-dashboard/src/App.tsx` — Route generation, auth flow, layout structure
|
||||
- `qqq-frontend-material-dashboard/src/qqq/pages/**` — All page components
|
||||
- `qqq-frontend-material-dashboard/src/qqq/components/**` — All shared components (~217 files)
|
||||
|
||||
- **Backend API (source of truth for endpoints):**
|
||||
- `qqq/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/specs/v1/` — All v1 endpoint specs
|
||||
- `qqq/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/QApplicationJavalinServer.java` — Route registration
|
||||
|
||||
---
|
||||
|
||||
## What You Must Produce
|
||||
|
||||
### Output Structure
|
||||
|
||||
Create a directory `docs/implementation-plans/` containing:
|
||||
|
||||
```
|
||||
docs/implementation-plans/
|
||||
├── 00-project-overview.md # Dependency graph, sequencing, shared conventions
|
||||
├── 01-scaffold-and-auth.md # Work Package 1
|
||||
├── 02-record-query.md # Work Package 2
|
||||
├── 03-record-view-and-crud.md # Work Package 3
|
||||
├── 04-process-execution.md # Work Package 4
|
||||
├── 05-dashboard-and-widgets.md # Work Package 5
|
||||
├── 06-polish-and-parity.md # Work Package 6
|
||||
├── shared-context/
|
||||
│ ├── api-contract.md # Extracted from requirements Section 3 — given to every agent
|
||||
│ ├── type-definitions.md # Key TypeScript types every agent needs
|
||||
│ └── coding-conventions.md # Project-wide patterns and rules
|
||||
└── review-checklists/
|
||||
├── review-01-scaffold.md # Review criteria for Package 1
|
||||
├── review-02-query.md # Review criteria for Package 2
|
||||
├── review-03-crud.md # Review criteria for Package 3
|
||||
├── review-04-processes.md # Review criteria for Package 4
|
||||
├── review-05-widgets.md # Review criteria for Package 5
|
||||
└── review-06-polish.md # Review criteria for Package 6
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## How to Build Each Work Package Plan
|
||||
|
||||
### Step 1: Read the Requirements and Code
|
||||
|
||||
Before writing any plans, read:
|
||||
1. The full requirements doc (`docs/qqq-admin-modernization-requirements.md`)
|
||||
2. The current `App.tsx` to understand route generation
|
||||
3. The current `QController.ts` and `QControllerV1.ts` to understand every API call
|
||||
4. A representative sample of page components (RecordQuery, RecordView, EntityForm, ProcessRun) to understand the current implementation patterns
|
||||
5. A representative sample of shared components to understand the component architecture
|
||||
|
||||
### Step 2: Produce the Project Overview (`00-project-overview.md`)
|
||||
|
||||
This file establishes the shared foundation. It must contain:
|
||||
|
||||
**Technology Stack Decisions:**
|
||||
- Next.js App Router (as recommended in requirements Section 4.3)
|
||||
- TypeScript in strict mode
|
||||
- Tailwind CSS for styling (utility-first, replaces MUI's styling system)
|
||||
- shadcn/ui as the component primitive library (headless, composable, Tailwind-native)
|
||||
- TanStack Table for the data grid (replaces MUI X DataGrid Pro)
|
||||
- React Hook Form + Zod for forms (replaces Formik + Yup)
|
||||
- TanStack Query for data fetching and caching
|
||||
- Vitest for unit tests, Playwright for E2E
|
||||
|
||||
**Project Structure:**
|
||||
Define the exact directory layout for the new project. Example:
|
||||
```
|
||||
src/
|
||||
├── app/ # Next.js App Router pages
|
||||
│ ├── (auth)/ # Auth routes (login, callback)
|
||||
│ ├── (dashboard)/ # Authenticated routes
|
||||
│ │ ├── app/
|
||||
│ │ │ ├── [appName]/
|
||||
│ │ │ ├── [tableName]/
|
||||
│ │ │ │ ├── page.tsx # RecordQuery
|
||||
│ │ │ │ ├── create/page.tsx # Create record
|
||||
│ │ │ │ ├── [recordId]/
|
||||
│ │ │ │ │ ├── page.tsx # RecordView
|
||||
│ │ │ │ │ └── edit/page.tsx # Edit record
|
||||
│ │ │ └── [processName]/
|
||||
│ │ │ └── page.tsx # ProcessRun
|
||||
│ │ └── layout.tsx # Dashboard layout (sidebar + nav)
|
||||
│ └── layout.tsx # Root layout
|
||||
├── components/
|
||||
│ ├── layout/ # Sidebar, navbar, breadcrumbs
|
||||
│ ├── records/ # Record-related components
|
||||
│ ├── forms/ # Dynamic form components
|
||||
│ ├── query/ # Filter builder, column config
|
||||
│ ├── process/ # Process step components
|
||||
│ ├── widgets/ # Widget system
|
||||
│ └── ui/ # shadcn/ui primitives
|
||||
├── lib/
|
||||
│ ├── api/ # API client (typed, generated from contract)
|
||||
│ ├── auth/ # Auth providers and session management
|
||||
│ ├── hooks/ # Shared React hooks
|
||||
│ └── utils/ # Utility functions
|
||||
├── types/ # Shared TypeScript types (ported from qqq-frontend-core)
|
||||
└── styles/ # Global styles, Tailwind config, theme tokens
|
||||
```
|
||||
|
||||
**Dependency Graph:**
|
||||
```
|
||||
Package 1 (Scaffold + Auth)
|
||||
↓
|
||||
Package 2 (Record Query) ←→ Package 3 (Record View + CRUD)
|
||||
↓ ↓
|
||||
Package 4 (Process Execution)
|
||||
↓
|
||||
Package 5 (Dashboard + Widgets)
|
||||
↓
|
||||
Package 6 (Polish + Parity)
|
||||
```
|
||||
Packages 2 and 3 can run in parallel after Package 1. Package 4 depends on form components from Package 3. Package 5 depends on the data grid from Package 2 and forms from Package 3. Package 6 depends on everything.
|
||||
|
||||
**Shared Conventions** (also extracted into `shared-context/coding-conventions.md`):
|
||||
- All components that render from metadata must accept the metadata type as a prop — never fetch it internally
|
||||
- API calls go through the typed client in `lib/api/` — never raw `fetch`
|
||||
- No hardcoded table names, field names, or process names anywhere in the UI
|
||||
- Every component must have a `data-qqq-id` attribute for CSS customization
|
||||
- All user-facing text that comes from metadata must use the `label` field, never `name`
|
||||
- Error boundaries at the route level and the widget level
|
||||
|
||||
### Step 3: Produce Each Work Package Plan
|
||||
|
||||
Each work package file (`01-scaffold-and-auth.md` through `06-polish-and-parity.md`) must follow this exact structure:
|
||||
|
||||
```markdown
|
||||
# Work Package [N]: [Name]
|
||||
|
||||
## Prerequisites
|
||||
- List which previous packages must be complete
|
||||
- List specific files/components from previous packages that this package depends on
|
||||
|
||||
## Requirements Traceability
|
||||
- List every section/subsection from the requirements doc that this package implements
|
||||
- Example: "Section 5.5 (Authentication), Section 5.3.1 (Layout Components), Section 3.2 (Auth Endpoints)"
|
||||
|
||||
## Shared Context Files
|
||||
- List which files from shared-context/ the implementation agent needs
|
||||
|
||||
## Scope
|
||||
|
||||
### In Scope
|
||||
- Specific list of features/components to build
|
||||
|
||||
### Out of Scope
|
||||
- What is explicitly NOT part of this package (to prevent scope creep)
|
||||
|
||||
## Detailed Implementation Steps
|
||||
|
||||
### Step 1: [Action]
|
||||
**What:** [Precise description of what to create/modify]
|
||||
**Files to create:**
|
||||
- `src/path/to/file.tsx` — [Purpose and key exports]
|
||||
- `src/path/to/file.ts` — [Purpose and key exports]
|
||||
|
||||
**Key implementation details:**
|
||||
- [Specific technical detail the agent needs to know]
|
||||
- [Edge case to handle]
|
||||
- [Pattern to follow from the existing codebase — reference specific files]
|
||||
|
||||
**API integration:**
|
||||
- Endpoint: `[METHOD] /qqq/v1/[path]`
|
||||
- Request: [Shape]
|
||||
- Response: [Shape]
|
||||
- Error handling: [What to do on 401, 404, 500]
|
||||
|
||||
**Reference implementation:**
|
||||
- See `qqq-frontend-material-dashboard/src/qqq/[path]` for the current implementation of this feature
|
||||
- Key behaviors to preserve: [list]
|
||||
- Key behaviors to change/improve: [list]
|
||||
|
||||
### Step 2: [Action]
|
||||
[Same structure...]
|
||||
|
||||
[Continue for all steps...]
|
||||
|
||||
## Component Specifications
|
||||
|
||||
For each component this package creates, specify:
|
||||
|
||||
### `ComponentName`
|
||||
**File:** `src/components/[path]/ComponentName.tsx`
|
||||
**Props:**
|
||||
```typescript
|
||||
interface ComponentNameProps {
|
||||
// exact prop types
|
||||
}
|
||||
```
|
||||
**Behavior:**
|
||||
- [What it renders in each state: loading, empty, error, populated]
|
||||
- [User interactions it handles]
|
||||
- [Events it emits / callbacks it invokes]
|
||||
|
||||
**Metadata-driven aspects:**
|
||||
- [Which metadata fields control its rendering]
|
||||
- [How it adapts to different field types / configurations]
|
||||
|
||||
**Responsive behavior:**
|
||||
- Mobile: [specific behavior]
|
||||
- Tablet: [specific behavior]
|
||||
- Desktop: [specific behavior]
|
||||
|
||||
## API Client Functions
|
||||
|
||||
For each API function this package requires:
|
||||
|
||||
### `functionName`
|
||||
**File:** `src/lib/api/[module].ts`
|
||||
**Signature:** `(params: Type) => Promise<ReturnType>`
|
||||
**Endpoint:** `[METHOD] /qqq/v1/[path]`
|
||||
**Caching:** [TanStack Query key, stale time, cache time]
|
||||
**Error handling:** [Specific error scenarios and how to handle them]
|
||||
|
||||
## Testing Requirements
|
||||
|
||||
### Unit Tests
|
||||
- [Specific components/functions to test]
|
||||
- [Key scenarios to cover]
|
||||
- [Mock data patterns]
|
||||
|
||||
### E2E Tests (Playwright)
|
||||
- [User flow 1: description and steps]
|
||||
- [User flow 2: description and steps]
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
Each criterion must be binary (pass/fail) and testable:
|
||||
|
||||
- [ ] [Specific, measurable criterion]
|
||||
- [ ] [Specific, measurable criterion]
|
||||
- [ ] [Example: "The sidebar renders all apps from appTree metadata with correct nesting and icons"]
|
||||
- [ ] [Example: "Clicking a table in the sidebar navigates to /app/{tableName} and the RecordQuery page loads"]
|
||||
- [ ] [Example: "A 401 response from any API call redirects to the login page without losing the current URL"]
|
||||
```
|
||||
|
||||
### Step 4: Produce Shared Context Files
|
||||
|
||||
**`shared-context/api-contract.md`:**
|
||||
Extract Section 3 from the requirements doc verbatim. Every implementation agent receives this file. It is the single source of truth for API integration.
|
||||
|
||||
**`shared-context/type-definitions.md`:**
|
||||
Port the key TypeScript types from `qqq-frontend-core/src/model/` into a single reference document. Include:
|
||||
- All metadata types (QInstance, QTableMetaData, QFieldMetaData, QProcessMetaData, QAppMetaData, QWidgetMetaData, etc.)
|
||||
- All data types (QRecord, QPossibleValue)
|
||||
- All query types (QQueryFilter, QFilterCriteria, QCriteriaOperator, QFilterOrderBy, QueryJoin)
|
||||
- All process types (QJobStarted, QJobRunning, QJobComplete, QJobError)
|
||||
- All enum types (QFieldType, QComponentType, AdornmentType, Capability, QAppNodeType)
|
||||
|
||||
Annotate each type with a one-line description of when/where it's used.
|
||||
|
||||
**`shared-context/coding-conventions.md`:**
|
||||
Define project-wide rules:
|
||||
- File naming conventions
|
||||
- Component patterns (server components vs. client components — when to use each)
|
||||
- State management approach (TanStack Query for server state, React context for UI state)
|
||||
- Error handling patterns
|
||||
- Import organization
|
||||
- Tailwind class ordering convention
|
||||
- Accessibility requirements (ARIA attributes, keyboard navigation, focus management)
|
||||
- `data-qqq-id` naming convention for CSS customization hooks
|
||||
- How to handle the QQQ theme tokens (CSS custom properties)
|
||||
|
||||
### Step 5: Produce Review Checklists
|
||||
|
||||
Each review checklist (`review-checklists/review-0N-*.md`) must contain:
|
||||
|
||||
1. **Requirements coverage:** For each requirement traced to this package, a yes/no check that it's implemented
|
||||
2. **Integration check:** Verify the package integrates with previous packages (imports resolve, types match, navigation works)
|
||||
3. **API contract compliance:** Verify every API call matches Section 3 exactly (correct path, correct request shape, correct response handling)
|
||||
4. **Metadata-driven check:** Verify no hardcoded table/field/process names exist
|
||||
5. **Responsive check:** Verify mobile/tablet/desktop behavior per Section 5.4
|
||||
6. **Accessibility check:** Keyboard navigation, ARIA attributes, color contrast
|
||||
7. **Type safety check:** No `any` types in component props, all API responses validated
|
||||
|
||||
---
|
||||
|
||||
## Work Package Definitions
|
||||
|
||||
Here is the scope for each package. Use these as starting points, then refine based on what you learn from reading the code.
|
||||
|
||||
### Package 1: Project Scaffold, Auth, and Layout Shell
|
||||
**Requirements:** Sections 5.1, 5.5, 5.3.1 (Layout Components), 3.2 (Auth Endpoints)
|
||||
**Delivers:**
|
||||
- Next.js project initialization with all dependencies
|
||||
- TypeScript types ported from qqq-frontend-core models
|
||||
- API client with typed functions for auth and metadata endpoints
|
||||
- Auth flow (OAuth2/OIDC via `getAuthenticationMetaData` → redirect → `manageSession` → session cookie)
|
||||
- Root layout with sidebar navigation generated from `appTree` metadata
|
||||
- Top navbar with breadcrumbs
|
||||
- 401 interceptor with re-auth flow
|
||||
- Branding/theming from `QBrandingMetaData` (accent colors, logo, banners)
|
||||
- Light/dark mode toggle
|
||||
- Empty placeholder pages for all dynamic routes (so navigation works end-to-end)
|
||||
|
||||
**Critical reading:** `App.tsx` (route generation logic), `QController.getAuthenticationMetaData()`, `QController.manageSession()`, `QController.loadMetaData()`, SideNav components, NavBar component, BrandedHeaderBar
|
||||
|
||||
### Package 2: Record Query Page
|
||||
**Requirements:** Sections 5.2.3 (query route), 5.3.2 (Data Grid), 5.3.5 (Query/Filter Components), 5.4.2 (responsive for query), 3.4.1 (Query), 3.4.2 (Count), 3.7 (Possible Values)
|
||||
**Delivers:**
|
||||
- Record Query page with full data grid (TanStack Table)
|
||||
- Server-side pagination, sorting, and filtering via `POST /table/{t}/query` and `POST /table/{t}/count`
|
||||
- Dynamic column generation from `QTableMetaData.fields`
|
||||
- Cell renderers per field type (string, number, date, boolean, etc.) and per adornment type (link, chip, file download, etc.)
|
||||
- Basic filter mode (quick text search) and advanced filter builder (full QQueryFilter with all operators, boolean logic, sub-filters)
|
||||
- Column configuration panel (show/hide, reorder)
|
||||
- Row selection (single and multi)
|
||||
- Export (CSV at minimum)
|
||||
- Saved views (save/load filter + column configurations)
|
||||
- Pagination controls with page size selector
|
||||
- Table variant selection
|
||||
- Row click → navigate to Record View
|
||||
- Toolbar with create button (if insertPermission), process launcher, column config, density control
|
||||
|
||||
**Critical reading:** RecordQuery.tsx (the entire file — this is the most complex page), CustomFilterPanel, FilterCriteriaRow, BasicAndAdvancedQueryControls, SavedViews, CustomColumnsPanel, ExportMenuItem
|
||||
|
||||
### Package 3: Record View and CRUD
|
||||
**Requirements:** Sections 5.2.3 (view/create/edit/copy routes), 5.3.2 (Record Detail View), 5.3.3 (Form Components), 5.4.2 (responsive for view and form), 3.4.3–3.4.6 (Get, Insert, Update, Delete), 3.7 (Possible Values)
|
||||
**Delivers:**
|
||||
- Record View page with section-organized field display
|
||||
- Field value rendering per type and adornment
|
||||
- Action menu (edit, delete, copy, launch process, share)
|
||||
- Entity Form component for create/edit/copy
|
||||
- Dynamic form generation from `QFieldMetaData` (all 12 field types)
|
||||
- Form validation (Zod schemas generated from field metadata: required, maxLength, type constraints)
|
||||
- Possible values autocomplete (`POST /possibleValues/{f}`) with debounced search
|
||||
- File upload fields
|
||||
- Boolean switch fields
|
||||
- Date/time pickers
|
||||
- Rich text fields (HTML type)
|
||||
- Section-based form layout with `gridColumns` support
|
||||
- Record delete with confirmation dialog
|
||||
- Record copy (pre-populate form without ID)
|
||||
- Associated records display
|
||||
- Help content tooltips on fields
|
||||
|
||||
**Critical reading:** RecordView.tsx, EntityForm.tsx, DynamicForm.tsx, DynamicFormField.tsx, DynamicSelect.tsx, BooleanFieldSwitch.tsx, FileInputField.tsx, all field adornment rendering logic
|
||||
|
||||
### Package 4: Process Execution
|
||||
**Requirements:** Sections 5.2.4 (process routes), 5.3.4 (Process Components), 3.5 (Process Endpoints), 3.7 (Possible Values)
|
||||
**Delivers:**
|
||||
- Process Run page with step wizard (stepper UI)
|
||||
- Dynamic step rendering based on `QFrontendStepMetaData`
|
||||
- Form steps (reuse EntityForm components from Package 3)
|
||||
- View-only steps
|
||||
- Validation review screen
|
||||
- Bulk load workflows: file upload, column-to-field mapping, value mapping, profile save/load
|
||||
- Async job handling: detect `QJobStarted`, poll `GET /status/{jobUUID}`, display `QJobRunning` progress, handle `QJobComplete` and `QJobError`
|
||||
- Process results summary (record counts, error lists, download links)
|
||||
- Process records endpoint (`GET /processes/{p}/{uuid}/records`) for paginated result viewing
|
||||
- Process cancellation
|
||||
- Support for both standalone processes and table-scoped processes (with pre-selected records via recordIds or filterJSON)
|
||||
|
||||
**Critical reading:** ProcessRun.tsx (the entire file), ProcessViewForm, ProcessSummaryResults, ValidationReview, all BulkLoad* components, QJobStarted/Running/Complete/Error types
|
||||
|
||||
### Package 5: Dashboard and Widgets
|
||||
**Requirements:** Sections 5.2.2 (App Home), 5.3.6 (Widget Components), 3.6 (Widget Endpoint)
|
||||
**Delivers:**
|
||||
- App Home page with dashboard layout
|
||||
- Widget container component (label, reload, export, dropdown menus, help content)
|
||||
- Widget grid layout with `gridColumns` support
|
||||
- All chart widgets (line, bar, horizontal bar, stacked bar, pie) — use Recharts or Chart.js
|
||||
- Statistics cards (MiniStatisticsCard, MultiStatisticsCard)
|
||||
- Record Grid Widget (embedded data grid with add/edit/delete)
|
||||
- Block system (TextBlock, BigNumberBlock, UpOrDownNumberBlock, ProgressBarBlock, ButtonBlock, IconBlock, ImageBlock, AudioBlock, DividerBlock, InputFieldBlock)
|
||||
- Composite and parent widget containers
|
||||
- Widget dropdown selections with possible-value sources
|
||||
- Data fetching with abort/cancel-and-replace for widget requests
|
||||
- Custom component widget mount point (for app-specific React components)
|
||||
- Specialized widgets: pivot table setup, filter/columns setup, CRON builder, US map (if feasible), script viewer
|
||||
|
||||
**Critical reading:** DashboardWidgets, Widget.tsx, all widget block components, RecordGridWidget, ChartSubheaderWithData, all chart components, CompositeWidget, ParentWidget
|
||||
|
||||
### Package 6: Polish, Parity, and Production Readiness
|
||||
**Requirements:** Sections 5.4 (all responsive), 5.6 (Performance), 5.7 (Developer Experience)
|
||||
**Delivers:**
|
||||
- Responsive design audit and fixes across all breakpoints for all pages
|
||||
- Mobile card view for Record Query
|
||||
- Command palette (Cmd+K) for quick navigation
|
||||
- Keyboard shortcuts
|
||||
- Audit trail display on Record View
|
||||
- Developer tools pages (script editor, logs, test execution)
|
||||
- Report execution pages
|
||||
- Storybook setup with stories for all shared components
|
||||
- Playwright E2E test suite for critical flows (login, query, CRUD, process execution)
|
||||
- Performance optimization (bundle analysis, lazy loading, caching tuning)
|
||||
- Accessibility audit (axe-core) and remediation
|
||||
- Theme token system (CSS custom properties) for all 60+ tokens from current qfmd
|
||||
- `data-qqq-id` attributes on all interactive elements
|
||||
- `customCss` injection support
|
||||
|
||||
**Critical reading:** All responsive-related code in current qfmd, CommandMenu, AuditBody, RecordDeveloperView, TableDeveloperView, ScriptViewer, ReportRun
|
||||
|
||||
---
|
||||
|
||||
## Quality Standards for Your Plans
|
||||
|
||||
Your plans will be judged on:
|
||||
|
||||
1. **Precision:** An implementation agent should not need to make architectural decisions. Every file path, component name, prop interface, and API integration point should be specified.
|
||||
|
||||
2. **Completeness:** Every feature in the requirements must appear in exactly one work package. Nothing can fall through the cracks. If a feature spans packages, specify which package owns it and what interface the other package consumes.
|
||||
|
||||
3. **Traceability:** Every implementation step must trace back to a specific requirement (section number). Every requirement must trace forward to a specific implementation step.
|
||||
|
||||
4. **Realism:** Steps should be ordered so that each step can be tested after completion. Don't put all the types first, then all the components, then all the wiring — interleave so that after each step, something new works.
|
||||
|
||||
5. **Edge case awareness:** Read the existing implementation. Note error handling, loading states, empty states, permission checks, and null cases. Your plans must address these. The current qfmd has years of edge case handling baked in — don't lose it.
|
||||
|
||||
6. **Integration specificity:** When Package N depends on Package N-1, specify the exact imports, types, and components it depends on. Don't say "use the API client from Package 1" — say "import `queryRecords` from `@/lib/api/tables` which returns `Promise<QRecord[]>`".
|
||||
|
||||
---
|
||||
|
||||
## Execution Instructions
|
||||
|
||||
1. Read the full requirements document first.
|
||||
2. Read the current codebase files listed above (use parallel reads where possible).
|
||||
3. Produce `00-project-overview.md` first — this establishes all shared decisions.
|
||||
4. Produce the three `shared-context/` files.
|
||||
5. Produce work packages 01 through 06 in order.
|
||||
6. Produce review checklists for each package.
|
||||
7. Do a final cross-check: scan every subsection of Section 5 in the requirements and verify it appears in at least one work package's scope.
|
||||
|
||||
Begin now.
|
||||
@@ -0,0 +1,6 @@
|
||||
# qqq-frontend-next
|
||||
|
||||
This Maven artifact contains the static export of the QQQ Next dashboard under
|
||||
`next-dashboard/`. It has no Java sources or API documentation; the source code is at
|
||||
https://github.com/QRun-IO/qqq-frontend-next. Add the jar to a QQQ application and
|
||||
`QApplicationJavalinServer` serves the dashboard at `/`.
|
||||
@@ -0,0 +1,26 @@
|
||||
# API playground controls in narrow panels — GH-724
|
||||
|
||||
## Requirement
|
||||
|
||||
Keep the original Next developer view and make its API specification actions fully visible and usable in narrow panels, including 320px phones. Preserve the wide desktop layout, real spec download/new-tab actions, light/dark themes and keyboard access.
|
||||
|
||||
## Evidence and implementation
|
||||
|
||||
- The packaged RC7 JAR fails the new real-backend visibility test: the Download button is partially clipped before any focus/click can horizontally scroll hidden ancestors. The View button extends outside the panel too.
|
||||
- RapiDoc 9.3.8 has a 360px host minimum, a negative overview info margin and two fixed-width, non-wrapping buttons.
|
||||
- Allow the host to fit its panel. Use the public overview/button CSS parts under a panel-width container query to provide safe inset and share available button width with wrapped labels. A named overview container limits button adjustments to specification actions, excluding authentication and endpoint controls. No dependency changes or shadow DOM mutation.
|
||||
|
||||
## Verification plan
|
||||
|
||||
1. Retain the failing Chromium phone trace and screenshot.
|
||||
2. Run light/dark, normal-width and 320px workflows with desktop and touch input. Check complete visibility before interaction, download the real OpenAPI JSON, and open the same spec using keyboard navigation.
|
||||
3. Inspect screenshots and compare wide desktop before/after layout.
|
||||
4. Run existing API contrast/theme workflows, unit suite, types, lint, export and bundle budgets. Obtain independent review before integration.
|
||||
|
||||
Evidence: original checkout `test-results/visual-review/api-playground-width/`. This follow-up is separate from frozen RC7 candidate `f65cf6b`.
|
||||
|
||||
## Results
|
||||
|
||||
The final production export passed 12 focused desktop/mobile workflows, with zero failures, skips, flaky outcomes or report errors. All 1,904 unit tests, types, lint/license checks and the default Turbopack export passed. The unchanged bundle budget passed at 963.3KB/1050KB. The 1440px overview/button geometry and entire RapiDoc screenshot are identical to RC7; final phone screenshots were inspected.
|
||||
|
||||
Earlier evidence is retained: the first green attempt exposed a relative-versus-absolute URL assertion error; a later run used a running Java server with a replaced export, invalidating its startup asset/CSP registry. The server was restarted against the finished export before the passing run. A Webpack build exceeded the size budget; the release/CI Turbopack pipeline passed without changing budgets. The incompatible old font-response mock was removed from the build command; the final build uses the normal font pipeline.
|
||||
@@ -0,0 +1,17 @@
|
||||
# Bounded LONG validation — GH-728
|
||||
|
||||
## Defect and correction
|
||||
|
||||
Configured LONG ranges exposed two defects in the shared metadata schema: chained Zod refinements called `BigInt` after format validation had already failed, and exact integer-string bounds were rounded through `Number`. Seven new schema cases reproduced exceptions or wrongly accepted values before the fix.
|
||||
|
||||
The schema now pipes successfully validated integer text into its range checks. Integer-string bounds remain BigInt values; numeric fractional bounds use native mixed numeric comparisons. Optional blanks, required-field errors, signs, and existing field messages are preserved. No rendering changes or dependencies are introduced.
|
||||
|
||||
## Verification
|
||||
|
||||
- Seven schema regressions passed after failing on the previous source.
|
||||
- Block and shared process-form component tests verify invalid fractional text, a below-minimum error, blocked submission, and correction to exact digits beyond the safe-number range.
|
||||
- An unrelated intermittent unit failure expected a globally unique “Green” label although both view and edit fields display it. Its separate correction targets the read-only value being tested.
|
||||
- All 1,913 units across 180 files passed; the 95 affected tests, types, lint/license and diff checks passed. Independent review approved both corrections.
|
||||
- Evidence: original checkout `test-results/visual-review/long-range-validation/`. The initial full-run failure remains retained in `units.log`; the final full run is `units-final.log`.
|
||||
|
||||
This follow-up is not included in immutable RC7 `f65cf6b`. Production export and real-backend verification of the next integrated candidate remain required; these results establish schema/component behavior, not a new backend range-metadata contract.
|
||||
@@ -0,0 +1,409 @@
|
||||
# Prompt: Add `node_app_*` jobs to kingsrook/qqq-orb
|
||||
|
||||
## Context
|
||||
|
||||
You are working in the `kingsrook/qqq-orb` CircleCI orb repo at:
|
||||
`/Users/james.maes/Git.Local/QRun-IO/qqq-orb`
|
||||
|
||||
The orb currently supports:
|
||||
- `mvn_*` — Maven Java libraries
|
||||
- `mvn_frontend_*` — Maven + bundled frontend
|
||||
- `node_*` — npm-published TypeScript libraries
|
||||
|
||||
You are adding a new project type: **`node_app_*`** — pnpm-based Node.js
|
||||
applications (Next.js, Vite, etc.) that are _deployed_, not published to npm.
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Read these files before writing anything
|
||||
|
||||
### qqq-orb existing patterns (understand before touching)
|
||||
|
||||
```
|
||||
src/@orb.yml ← orb-level imports (browser-tools, node orbs already imported)
|
||||
src/executors/default.yml ← Docker executor (cimg/node)
|
||||
src/jobs/node_test_only.yml ← existing node job structure
|
||||
src/jobs/node_publish.yml ← existing node publish job
|
||||
src/commands/node_install_dependencies.yml
|
||||
src/commands/node_run_tests.yml
|
||||
src/commands/node_build_package.yml
|
||||
src/scripts/node_npm_auth.sh ← existing script style/conventions
|
||||
src/scripts/node_version_commit.sh ← existing script style/conventions
|
||||
CLAUDE.md ← orb architecture, make commands, linting rules
|
||||
```
|
||||
|
||||
### Munitor reference orb (copy engineering patterns from here)
|
||||
|
||||
These files live at `/Users/james.maes/Git.Local/Kof22/Munitor/`. Read them
|
||||
to understand the patterns you must replicate — **not to copy content**, but
|
||||
to adopt the same engineering discipline.
|
||||
|
||||
```
|
||||
src/scripts/munitor_helpers.sh ← shared helper library pattern
|
||||
src/scripts/install_node.sh ← nvm-based node install with BASH_ENV export
|
||||
src/scripts/npm_install.sh ← lockfile validation before install
|
||||
src/scripts/npm_test.sh ← JSON test_commands array override pattern
|
||||
src/scripts/npm_lint.sh ← lint with JSON artifact output
|
||||
src/scripts/run_e2e_playwright.sh ← Playwright install + JUnit reporter
|
||||
src/commands/restore_npm_cache.yml ← cache key pattern
|
||||
src/commands/save_npm_cache.yml
|
||||
src/jobs/npm_build_and_test.yml ← two-job pipeline with workspace handoff
|
||||
src/jobs/npm_e2e_test.yml ← E2E job that attaches workspace
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Create a shared helper script
|
||||
|
||||
### `src/scripts/qqq_helpers.sh`
|
||||
|
||||
Model this on Munitor's `munitor_helpers.sh`. It must provide:
|
||||
|
||||
- `qqq_header <step_name>` — prints a banner with step name, date (UTC),
|
||||
hostname, and current directory. Format to match qqq style (not Munitor branding).
|
||||
- `qqq_check_tool <command> [version_flag]` — verifies tool is in PATH,
|
||||
prints its version. Exits 1 with clear error if missing.
|
||||
- Guard against double-sourcing (`[[ -n "${_QQQ_HELPERS_LOADED:-}" ]] && return 0`)
|
||||
|
||||
Sourcing pattern every script must use at the top:
|
||||
```bash
|
||||
QQQ_HELPERS="${QQQ_HELPERS:-$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)/qqq_helpers.sh}"
|
||||
[[ -f "${QQQ_HELPERS}" ]] && source "${QQQ_HELPERS}"
|
||||
```
|
||||
|
||||
With a fallback stub if helpers are missing (same pattern as Munitor):
|
||||
```bash
|
||||
elif ! type qqq_header &>/dev/null; then
|
||||
qqq_header() { echo "=== QQQ: ${1:-unknown} ==="; }
|
||||
qqq_check_tool() { command -v "$1" &>/dev/null || { echo "ERROR: $1 not found"; exit 1; }; }
|
||||
fi
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Create shell scripts
|
||||
|
||||
All scripts: `#!/usr/bin/env bash`, `set -euo pipefail`, source `qqq_helpers.sh`
|
||||
at top. Clear section headers. Descriptive error messages. Print summaries.
|
||||
|
||||
### `src/scripts/node_app_install.sh`
|
||||
|
||||
Purpose: Enable pnpm via corepack, validate lockfile exists, install deps.
|
||||
|
||||
Environment variables consumed:
|
||||
- `NODE_PKG_MANAGER` — `pnpm` (default), `npm`, or `yarn`
|
||||
|
||||
Logic:
|
||||
1. Call `qqq_header "node_app_install"`
|
||||
2. Check tool: node, npm (for corepack)
|
||||
3. If `NODE_PKG_MANAGER=pnpm`:
|
||||
- Run `corepack enable`
|
||||
- Run `corepack prepare pnpm@latest --activate`
|
||||
- Verify `pnpm --version`
|
||||
- Validate `pnpm-lock.yaml` exists; exit 1 with clear message if not
|
||||
- Run `pnpm install --frozen-lockfile`
|
||||
4. If `NODE_PKG_MANAGER=npm`:
|
||||
- Validate `package-lock.json` exists
|
||||
- Print lockfile version + npm version compatibility warning (copy logic from
|
||||
Munitor's `npm_install.sh`)
|
||||
- Run `npm ci`
|
||||
5. If `NODE_PKG_MANAGER=yarn`:
|
||||
- Validate `yarn.lock` exists
|
||||
- Run `yarn install --frozen-lockfile`
|
||||
6. Print count of installed packages
|
||||
|
||||
### `src/scripts/node_app_test.sh`
|
||||
|
||||
Purpose: Run type check, lint, and unit tests. Each step is individually
|
||||
skippable. Supports custom test commands via JSON array (replaces default test
|
||||
runner when provided).
|
||||
|
||||
Environment variables consumed:
|
||||
- `NODE_PKG_MANAGER` — pnpm | npm | yarn (default: pnpm)
|
||||
- `TYPE_CHECK_SCRIPT` — npm script name, or empty to skip (default: `tsc`)
|
||||
- `TYPE_CHECK_ARGS` — extra args (default: `--noEmit`)
|
||||
- `LINT_SCRIPT` — npm script name, or empty to skip (default: `lint`)
|
||||
- `TEST_SCRIPT` — npm script name (default: `test`)
|
||||
- `QQQ_TEST_COMMANDS_JSON` — JSON array of shell commands; if set and non-empty,
|
||||
runs these instead of the default test command (same pattern as Munitor's
|
||||
`MUNITOR_TEST_COMMANDS_JSON`)
|
||||
|
||||
Logic:
|
||||
1. `qqq_header "node_app_test"`
|
||||
2. If `TYPE_CHECK_SCRIPT` is non-empty: run `<pkg_manager> run <TYPE_CHECK_SCRIPT> <TYPE_CHECK_ARGS>`
|
||||
3. If `LINT_SCRIPT` is non-empty: run `<pkg_manager> run <LINT_SCRIPT>`
|
||||
4. If `QQQ_TEST_COMMANDS_JSON` is set and not `[]`/`null`:
|
||||
- Parse JSON array using `jq` and run each command in sequence
|
||||
5. Else: run `<pkg_manager> run <TEST_SCRIPT>`
|
||||
6. Print pass/fail summary
|
||||
|
||||
### `src/scripts/node_app_build.sh`
|
||||
|
||||
Purpose: Run the production build. For Next.js this is `pnpm build`. Exits
|
||||
non-zero on failure with clear output.
|
||||
|
||||
Environment variables consumed:
|
||||
- `NODE_PKG_MANAGER` — default: pnpm
|
||||
- `BUILD_SCRIPT` — npm script name (default: `build`)
|
||||
|
||||
Logic:
|
||||
1. `qqq_header "node_app_build"`
|
||||
2. Check pkg manager is available
|
||||
3. Run `<pkg_manager> run <BUILD_SCRIPT>`
|
||||
4. Print success with timing
|
||||
|
||||
### `src/scripts/node_app_e2e.sh`
|
||||
|
||||
Purpose: Install Playwright browsers and run E2E tests. Reports results in
|
||||
JUnit format for CircleCI test analytics.
|
||||
|
||||
Environment variables consumed:
|
||||
- `NODE_PKG_MANAGER` — default: pnpm
|
||||
- `E2E_TEST_DIR` — default: `e2e`
|
||||
- `PLAYWRIGHT_BROWSERS` — comma-separated browsers (default: `chromium`)
|
||||
|
||||
Logic:
|
||||
1. `qqq_header "node_app_e2e"`
|
||||
2. Install Playwright browsers: `npx playwright install --with-deps <browsers>`
|
||||
3. Run: `npx playwright test --reporter=junit --output=e2e-results`
|
||||
- If `E2E_TEST_DIR/playwright.config.ts` exists, pass `--config=<path>`
|
||||
4. On failure: print "E2E tests failed. See playwright-report artifact." and
|
||||
re-exit with the original exit code
|
||||
5. Print pass summary
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Create commands
|
||||
|
||||
### `src/commands/node_app_install.yml`
|
||||
|
||||
Parameters:
|
||||
- `pkg_manager` — string, default: `"pnpm"` — package manager (pnpm | npm | yarn)
|
||||
|
||||
Steps:
|
||||
1. Restore cache with keys:
|
||||
- `node-app-v1-{{ checksum "pnpm-lock.yaml" }}-{{ checksum "package-lock.json" }}-{{ checksum "yarn.lock" }}`
|
||||
- `node-app-v1-`
|
||||
2. Run `node_app_install.sh` with environment `NODE_PKG_MANAGER: << parameters.pkg_manager >>`
|
||||
3. Save cache key (same composite key as restore), paths: `node_modules`
|
||||
|
||||
### `src/commands/node_app_test.yml`
|
||||
|
||||
Parameters:
|
||||
- `pkg_manager` — string, default: `"pnpm"`
|
||||
- `type_check_script` — string, default: `"tsc"` — set empty to skip
|
||||
- `type_check_args` — string, default: `"--noEmit"`
|
||||
- `lint_script` — string, default: `"lint"` — set empty to skip
|
||||
- `test_script` — string, default: `"test"`
|
||||
- `test_commands` — string, default: `""` — JSON array of custom test commands
|
||||
|
||||
Steps:
|
||||
1. Run `node_app_test.sh` with environment:
|
||||
```
|
||||
NODE_PKG_MANAGER: << parameters.pkg_manager >>
|
||||
TYPE_CHECK_SCRIPT: << parameters.type_check_script >>
|
||||
TYPE_CHECK_ARGS: << parameters.type_check_args >>
|
||||
LINT_SCRIPT: << parameters.lint_script >>
|
||||
TEST_SCRIPT: << parameters.test_script >>
|
||||
QQQ_TEST_COMMANDS_JSON: << parameters.test_commands >>
|
||||
```
|
||||
2. store_artifacts: path: `test-results`, destination: `test-results` (if exists)
|
||||
|
||||
### `src/commands/node_app_build_step.yml`
|
||||
|
||||
Parameters:
|
||||
- `pkg_manager` — string, default: `"pnpm"`
|
||||
- `build_script` — string, default: `"build"`
|
||||
|
||||
Steps:
|
||||
1. Run `node_app_build.sh` with environment:
|
||||
```
|
||||
NODE_PKG_MANAGER: << parameters.pkg_manager >>
|
||||
BUILD_SCRIPT: << parameters.build_script >>
|
||||
```
|
||||
|
||||
### `src/commands/node_app_e2e.yml`
|
||||
|
||||
Parameters:
|
||||
- `pkg_manager` — string, default: `"pnpm"`
|
||||
- `test_dir` — string, default: `"e2e"`
|
||||
- `browsers` — string, default: `"chromium"`
|
||||
|
||||
Steps:
|
||||
1. Run `node_app_e2e.sh` with environment:
|
||||
```
|
||||
NODE_PKG_MANAGER: << parameters.pkg_manager >>
|
||||
E2E_TEST_DIR: << parameters.test_dir >>
|
||||
PLAYWRIGHT_BROWSERS: << parameters.browsers >>
|
||||
```
|
||||
2. store_test_results: path: `e2e-results`
|
||||
3. store_artifacts: path: `e2e-results`, destination: `e2e-results`
|
||||
4. store_artifacts: path: `test-results/traces`, destination: `playwright-traces`
|
||||
(use `when: always` so traces are captured even on failure)
|
||||
|
||||
---
|
||||
|
||||
## Step 5: Create jobs
|
||||
|
||||
### `src/jobs/node_app_test_only.yml`
|
||||
|
||||
For feature/non-protected branches. Run install + test only (no build).
|
||||
|
||||
Executor: `default` (existing `cimg/node` Docker executor)
|
||||
- Use `default` executor, tag parameter: `<< parameters.node_version >>`
|
||||
|
||||
Parameters:
|
||||
- `node_version` — string, default: `"lts"`
|
||||
- `pkg_manager` — string, default: `"pnpm"`
|
||||
- `type_check_script` — string, default: `"tsc"`
|
||||
- `type_check_args` — string, default: `"--noEmit"`
|
||||
- `lint_script` — string, default: `"lint"`
|
||||
- `test_script` — string, default: `"test"`
|
||||
- `test_commands` — string, default: `""`
|
||||
|
||||
Steps:
|
||||
1. `git_full_checkout`
|
||||
2. `node_app_install` (pass `pkg_manager`)
|
||||
3. `node_app_test` (pass all test params)
|
||||
|
||||
### `src/jobs/node_app_build.yml`
|
||||
|
||||
For protected branches (develop, release/*, main, hotfix/*). Run install +
|
||||
test + production build. Persists workspace for a downstream E2E job.
|
||||
|
||||
Executor: `default` (cimg/node Docker)
|
||||
|
||||
Parameters: (same as node_app_test_only PLUS:)
|
||||
- `branch_type` — string, default: `"snapshot"` — reserved for future deploy hooks
|
||||
- `build_script` — string, default: `"build"`
|
||||
|
||||
Steps:
|
||||
1. `git_full_checkout`
|
||||
2. `node_app_install` (pass `pkg_manager`)
|
||||
3. `node_app_test` (pass all test params)
|
||||
4. `node_app_build_step` (pass `pkg_manager`, `build_script`)
|
||||
5. `persist_to_workspace` — root: `.`, paths: `["."]`
|
||||
(full workspace so E2E job gets built artifacts)
|
||||
|
||||
### `src/jobs/node_app_e2e.yml`
|
||||
|
||||
Separate E2E job. Attaches workspace from `node_app_build` and runs Playwright.
|
||||
|
||||
Executor: `default` (cimg/node Docker) — browsers are installed by the script
|
||||
using `npx playwright install --with-deps`, which works in Docker with root.
|
||||
|
||||
Parameters:
|
||||
- `node_version` — string, default: `"lts"`
|
||||
- `pkg_manager` — string, default: `"pnpm"`
|
||||
- `test_dir` — string, default: `"e2e"`
|
||||
- `browsers` — string, default: `"chromium"`
|
||||
|
||||
Steps:
|
||||
1. `attach_workspace` — at: `.`
|
||||
2. `node_app_e2e` (pass all E2E params)
|
||||
|
||||
---
|
||||
|
||||
## Step 6: Do NOT modify any existing files
|
||||
|
||||
These must be completely unchanged:
|
||||
- All `mvn_*` jobs, commands, and scripts
|
||||
- All `mvn_frontend_*` jobs, commands, and scripts
|
||||
- All `node_*` jobs (node_test_only, node_publish), commands, and scripts
|
||||
- `src/@orb.yml` — do NOT add new orb imports; existing `browser-tools`,
|
||||
`node`, and `slack` orbs are already available
|
||||
- `src/executors/default.yml`
|
||||
|
||||
---
|
||||
|
||||
## Step 7: Validate your work
|
||||
|
||||
After creating all files, run these commands from the orb root:
|
||||
|
||||
```bash
|
||||
make lint # yamllint + shellcheck + circleci validate
|
||||
make pack # pack to target/qqq-orb-packed.yml
|
||||
make validate # pack + validate
|
||||
```
|
||||
|
||||
Fix any yamllint, shellcheck, or orb validation errors before finishing.
|
||||
|
||||
---
|
||||
|
||||
## Target usage (what consuming repos will write)
|
||||
|
||||
After your changes, a pnpm Next.js app at `QRun-IO/qqq-frontend-next` writes:
|
||||
|
||||
```yaml
|
||||
version: 2.1
|
||||
|
||||
orbs:
|
||||
qqq-orb: qrun-io/qqq-orb@<new-version>
|
||||
|
||||
workflows:
|
||||
|
||||
test_only:
|
||||
jobs:
|
||||
- qqq-orb/node_app_test_only:
|
||||
context: [ qqq-maven-registry-credentials ]
|
||||
filters:
|
||||
branches:
|
||||
ignore: /(develop|main|release\/.*|hotfix\/.*|integration.*)/
|
||||
tags:
|
||||
only: []
|
||||
|
||||
publish_snapshot:
|
||||
jobs:
|
||||
- qqq-orb/node_app_build:
|
||||
branch_type: snapshot
|
||||
context: [ qqq-maven-registry-credentials ]
|
||||
filters:
|
||||
branches:
|
||||
only: [ develop ]
|
||||
|
||||
publish_release_candidate:
|
||||
jobs:
|
||||
- qqq-orb/node_app_build:
|
||||
branch_type: release_candidate
|
||||
context: [ qqq-maven-registry-credentials ]
|
||||
filters:
|
||||
branches:
|
||||
only: [ /release\/.*/ ]
|
||||
|
||||
publish_release:
|
||||
jobs:
|
||||
- qqq-orb/node_app_build:
|
||||
branch_type: release
|
||||
context: [ qqq-maven-registry-credentials ]
|
||||
filters:
|
||||
branches:
|
||||
only: [ main ]
|
||||
tags:
|
||||
only: [ /v.*/ ]
|
||||
|
||||
publish_hotfix_release:
|
||||
jobs:
|
||||
- qqq-orb/node_app_build:
|
||||
branch_type: hotfix
|
||||
context: [ qqq-maven-registry-credentials ]
|
||||
filters:
|
||||
branches:
|
||||
only: [ /hotfix\/.*/ ]
|
||||
```
|
||||
|
||||
All parameters use defaults that work for any standard pnpm Next.js project
|
||||
with `tsc`, `lint`, `test`, and `build` scripts — zero configuration needed
|
||||
unless the project deviates from those conventions.
|
||||
|
||||
---
|
||||
|
||||
## Engineering quality bar
|
||||
|
||||
Every script must meet the standard set by Munitor's scripts:
|
||||
- `#!/usr/bin/env bash` + `set -euo pipefail`
|
||||
- `qqq_helpers.sh` sourced at top with fallback stub
|
||||
- `qqq_header` called as first real step
|
||||
- `qqq_check_tool` used to validate required tools before using them
|
||||
- Clear, actionable error messages with fix instructions
|
||||
- Summary output at the end (what ran, pass/fail, counts)
|
||||
- shellcheck-clean (no SC warnings)
|
||||
@@ -0,0 +1,73 @@
|
||||
# RecordQuery Feature Gap Analysis
|
||||
_Generated: 2026-02-28_
|
||||
|
||||
## Summary
|
||||
|
||||
The new RecordQuery implementation covers the core query-page surface area well: quick search, advanced filter builder with nested AND/OR groups and possible-value comboboxes, server-side pagination, single-column sort, column show/hide/reorder with drag-and-drop, column resize, saved views, CSV export, bulk process launch, row selection, density picker, grid/card view toggle, variant picker, and mobile filter bottom-sheet. The foundation is solid and the hook architecture (`useRecordQuery`) is clean.
|
||||
|
||||
The primary gaps fall into three categories: (1) **multi-column sort** — the DataGrid sort handler replaces the entire sort array on every click instead of appending to it, so only one column can be sorted at a time; (2) **join-aware filtering** — the FilterBuilder only lists columns from the primary table; joined-table fields exposed via `exposedJoins` cannot be added as filter criteria even though the backend type (`QFilterCriteria`) has no `joinName` field and uses dot-notation `fieldName` instead; (3) **select-all-across-pages / bulk delete** — the selection affordance is page-scoped (only rows visible on the current page can be checked) and the BulkActionBar's delete button is wired up to an `onDeleteSelected` prop that `RecordQuery` never passes, so bulk delete is dead code.
|
||||
|
||||
Several lower-impact features from the legacy implementation are also absent: per-row action buttons (edit/delete inline on each row), inline cell editing, and distinct-count display.
|
||||
|
||||
---
|
||||
|
||||
## Feature Inventory
|
||||
|
||||
| Feature | Status | Notes |
|
||||
|---------|--------|-------|
|
||||
| Multi-column sort | ⚠️ Partial | State and API types support multiple `orderBys`, but `handleSortColumn` in `DataGrid.tsx` calls `onSortChange([{ fieldName, isAscending: true }])` — a single-element array — on every click, replacing any prior sort. Shift-click to append a secondary sort is not implemented. |
|
||||
| Join filters | ⚠️ Partial | `use-record-query.ts` derives `QueryJoin[]` from `exposedJoins` and sends them to the API. However `FilterBuilder.tsx` builds its field list solely from `tableMetaData.fields` and has no awareness of joined-table fields. Users cannot add filter criteria on joined columns. The `QFilterCriteria` type uses dot-notation `fieldName` (e.g. `"joinedTable.fieldName"`) rather than a separate `joinName` property, so the backend contract is compatible — the UI just never produces such criteria. |
|
||||
| Bulk select across pages | ❌ Not implemented | The select-all checkbox (`grid-select-all`) selects only rows on the current page (TanStack Table `getToggleAllRowsSelectedHandler`). There is no "Select all N matching records" affordance analogous to the legacy "select all" banner. The `BulkActionBar` shows `"{selected} of {total} selected"` but has no way to extend selection beyond the page. |
|
||||
| Export | ✅ Implemented | `ExportButton.tsx` supports three scopes: all records (up to 10,000), current page, and selected records. CSV is built client-side from visible/ordered fields and downloaded via a temporary `<a>` element. No server-side export endpoint is called; this may diverge from the legacy approach for very large datasets or Excel format support. |
|
||||
| Inline edit | ❌ Not implemented | `DataCell.tsx` is read-only. There is no double-click-to-edit or pencil-icon affordance on any cell. The legacy implementation offered inline editing on editable fields. |
|
||||
| Saved views | ✅ Implemented | `SavedViewsMenu.tsx` and the `views` namespace in `use-record-query.ts` fully implement save / load / delete of named views persisted to `localStorage`. Views capture filter, column visibility, column order, and sort order. |
|
||||
| Quick search | ✅ Implemented | Debounced input in `RecordQueryToolbar.tsx` drives an OR-joined `CONTAINS` search across all visible `STRING`/`TEXT` columns. Mutually exclusive with the advanced filter panel. |
|
||||
| Column resizing | ✅ Implemented | `DataGrid.tsx` implements mousedown/mousemove/mouseup column resize with a minimum width of 60 px. Widths are persisted to `localStorage` via `useRecordQuery`. Resize listeners are cleaned up on unmount. |
|
||||
| Column reordering | ✅ Implemented | `ColumnConfig.tsx` supports HTML5 drag-and-drop reordering as well as keyboard Up/Down arrow keys on the grip button. Order is persisted to `localStorage`. Drag-reorder directly in the grid header is not implemented (only via the side panel). |
|
||||
| Pinned columns | ❌ Not implemented | No sticky/pinned column support. TanStack Table supports column pinning but it is not wired up. |
|
||||
| Bulk actions | ⚠️ Partial | Process launch from the bulk bar works (`ProcessLauncherMenu` inside `BulkActionBar`). Bulk delete is dead code — `BulkActionBar` renders a Delete button only when `onDeleteSelected` is provided, but `RecordQueryBulkBar.tsx` never passes that prop. There is no "delete selected" handler anywhere in `RecordQuery.tsx`. |
|
||||
| Row-level actions | ❌ Not implemented | Each row navigates to the record detail view on click but has no per-row action buttons (edit pencil, delete trash, custom process). The legacy implementation rendered per-row action icon buttons in a trailing "actions" column. |
|
||||
| Pagination | ✅ Implemented | Standard offset-based pagination with first/prev/next/last buttons, a page-size selector (10/25/50/100/250), and a "Go to page" input when `totalPages > 5`. Controlled by `use-record-query.ts`. |
|
||||
| Distinct count | ⚠️ Partial | `countRecords` in `tables.ts` accepts `includeDistinct = false` as a third parameter and the `CountRecordsResponse` type includes `distinctCount?`. However `use-record-query.ts` always calls `countRecords(tableName, { filter: countFilter, joins }, false)` (or omits the flag, defaulting to `false`) and the UI never reads or displays `distinctCount`. The infrastructure is there; it is just never invoked or surfaced. |
|
||||
|
||||
---
|
||||
|
||||
## Priority Gaps (implement these next)
|
||||
|
||||
Ranked by user-visible impact on day-to-day admin usage:
|
||||
|
||||
1. **Multi-column sort** — High impact, low effort. Change `handleSortColumn` in `DataGrid.tsx` to detect a Shift-click and append/toggle the clicked column in the existing `sortOrder` array rather than replacing it. Update the column header button to accept a `shiftKey` event attribute. The `QFilterOrderBy[]` structure and API calls already support multiple entries.
|
||||
|
||||
2. **Join filters** — High impact, medium effort. Extend `FilterBuilder.tsx` to accept a `joinedFields` prop (or derive it from `tableMetaData.exposedJoins`). Prefix each joined field's display label with the join table label (e.g. "Order → Customer Name"). Use dot-notation fieldName (e.g. `"customer.name"`) in the emitted `QFilterCriteria`. The backend already expects this format.
|
||||
|
||||
3. **Bulk select across pages** — Medium impact, medium effort. Add a "Select all {N} records" banner that appears below the bulk-action bar when the page-scoped selection equals `pageSize` and `totalCount > pageSize`. Store a `selectAllMode: boolean` flag in state. When `true`, submit the current filter as the selection scope to process launches rather than the explicit ID list.
|
||||
|
||||
4. **Bulk delete** — Medium impact, low effort. Wire up `onDeleteSelected` in `RecordQuery.tsx` (and pass it through `RecordQueryBulkBar`). Implement a confirmation dialog, call `deleteRecord` in a loop (or a future bulk-delete endpoint), then invalidate the records query.
|
||||
|
||||
5. **Row-level actions** — Medium impact, medium effort. Add an "actions" column to `DataGrid.tsx` as the last column. Render icon buttons for Edit (navigate to `/{tableName}/{id}/edit`), Delete (single-record confirmation modal), and optionally a per-row process dropdown if the table's `processes` array contains single-record processes. Guard each button on the respective permission flag from `tableMetaData`.
|
||||
|
||||
6. **Pinned columns** — Low-to-medium impact, medium effort. Enable TanStack Table's `columnPinning` feature. Add a "Pin left" option to the `ColumnConfig` panel or a column header context menu. Apply `position: sticky; left: 0` styling to pinned headers and cells. Persist the pinned set to `localStorage` alongside visibility and order.
|
||||
|
||||
7. **Distinct count display** — Low impact, low effort. Pass `includeDistinct = true` in `countRecords` when `tableMetaData.usesVariants` is true or when the query includes a join with `type: 'LEFT'`. Read `countQuery.data?.distinctCount` and display it alongside or instead of the plain count in the `Pagination` component.
|
||||
|
||||
8. **Inline edit** — Low impact (admin use case), high effort. On double-click of an editable cell, swap `DataCell` for a `DataCellEditor` component that renders an appropriate input (text, number, date, possible-value combobox). On blur or Enter, call `updateRecord` and invalidate the query cache. Requires a new `DataCellEditor` component, field-type-aware input selection, and optimistic update handling.
|
||||
|
||||
---
|
||||
|
||||
## Reference Implementation Notes
|
||||
|
||||
The following patterns from the legacy `RecordQuery.tsx` (MUI + React) are worth preserving or adapting as the new implementation matures:
|
||||
|
||||
- **Multi-column sort via Shift-click:** The legacy grid accumulates `orderBys` entries when the user holds Shift while clicking a column header. This is the industry-standard UX (matching MUI DataGrid Pro) and should be replicated exactly.
|
||||
|
||||
- **Join filter field grouping:** The legacy FilterBuilder groups fields under their parent table heading in the field selector dropdown (e.g. a separator "— Order fields —" followed by "— Customer fields —"). This makes joined fields discoverable rather than mixed into a flat list.
|
||||
|
||||
- **Select-all banner:** When the user selects all rows on the current page, a blue banner appears: "All 25 records on this page are selected. Select all 1,247 records matching your filter." Clicking the link sets a `filterOnlyMode` flag that replaces the ID list with the filter in process invocations.
|
||||
|
||||
- **Per-row action icons:** The legacy grid renders three small icon buttons at the end of every row (view, edit, delete) that are hidden until hover. They are absolutely positioned so they do not affect column widths. The new DataGrid's trailing-column approach is acceptable but should use `opacity-0 group-hover:opacity-100` CSS to avoid visual noise.
|
||||
|
||||
- **Export via server endpoint (Excel):** The legacy implementation calls a dedicated server-side export endpoint (`POST /table/{name}/export`) that returns an Excel file stream. The current implementation builds a CSV client-side from the raw query results. This has a hard limit of 10,000 rows and produces only CSV. For parity, a server-side export endpoint call should be added; the client-side fallback can remain for smaller datasets.
|
||||
|
||||
- **Distinct count in count query:** The legacy implementation passes `includeDistinctCount=true` for tables that use joins, and displays "N (M distinct)" in the pagination footer when `distinctCount < count`. This helps users understand that a LEFT join is inflating the row count.
|
||||
|
||||
- **Filter variable expressions (NOW, NOW\_WITH\_OFFSET):** The legacy FilterBuilder renders dedicated UI controls for dynamic date expressions so users can save filters like "created in the last 7 days" that remain accurate over time. The types (`NowExpression`, `NowWithOffsetExpression`, `ThisOrLastPeriodExpression`) are already defined in `src/types/query.ts` and `filter-utils.ts` includes type guards and display formatting for them, but the `FilterBuilder` UI never offers these expressions as selectable value options — only static scalar inputs are rendered.
|
||||
@@ -0,0 +1,25 @@
|
||||
# Next UI 1.0 testing period
|
||||
|
||||
## Final release is on hold
|
||||
|
||||
On September 29, 2026, the project owner requested a couple of weeks to test RC1 in real applications and report issues. **Do not tag, publish, or deploy final 1.0 without renewed, explicit owner approval.** Elapsed time and passing CI are not release approval.
|
||||
|
||||
RC1 remains immutable: [v1.0.0-RC.1](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.1), source `e901df91017204add7ebffa81765a02094e581b0`. See [RC1 release notes](1.0.0-RC.1.md) for installation details.
|
||||
|
||||
## Release candidate cadence
|
||||
|
||||
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.
|
||||
|
||||
## 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
|
||||
|
||||
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.
|
||||
|
||||
## Reporting application issues
|
||||
|
||||
Create or update the central QQQ ticket and Project 12 item using the [tracking policy](../ISSUE-TRACKING.md). Include the exact UI artifact/version or branch commit, QQQ backend version, page/workflow, expected and actual results, reproduction steps, and relevant screenshots or sanitized errors. This distinguishes an issue in a published RC from a later branch regression. Never include credentials or sensitive application records.
|
||||
|
||||
The compatibility ledger and remaining acceptance work still govern final readiness. The testing hold does not waive those requirements or authorize a QQQ 4.1 release.
|
||||
@@ -0,0 +1,57 @@
|
||||
# Next UI 1.0.0-RC.1
|
||||
|
||||
Release candidate for testing in real QQQ applications. Published from commit `e901df91017204add7ebffa81765a02094e581b0`. Final 1.0 visual review and compatibility work remain open.
|
||||
|
||||
## Install
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>com.kingsrook.qqq</groupId>
|
||||
<artifactId>qqq-frontend-next</artifactId>
|
||||
<version>1.0.0-RC.1</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
Override any older BOM-managed frontend version. Select Next with `-Dqqq.javalin.frontend=next`. The JAR serves the dashboard at `/` alongside `/qqq/v1`; no Node server is required. Keep Material available during evaluation if you need to switch back with `-Dqqq.javalin.frontend=material`.
|
||||
|
||||
[Maven Central JAR](https://repo.maven.apache.org/maven2/com/kingsrook/qqq/qqq-frontend-next/1.0.0-RC.1/qqq-frontend-next-1.0.0-RC.1.jar) — verified by download, checksum, a consuming Maven project, and real-backend runtime checks.
|
||||
|
||||
Container: `ghcr.io/qrun-io/qqq-frontend-next:1.0.0-RC.1` (linux/amd64 and linux/arm64).
|
||||
|
||||
```text
|
||||
ghcr.io/qrun-io/qqq-frontend-next@sha256:6edd16f20557c189bc618079851cd2d567c04001c6e34a5d3b7dde30895f9958
|
||||
```
|
||||
|
||||
The quickstart image expects its backend at `http://host.docker.internal:8000`; that origin is fixed at build time. For other deployments, use the Maven artifact or build the image with the appropriate backend configuration.
|
||||
|
||||
This candidate is tested with the QQQ **4.1.0-SNAPSHOT development backend**, including the Sleep Interactive fix merged in [QQQ PR #896](https://github.com/QRun-IO/qqq/pull/896) (`e33e1fc52c55c203e3ee3014837970ef2614b809`). It does not publish QQQ 4.1 or establish compatibility with every older backend release. Pin and report your backend version/commit.
|
||||
|
||||
## Verification
|
||||
|
||||
- **547 real-backend Chromium scenarios passed**, with zero failures, skips or flaky retries; all 389 required acceptance rows passed, with three previously approved external-service exclusions. [Exact-candidate CI](https://github.com/QRun-IO/qqq-frontend-next/actions/runs/36601699322/job/109521050432).
|
||||
- 1,806 unit tests, type checking, lint, mocked E2E, Storybook, license and bundle-budget checks passed. Candidate dependency audit reported zero known vulnerabilities on 2026-09-29.
|
||||
- The actual Central JAR resolved in a consuming Maven project. It contains 186 dashboard export files, expected routes and license, and no development mock worker. Its checksum verified; a signature is published. The CI production export uses a different bundler from the local 204-file verification build.
|
||||
- The downloaded JAR ran against the real sample backend: Sleep Interactive defaults, required validation and completion passed; report-filtered statistics and exported CSV matched independent SQL.
|
||||
- The published container manifest includes both architectures; the downloaded arm64 image started and served the dashboard successfully. This container check is a runtime/HTML smoke test, separate from the full backend workflow suite.
|
||||
|
||||
Maven JAR SHA-256: `5a5f56e892164c2b3c220d929372cf57c019dabaa35340839be3b9a6907076eb`.
|
||||
|
||||
Testing prioritizes complete Chromium workflows: normal entry/defaults, validation, editing/cancellation, save/reopen, dependent selections, permissions, recovery and persisted/downloaded results. Test counts do not prove every application configuration works.
|
||||
|
||||
## Known limitations
|
||||
|
||||
- Secondary-browser verification is complete and has unresolved failures. In the [candidate run](https://github.com/QRun-IO/qqq-frontend-next/actions/runs/36601699322), WebKit passed 546 and failed NAV-034 (legacy process redirect, internal navigation error); Firefox passed 546 and failed WID-021 (process widget visibility timeout); phone/tablet passed 1,043 and failed tablet SEC-002 (internal WebKit navigation error). The [tag run](https://github.com/QRun-IO/qqq-frontend-next/actions/runs/36605084236) passed Chromium again but failed Firefox RPT-016 (report sharing), WebKit REL-007 (association navigation), and tablet NAV-015 (document-title navigation), with the same respective passing counts. Logs/traces are retained; these failures have not been waived or established as infrastructure-only. No complete cross-browser pass is claimed.
|
||||
- Hosting beneath a URL base path is not implemented ([QQQ #732](https://github.com/QRun-IO/qqq/issues/732)); evaluate at the origin root.
|
||||
- Google Drive folder selection remains a stub ([QQQ #704](https://github.com/QRun-IO/qqq/issues/704)).
|
||||
- Full embedded report/query equivalence remains under review ([QQQ #722](https://github.com/QRun-IO/qqq/issues/722)).
|
||||
- Remaining input-block configurations and unsupported Material icon names need completion ([QQQ #728](https://github.com/QRun-IO/qqq/issues/728)).
|
||||
- Script editor autocomplete is absent ([QQQ #724](https://github.com/QRun-IO/qqq/issues/724)).
|
||||
- Full visual comparison against the original Next UI design remains open. Material is the behavior reference; the original Next UI is the visual reference.
|
||||
|
||||
These are recorded RC1 gaps, not approved exclusions from final 1.0. The [behavior parity ledger](https://github.com/QRun-IO/qqq-frontend-next/blob/e901df91017204add7ebffa81765a02094e581b0/docs/acceptance/material-parity.md) remains authoritative.
|
||||
|
||||
## Guides and bug reports
|
||||
|
||||
[Developer guide](https://github.com/QRun-IO/qqq-frontend-next/blob/e901df91017204add7ebffa81765a02094e581b0/docs/guides/next-ui-developer-guide.md) · [Agent guide](https://github.com/QRun-IO/qqq-frontend-next/blob/e901df91017204add7ebffa81765a02094e581b0/docs/guides/next-ui-agent-guide.md) · [Acceptance instructions](https://github.com/QRun-IO/qqq-frontend-next/blob/e901df91017204add7ebffa81765a02094e581b0/tests/acceptance/README.md).
|
||||
|
||||
[Report an issue](https://github.com/QRun-IO/qqq-frontend-next/issues/new) with frontend/backend versions, browser, affected widget or metadata, steps from a fresh page, expected/actual behavior and whether it survives reload. Include screenshots or sanitized errors where useful. For reports, include table, filters, sorts and columns; for processes, include step/input configuration. Remove credentials, session cookies and private application data.
|
||||
@@ -0,0 +1,30 @@
|
||||
# Next UI 1.0.0-RC.10 — preparation
|
||||
|
||||
RC10 is being prepared for application testing and **is not yet published**. [RC9](1.0.0-RC.9.md) remains the latest verified public candidate and is immutable. Planned artifacts are `com.kingsrook.qqq:qqq-frontend-next:1.0.0-RC.10` and `ghcr.io/qrun-io/qqq-frontend-next:1.0.0-RC.10`. Public checksums, signatures and image digests will be recorded after delivery verification. Final 1.0 remains held for application soak and explicit owner approval.
|
||||
|
||||
## Changes from RC9
|
||||
|
||||
- Fix section-hash loss and stale immediate URLs when changing record tabs or views (#989), preserving URL context, return links, focus and history. The baseline eventually restored the tab; this is not a claim of proven tab loss.
|
||||
- Preserve search-dialog keyboard focus (#950), WebKit possible-value selections (#978), and authentication loading through logout/provider departure (#984).
|
||||
- Add ESB developer views and overview/management controls, with real-backend acceptance. See the [ESB contract](../acceptance/esb.md).
|
||||
- Improve browser readiness and diagnostic checks while preserving assertions. The pinned Playwright runner contains the corrected Linux WebKit library; stable-runner migration remains #973. See [change details](unreleased.md).
|
||||
|
||||
## Verification
|
||||
|
||||
Verification uses source `5e3738b73f14d7ed25580560f3c97b91aed58fce` ([PR21](https://github.com/QRun-IO/qqq-frontend-next/pull/21)) with RC10 metadata and QQQ backend `55b33e6bd7f8cc2aef3b41546ef8409ad307ff05`.
|
||||
|
||||
- **1,979 unit tests / 184 files**, typecheck, lint/license (zero errors, five existing warnings), static export, Maven packaging and bundle budgets pass.
|
||||
- The actual RC10 JAR passes **85/85** auth/focus/selection/ESB real-backend cases: Chromium, Firefox and WebKit 21 each; mobile and tablet 11 each.
|
||||
- Four navigation regressions pass **8/8 in Firefox/WebKit**, using the same JAR's assets with mocked APIs. These are separate from real-backend API coverage.
|
||||
|
||||
Both browser reports have zero failures, skips, flaky outcomes or report errors, with retries disabled. All 192 dashboard files are packaged without a mock service worker; Maven and compiled frontend versions are RC10. Served JavaScript and font bytes match the JAR. The primary fixture serves the JAR directly; authentication variants use verified assets extracted from it. These are local candidate checks, not public delivery receipts. PR23 changes test selection and documentation only; production code and test assertions are unchanged from this verified candidate.
|
||||
|
||||
## Browser coverage and remaining RC limits
|
||||
|
||||
The historical [PR18 run](https://github.com/QRun-IO/qqq-frontend-next/actions/runs/36942612338) failed its touch-coverage gate despite all 2,972 executed cases passing: ESB-001–010 were required but not selected on phone and tablet. The original failed result is retained under [#990](https://github.com/QRun-IO/qqq/issues/990).
|
||||
|
||||
[PR23](https://github.com/QRun-IO/qqq-frontend-next/pull/23) corrects selection by adding touch markers to the ten existing ESB tests, preserving production code and every assertion. Its separate focused real-broker run on the PR18 export and QQQ `55b33e6` passed all 20 phone/tablet cases with zero retries, failures, skips, flaky outcomes or report errors. This focused result is not a fresh full hosted matrix pass; full hosted verification remains pending at this preparation checkpoint.
|
||||
|
||||
The secondary-browser/tablet RC exception remains documented in [#985](https://github.com/QRun-IO/qqq/issues/985#issuecomment-5943724086) and [#798](https://github.com/QRun-IO/qqq/issues/798#issuecomment-5943457468). No gate, test requirement or exclusion was weakened. This exception does not waive product defects or authorize GA.
|
||||
|
||||
Remaining parity, real Auth0/Drive/QuickSight checks, browser investigations, visual review and application soak stay tracked in [#713](https://github.com/QRun-IO/qqq/issues/713), [#985](https://github.com/QRun-IO/qqq/issues/985), [#711](https://github.com/QRun-IO/qqq/issues/711) and [#712](https://github.com/QRun-IO/qqq/issues/712). The container backend origin is fixed at build time to `host.docker.internal:8000`; use the JAR or rebuild for another origin. This candidate does not itself publish QQQ 4.1.
|
||||
@@ -0,0 +1,19 @@
|
||||
# Next UI 1.0.0-RC.11 — preparation
|
||||
|
||||
At this preparation checkpoint, RC11 is being prepared for application testing and **is not yet published**. [RC10](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.10) remains the latest verified public candidate and is immutable. Planned artifacts are `com.kingsrook.qqq:qqq-frontend-next:1.0.0-RC.11` and `ghcr.io/qrun-io/qqq-frontend-next:1.0.0-RC.11`. Public hashes, signatures and image digests will be recorded after delivery verification. Final 1.0 remains held for application soak and explicit owner approval.
|
||||
|
||||
## Changes from RC10
|
||||
|
||||
[PR24](https://github.com/QRun-IO/qqq-frontend-next/pull/24), merged source `a1d3e48419812fcb8d47091cb4af315cad4e76e9` (tree-identical to reviewed head `ea73118fcabdf3c08ea8614c8bd0ad63749fb598`), invalidates stale searchable-selector results immediately when input changes (#970), and keeps script-completion suggestions open when a delayed scroll event reports an unchanged position (#954). Actual position changes still dismiss suggestions. No acceptance assertion or gate is weakened.
|
||||
|
||||
## Verification checkpoint
|
||||
|
||||
PR24's [hosted primary acceptance](https://github.com/QRun-IO/qqq-frontend-next/actions/runs/36954433088) passes on its exact source: 614 cases and 401 required rows. The actual merged source above has the same tree. With RC11 metadata, fresh verification passes **1,984 unit tests / 184 files**, typecheck, lint/license (zero errors, five existing warnings), static export, Maven packaging and bundle budgets. All **192 dashboard files** match the export byte-for-byte, with no mock service worker and matching RC11 frontend/Maven versions.
|
||||
|
||||
The actual RC11 JAR passes **125/125 real-backend cases** against QQQ `55b33e6bd7f8cc2aef3b41546ef8409ad307ff05`: 25 each in Chromium, Firefox, WebKit, phone and tablet. These include the 105 auth/focus/selection/ESB cases and 20 selector/completion regression cases. A separate **8/8 record-navigation regressions in Firefox/WebKit** use the same JAR assets with mocked APIs. Both reports have zero failures, skips, flaky outcomes or report errors, with retries disabled. Served versioned JavaScript and font bytes match the JAR. The scoped matrix check passes only the exercised rows; it is not a full hosted matrix result. These local candidate checks are separate from eventual public-artifact verification; no RC10 test count or artifact digest is attributed to RC11.
|
||||
|
||||
## Remaining RC limits
|
||||
|
||||
Historical observations remain preserved: REC-054 on mobile light expected three completion options but found none; PRC-061 in WebKit left the Casey selection at its placeholder. See [#954](https://github.com/QRun-IO/qqq/issues/954), [#970](https://github.com/QRun-IO/qqq/issues/970) and [RC10 delivery notes](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.10). The original [PR18 touch-coverage failure](https://github.com/QRun-IO/qqq-frontend-next/actions/runs/36942612338) remains historical evidence. [#990 is complete](https://github.com/QRun-IO/qqq/issues/990#issuecomment-5944836159): PR23’s complete touch report covers all 401 required IDs on each profile, with zero missing, and all ten ESB scenarios pass first attempt on both phone and tablet. That report records **1,149 passes and one unrelated tablet NAV-056 failure**; its full touch gate and whole workflow remain **failed**. [NAV-056 diagnostics remain under investigation](https://github.com/QRun-IO/qqq/issues/985#issuecomment-5944830383), provisionally tracked in [#904](https://github.com/QRun-IO/qqq/issues/904#issuecomment-5944558037), without claiming a shared historical cause. Scoped #990 completion does not imply an all-browser pass or waive that failure.
|
||||
|
||||
The documented secondary-browser/tablet RC exception remains under [#985](https://github.com/QRun-IO/qqq/issues/985#issuecomment-5943724086) and [#798](https://github.com/QRun-IO/qqq/issues/798#issuecomment-5943457468); it does not waive new product defects or authorize GA. Material parity, sub-path hosting, real Auth0/Drive/QuickSight verification, visual review and application soak remain tracked in [#713](https://github.com/QRun-IO/qqq/issues/713), [#711](https://github.com/QRun-IO/qqq/issues/711) and [#712](https://github.com/QRun-IO/qqq/issues/712). The image backend origin remains fixed at build time to `host.docker.internal:8000`; use the JAR or rebuild for another origin. This candidate does not publish QQQ 4.1.
|
||||
@@ -0,0 +1,60 @@
|
||||
# Next UI 1.0.0-RC.12 — preparation
|
||||
|
||||
At this preparation checkpoint, RC12 is being prepared for application testing and **is not yet published**. [RC11](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.11) remains the latest verified public candidate and is immutable. Planned artifacts are `com.kingsrook.qqq:qqq-frontend-next:1.0.0-RC.12` and `ghcr.io/qrun-io/qqq-frontend-next:1.0.0-RC.12`. 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 `b0b0d469feb0b1bd473fbf189540e712ffc09807` (merge of [PR46](https://github.com/QRun-IO/qqq-frontend-next/pull/46)). Merged since RC11: PR25, PR27, PR28, PR29, PR30, PR32, PR35, PR36, PR38, PR40, PR42, PR43, PR44 and PR46. The SQL Workbench work in draft PR47 is not included.
|
||||
|
||||
## Backend requirements
|
||||
|
||||
RC12 runs on a published QQQ 4.1 candidate: **4.1.0-RC.1** or later. Standalone branding images (below) need **4.1.0-RC.2** or later, which contains QQQ #1009's `/qqq/branding/logo` and `/qqq/branding/icon` endpoints. RC12 does not publish QQQ 4.1.
|
||||
|
||||
## Product fixes
|
||||
|
||||
- Always show the record view's Actions menu when it has entries. Tables without processes or a custom `VIEW_SCREEN_ACTIONS` menu, and read-only tables, again reach Developer Mode and Audit (Material parity); Copy and Delete move into that menu instead of also appearing as separate buttons. [#45](https://github.com/QRun-IO/qqq-frontend-next/issues/45)
|
||||
- Clear remembered query filters and saved-view selections on logout or a detected user identity change. Same-user resumes keep their query state; global appearance, density and legacy column preferences remain intact. This correction awaits its own branch acceptance. [#1006](https://github.com/QRun-IO/qqq/issues/1006)
|
||||
- In searchable possible-value fields, invalidate old results immediately when the search term changes, so a debounced request cannot remove an option during its selection. [#970](https://github.com/QRun-IO/qqq/issues/970)
|
||||
- Keep code suggestions open when a delayed textarea scroll event reports the position already used for the popup; dismiss them when the scroll position actually changes. [#954](https://github.com/QRun-IO/qqq/issues/954)
|
||||
- Preserve focus on the search dialog's Clear control when keyboard interaction occurs before its scheduled initial focus. [#950](https://github.com/QRun-IO/qqq/issues/950)
|
||||
- Preserve a clicked possible-value option in WebKit when blurring its search input would scroll the form between mousedown and mouseup. The selected value now reaches process submission and generated output. [#978](https://github.com/QRun-IO/qqq/issues/978)
|
||||
- Keep authentication loading until explicit logout finishes, so a late logout response cannot erase cookies from a subsequent successful sign-in. [#984](https://github.com/QRun-IO/qqq/issues/984)
|
||||
|
||||
## Standalone branding and CSP (QQQ #1009, #1017)
|
||||
|
||||
The standalone Node server can route the exact root-relative `branding.logo` and
|
||||
`branding.icon` paths published by authentication metadata through fixed backend
|
||||
image roles. Local Node `public` files keep precedence; external image URLs and
|
||||
Java dashboard serving are unchanged. This does not expose the whole application
|
||||
overlay or add a general proxy.
|
||||
|
||||
This requires **a QQQ middleware-javalin build containing QQQ #1009's
|
||||
`/qqq/branding/logo` and `/qqq/branding/icon` endpoints**. They are absent from
|
||||
published **4.1.0-RC.1**. The minimum numbered release is not yet assigned; a
|
||||
source build must record the exact backend commit/JAR hashes. Older deployments
|
||||
must supply branding through their Node `public` files or an existing external
|
||||
image URL. No automatic compatibility with old relative backend assets is claimed.
|
||||
|
||||
The preserved standalone qualification against public RC1 remains **5 passed,
|
||||
38 failed** out of 43 cases because of missing branding assets. The original
|
||||
staging failure is also retained. The corrected cross-repository pairing has
|
||||
not yet received its browser qualification; neither #1009 nor #734 is closed by
|
||||
focused unit/HTTP results.
|
||||
|
||||
## Browser testing and infrastructure
|
||||
|
||||
- Pin the official Playwright runner containing the fixed Linux WebKit network library, and retain native browser diagnostics for navigation failures. The previous bundle's heap corruption was reproduced; migration to a stable runner remains tracked separately. [#904](https://github.com/QRun-IO/qqq/issues/904), [#973](https://github.com/QRun-IO/qqq/issues/973)
|
||||
- Correlate interrupted Firefox font diagnostics with real request and decode evidence, wait for initial fonts before the deliberate redirect, and observe transient feedback during the action that produces it. [#951](https://github.com/QRun-IO/qqq/issues/951), [#953](https://github.com/QRun-IO/qqq/issues/953), [#967](https://github.com/QRun-IO/qqq/issues/967)
|
||||
- Wait for nested report metadata and the destination's owned iframe before advancing acceptance navigation. [#961](https://github.com/QRun-IO/qqq/issues/961), [#972](https://github.com/QRun-IO/qqq/issues/972)
|
||||
- Verify Linux WebKit's downloaded filename normalization while retaining file-content and cleanup assertions. [#979](https://github.com/QRun-IO/qqq/issues/979)
|
||||
- Verify actual browser refusal of disallowed framing with a control that renders when the protective headers are removed. [#981](https://github.com/QRun-IO/qqq/issues/981)
|
||||
- Recognize Firefox's network-error diagnostic for the gallery's intentionally missing extension only after that same script request returns HTTP 404; preserve the raw evidence and reject unrelated failures. [#983](https://github.com/QRun-IO/qqq/issues/983)
|
||||
- Select all ten ESB scenarios for both phone and tablet acceptance, retaining real broker delivery, lifecycle, replay, SQL and permission checks. Matrix references now use the central QQQ tickets. [#986](https://github.com/QRun-IO/qqq/issues/986), [#990](https://github.com/QRun-IO/qqq/issues/990)
|
||||
|
||||
## Verification checkpoint
|
||||
|
||||
PR checks on the RC12 preparation source (typecheck, lint, unit tests and coverage, mocked e2e, bundle budgets, Storybook, image builds and the full real-backend acceptance workflow) are recorded on the preparation PR. Tag acceptance and public-artifact verification are recorded in the [RC12 GitHub release](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.12) after delivery. No RC11 test count or artifact digest is attributed to RC12.
|
||||
|
||||
## Remaining RC limits
|
||||
|
||||
The RC11 limits carry forward unchanged. The RC11 tag acceptance recorded 2,990 passes and 2 owner-accepted failures (Firefox NAV-014 font diagnostic, tablet NAV-056 access-control diagnostics) under the documented secondary-browser/tablet exception ([#985](https://github.com/QRun-IO/qqq/issues/985), [#798](https://github.com/QRun-IO/qqq/issues/798)); it does not waive new product defects or authorize GA. Historical REC-054/PRC-061 observations remain under [#954](https://github.com/QRun-IO/qqq/issues/954) and [#970](https://github.com/QRun-IO/qqq/issues/970). Material parity, sub-path hosting, real Auth0/Drive/QuickSight verification, visual review and application soak remain tracked in [#713](https://github.com/QRun-IO/qqq/issues/713), [#711](https://github.com/QRun-IO/qqq/issues/711), [#712](https://github.com/QRun-IO/qqq/issues/712) and [#904](https://github.com/QRun-IO/qqq/issues/904). The image backend origin remains fixed at build time to `host.docker.internal:8000`; use the JAR or rebuild for another origin.
|
||||
@@ -0,0 +1,43 @@
|
||||
# Next UI 1.0.0-RC.13 — preparation
|
||||
|
||||
At this preparation checkpoint, RC13 is being prepared for application testing and **is not yet published**. [RC12](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.12) remains the latest verified public candidate and is immutable. Planned artifacts are `com.kingsrook.qqq:qqq-frontend-next:1.0.0-RC.13` and `ghcr.io/qrun-io/qqq-frontend-next:1.0.0-RC.13`. Public hashes, signatures and image digests will be recorded after delivery verification. Final 1.0 remains held for application soak and explicit owner approval.
|
||||
|
||||
## Source
|
||||
|
||||
Main at `84c95e524a93056864e60863f5ab45357969e786` (merge of [PR53](https://github.com/QRun-IO/qqq-frontend-next/pull/53)). Merged since RC12: [PR47](https://github.com/QRun-IO/qqq-frontend-next/pull/47) (SQL Workbench), [PR49](https://github.com/QRun-IO/qqq-frontend-next/pull/49) (FILE_PREVIEW thumbnails and lightbox), [PR50](https://github.com/QRun-IO/qqq-frontend-next/pull/50) (responsive layouts, readable child lists, compact dates) and [PR53](https://github.com/QRun-IO/qqq-frontend-next/pull/53) (instance home app). The single page scroll container ([PR54](https://github.com/QRun-IO/qqq-frontend-next/pull/54), its WebKit acceptance not yet green) and widget width readability ([PR55](https://github.com/QRun-IO/qqq-frontend-next/pull/55)) are not included; they follow in RC14.
|
||||
|
||||
## Backend requirements
|
||||
|
||||
RC13 runs on a published QQQ 4.1 candidate: **4.1.0-RC.1** or later. Standalone branding images need **4.1.0-RC.2** or later (QQQ #1009). **FILE_PREVIEW thumbnails and the instance home app need QQQ 4.1.0-RC.3 or later**, which carries the `FILE_PREVIEW` adornment ([qqq#1056](https://github.com/QRun-IO/qqq/issues/1056)) and `QInstance.homeAppName` ([qqq#1059](https://github.com/QRun-IO/qqq/issues/1059)); on older backends neither appears and the rest of RC13 works unchanged. The SQL Workbench page needs the [qbit-sql-workbench](https://github.com/QRun-IO/qbit-sql-workbench) QBit, **1.0.0-RC.1** or later, on the backend. RC13 does not publish QQQ 4.1.
|
||||
|
||||
## New features
|
||||
|
||||
- SQL Workbench: render the `SQL_WORKBENCH` widget of the [qbit-sql-workbench](https://github.com/QRun-IO/qbit-sql-workbench) QBit as a super-user SQL notebook.
|
||||
- Personal and shared notebooks with SQL and markdown cells, `:parameters`, versions and pinned results.
|
||||
- A CodeMirror 6 editor with dialect highlighting, schema and QQQ-name autocomplete, and formatting.
|
||||
- A typed, sortable, paged result grid with CSV/XLSX/JSON export, quick charts, and EXPLAIN plan trees.
|
||||
- Read-only runs by default, and a two-step write confirmation for users with the write permission.
|
||||
- CodeMirror and the formatter load only on that page (new `lazyChunks` budgets).
|
||||
|
||||
## Features
|
||||
|
||||
- Support an instance home app. When the backend designates one with `QInstance.homeAppName` ([qqq#1059](https://github.com/QRun-IO/qqq/issues/1059)), and the user may open it, `/app` shows that app's dashboard (a client-side redirect, matching backends that redirect full page loads of `/app/`), the sidebar logo and the breadcrumb Home crumb link there, and the sidebar drops its separate Dashboard entry. Without a home app, or with one the user may not open, `/app` remains Next's overview.
|
||||
- Show `FILE_PREVIEW`-adorned fields as thumbnails, and open a preview on click or Enter. Thumbnails appear in the query grid and card view, the record view (a larger thumbnail with the file name), associated-record tables, child-record list widgets and record-grid widgets. They load lazily at a fixed size, so loading does not shift the layout, with a loading pulse and a file-type icon fallback. The preview is an accessible dialog: focus is trapped and restored, Escape closes it, and Left/Right/Home/End step through the other previews on the same screen. It offers zoom and fit, open-original and download links, and shows type, size, dimensions and caption. PDFs served by the backend (same-origin `/qqq/` paths) and video/audio play inline; other types, and other or cross-origin PDFs (frames are limited to `'self'`), get a file card or open in a new tab. Thumbnails are shown only for explicitly adorned fields; plain URL values are never auto-detected. URLs are limited to http(s), same-origin paths and inline images. The lightbox loads on demand (about 3.5 KB gzip). Requires the QQQ backend adornment from [qqq#1056](https://github.com/QRun-IO/qqq/issues/1056).
|
||||
|
||||
## Product fixes
|
||||
|
||||
- Size record-view cards, dashboard widgets and form fields by the width of their own grid (container queries) rather than the viewport: table panels sit side by side only when each is readable and otherwise stack, cards keep their own height, fields share a row only when they fit, and very wide screens use the room. No horizontal page scroll from 360px up.
|
||||
- Make child-record lists in record-view panels readable: they show the columns that matter most and fit (the record name, status and boolean columns, the most recently changed date; the key and long text last), with `+N more columns` and View All for the rest. Cells align to the top and long text is clamped with the full text on hover. A widget's `onlyIncludeFieldNames` order is respected.
|
||||
- Show dates compactly everywhere: `6:33 PM` today, `Jul 5, 6:33 PM` this year, `Jul 5, 2025` before, on one line with the full zoned timestamp on hover, in the viewer's zone and locale. DATE values are no longer shifted a day west of UTC. Exports are unchanged.
|
||||
|
||||
## Bundle budget
|
||||
|
||||
The record developer route `/app/_/_/dev` and the table developer route `/app/_/dev` each have a 360 KB first-load cap (measured 354.9 KB and 350.8 KB on the combined tree); every other route keeps the 350 KB default. The SQL Workbench editor (CodeMirror 6) and formatter load on demand under separate `lazyChunks` budgets; the FILE_PREVIEW lightbox is a lazy chunk.
|
||||
|
||||
## Verification checkpoint
|
||||
|
||||
PR checks on the RC13 preparation source (typecheck, lint, unit tests and coverage, mocked e2e, bundle budgets, Storybook, image builds and the full real-backend acceptance workflow) are recorded on the preparation PR. During integration the WebKit/tablet NAV-016, NAV-023 and NAV-056 access-control diagnostics and one Firefox image-decode diagnostic (SEC-026) recurred intermittently and passed on rerun; they belong to the tracked [#985](https://github.com/QRun-IO/qqq/issues/985) / [#951](https://github.com/QRun-IO/qqq/issues/951) investigations and no assertion was relaxed. Tag acceptance and public-artifact verification are recorded in the [RC13 GitHub release](https://github.com/QRun-IO/qqq-frontend-next/releases/tag/v1.0.0-RC.13) after delivery. No RC12 test count or artifact digest is attributed to RC13.
|
||||
|
||||
## Remaining RC limits
|
||||
|
||||
The RC12 limits carry forward unchanged: Material parity, sub-path hosting, real Auth0/Drive/QuickSight verification, visual review and application soak remain tracked in [#713](https://github.com/QRun-IO/qqq/issues/713), [#711](https://github.com/QRun-IO/qqq/issues/711), [#712](https://github.com/QRun-IO/qqq/issues/712) and [#904](https://github.com/QRun-IO/qqq/issues/904). The image backend origin remains fixed at build time to `host.docker.internal:8000`; use the JAR or rebuild for another origin.
|
||||