pgkiln 0.41.0 → 0.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/db/migrations/097_generated_app_quality.sql +911 -0
- package/db/migrations/098_app_activity.sql +37 -0
- package/docs/apex-feature-parity.md +5 -5
- package/docs/guide/03-builder.md +66 -18
- package/docs/guide/09-reference.md +6 -2
- package/docs/guide/11-from-apex.md +1 -1
- package/docs/guide/12-development.md +1 -1
- package/docs/guide/14-globalization.md +7 -3
- package/docs/guide/17-mobile.md +1 -1
- package/package.json +1 -1
- package/public/app.css +175 -33
- package/public/app.js +80 -0
- package/public/builder.css +30 -2
- package/public/builder.js +31 -0
- package/src/builder/appfromfile.ts +5 -4
- package/src/builder/appsheets.ts +23 -9
- package/src/builder/appwizard.ts +215 -70
- package/src/builder/blueprints.ts +2 -1
- package/src/builder/components.ts +1 -1
- package/src/builder/home.ts +2 -32
- package/src/builder/newapp.ts +182 -5
- package/src/builder/routes.ts +4 -28
- package/src/i18n/ar.ts +1 -0
- package/src/i18n/cs.ts +1 -0
- package/src/i18n/da.ts +1 -0
- package/src/i18n/de.ts +1 -0
- package/src/i18n/el.ts +1 -0
- package/src/i18n/es.ts +1 -0
- package/src/i18n/fi.ts +1 -0
- package/src/i18n/fr.ts +1 -0
- package/src/i18n/he.ts +1 -0
- package/src/i18n/it.ts +1 -0
- package/src/i18n/ja.ts +1 -0
- package/src/i18n/ko.ts +1 -0
- package/src/i18n/nb.ts +1 -0
- package/src/i18n/pl.ts +1 -0
- package/src/i18n/pt.ts +1 -0
- package/src/i18n/ru.ts +1 -0
- package/src/i18n/sv.ts +1 -0
- package/src/i18n/tr.ts +1 -0
- package/src/i18n/uk.ts +1 -0
- package/src/i18n/zh.ts +1 -0
- package/src/i18n.ts +2 -0
- package/src/icons.ts +2 -1
- package/src/layout.ts +2 -2
- package/src/runtime/charts.ts +8 -3
- package/src/runtime/devbar.ts +35 -14
- package/src/runtime/engine.ts +7 -4
- package/src/runtime/locale.ts +2 -2
- package/src/runtime/regions.ts +25 -2
- package/src/runtime/render.ts +18 -4
- package/src/runtime/report.ts +15 -9
- package/src/runtime/styles.ts +5 -2
package/public/app.css
CHANGED
|
@@ -76,12 +76,100 @@
|
|
|
76
76
|
color-scheme: dark;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
/* ---------- base style "Iris" (theme.base = 'iris'; the default for new
|
|
80
|
+
applications, like APEX 26's Iris theme style): (sprint 72) a light body,
|
|
81
|
+
a dark warm-grey header and menu with a thin accent strip, square-ish
|
|
82
|
+
regions with a rule under their header, 4 px corners, a dark primary button
|
|
83
|
+
and a large page title on the body. Selectors are html[data-style="iris"]:
|
|
84
|
+
they beat the Standard values above, and the application's own colours
|
|
85
|
+
(src/runtime/styles.ts, html:root, later in the page) beat them in turn.
|
|
86
|
+
Contrast: text ≥ 15:1, muted ≥ 5.8:1, accent ≥ 5.5:1, menu text ≥ 10:1. */
|
|
87
|
+
html[data-style="iris"] {
|
|
88
|
+
--bg: #ffffff;
|
|
89
|
+
--surface: #ffffff;
|
|
90
|
+
--surface-2: #f7f6f5;
|
|
91
|
+
--surface-3: #efedeb;
|
|
92
|
+
--text: #1f1c1a;
|
|
93
|
+
--muted: #645e59;
|
|
94
|
+
--border: #e2dfdc;
|
|
95
|
+
--accent: #2b62c4;
|
|
96
|
+
--accent-text: #ffffff;
|
|
97
|
+
--accent-soft: #e9f0fb;
|
|
98
|
+
--header: #3a3632;
|
|
99
|
+
--header-text: #f7f5f3;
|
|
100
|
+
--nav-bg: #2f2b28;
|
|
101
|
+
--nav-text: #ece8e4;
|
|
102
|
+
--nav-muted: #b9b2ab;
|
|
103
|
+
--nav-hover: #403b37;
|
|
104
|
+
--nav-active: #4a443f;
|
|
105
|
+
--nav-current: #ffffff;
|
|
106
|
+
--hot: #2f2b28;
|
|
107
|
+
--hot-text: #ffffff;
|
|
108
|
+
--radius: 4px;
|
|
109
|
+
--font: "Inter", "Segoe UI Variable Text", "Segoe UI", system-ui, -apple-system, Roboto, sans-serif;
|
|
110
|
+
--shadow: none;
|
|
111
|
+
}
|
|
112
|
+
@media (prefers-color-scheme: dark) {
|
|
113
|
+
html[data-style="iris"]:not([data-theme="light"]) {
|
|
114
|
+
--bg: #1a1817;
|
|
115
|
+
--surface: #232120;
|
|
116
|
+
--surface-2: #2a2826;
|
|
117
|
+
--surface-3: #33302d;
|
|
118
|
+
--text: #ece9e6;
|
|
119
|
+
--muted: #aaa49e;
|
|
120
|
+
--border: #3a3633;
|
|
121
|
+
--accent: #84aef2;
|
|
122
|
+
--accent-text: #10131a;
|
|
123
|
+
--accent-soft: #23324a;
|
|
124
|
+
--header: #121110;
|
|
125
|
+
--header-text: #ece9e6;
|
|
126
|
+
--nav-bg: #1d1b1a;
|
|
127
|
+
--nav-text: #e2ded9;
|
|
128
|
+
--nav-muted: #a39c95;
|
|
129
|
+
--nav-hover: #2c2927;
|
|
130
|
+
--nav-active: #36322f;
|
|
131
|
+
--nav-current: #ffffff;
|
|
132
|
+
--hot: #ece9e6;
|
|
133
|
+
--hot-text: #1a1817;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
html[data-style="iris"][data-theme="dark"] {
|
|
137
|
+
--bg: #1a1817;
|
|
138
|
+
--surface: #232120;
|
|
139
|
+
--surface-2: #2a2826;
|
|
140
|
+
--surface-3: #33302d;
|
|
141
|
+
--text: #ece9e6;
|
|
142
|
+
--muted: #aaa49e;
|
|
143
|
+
--border: #3a3633;
|
|
144
|
+
--accent: #84aef2;
|
|
145
|
+
--accent-text: #10131a;
|
|
146
|
+
--accent-soft: #23324a;
|
|
147
|
+
--header: #121110;
|
|
148
|
+
--header-text: #ece9e6;
|
|
149
|
+
--nav-bg: #1d1b1a;
|
|
150
|
+
--nav-text: #e2ded9;
|
|
151
|
+
--nav-muted: #a39c95;
|
|
152
|
+
--nav-hover: #2c2927;
|
|
153
|
+
--nav-active: #36322f;
|
|
154
|
+
--nav-current: #ffffff;
|
|
155
|
+
--hot: #ece9e6;
|
|
156
|
+
--hot-text: #1a1817;
|
|
157
|
+
}
|
|
158
|
+
html[data-style="iris"] .t-header { box-shadow: inset 0 3px 0 var(--accent); }
|
|
159
|
+
html[data-style="iris"] .t-nav { border-right-color: transparent; }
|
|
160
|
+
html[data-style="iris"] .t-titlebar { background: transparent; border-bottom: 0; padding: 1.25rem 1.5rem 0.25rem; }
|
|
161
|
+
html[data-style="iris"] .t-titlebar h1 { font-size: 2rem; font-weight: 700; letter-spacing: -0.02em; line-height: 1.2; }
|
|
162
|
+
html[data-style="iris"] .region-header h2 { font-size: 1.05rem; }
|
|
163
|
+
html[data-style="iris"] .btn-hot { background: var(--hot); border-color: var(--hot); color: var(--hot-text); }
|
|
164
|
+
html[data-style="iris"] .btn-hot:hover { background: color-mix(in srgb, var(--hot) 82%, var(--bg)); }
|
|
165
|
+
html[data-style="iris"] .btn-hot .btn-badge { background: var(--hot-text); color: var(--hot); }
|
|
166
|
+
|
|
79
167
|
/* ---------- base style "Iris" (theme.base = 'iris'; the default for new
|
|
80
168
|
applications, like APEX 26.1's Iris theme style). Selectors are
|
|
81
|
-
html[data-style="
|
|
169
|
+
html[data-style="lavender"]: they beat the Standard values above, and the
|
|
82
170
|
application's own colours (src/runtime/styles.ts, html:root, later in the
|
|
83
171
|
page) beat them in turn. Contrast: text ≥ 15:1, muted ≥ 5.8:1, accent ≥ 6.5:1. */
|
|
84
|
-
html[data-style="
|
|
172
|
+
html[data-style="lavender"] {
|
|
85
173
|
--bg: #f6f6fb;
|
|
86
174
|
--surface: #ffffff;
|
|
87
175
|
--surface-2: #f8f8fc;
|
|
@@ -101,7 +189,7 @@ html[data-style="iris"] {
|
|
|
101
189
|
--shadow: 0 1px 2px rgb(30 27 75 / 5%), 0 2px 8px rgb(30 27 75 / 6%);
|
|
102
190
|
}
|
|
103
191
|
@media (prefers-color-scheme: dark) {
|
|
104
|
-
html[data-style="
|
|
192
|
+
html[data-style="lavender"]:not([data-theme="light"]) {
|
|
105
193
|
--bg: #121120;
|
|
106
194
|
--surface: #1a1930;
|
|
107
195
|
--surface-2: #201f38;
|
|
@@ -119,7 +207,7 @@ html[data-style="iris"] {
|
|
|
119
207
|
--shadow: 0 1px 2px rgb(0 0 0 / 30%), 0 2px 8px rgb(0 0 0 / 25%);
|
|
120
208
|
}
|
|
121
209
|
}
|
|
122
|
-
html[data-style="
|
|
210
|
+
html[data-style="lavender"][data-theme="dark"] {
|
|
123
211
|
--bg: #121120;
|
|
124
212
|
--surface: #1a1930;
|
|
125
213
|
--surface-2: #201f38;
|
|
@@ -136,13 +224,13 @@ html[data-style="iris"][data-theme="dark"] {
|
|
|
136
224
|
--nav-active: #2a2752;
|
|
137
225
|
--shadow: 0 1px 2px rgb(0 0 0 / 30%), 0 2px 8px rgb(0 0 0 / 25%);
|
|
138
226
|
}
|
|
139
|
-
/*
|
|
140
|
-
html[data-style="
|
|
141
|
-
html[data-style="
|
|
142
|
-
html[data-style="
|
|
143
|
-
html[data-style="
|
|
144
|
-
html[data-style="
|
|
145
|
-
html[data-style="
|
|
227
|
+
/* Lavender: rounded navigation pills instead of an edge bar, quieter region headers, pill-shaped primary buttons */
|
|
228
|
+
html[data-style="lavender"] .t-nav a[aria-current="page"] { box-shadow: none; border-radius: calc(var(--radius) * 0.75); }
|
|
229
|
+
html[data-style="lavender"] .t-nav a, html[data-style="lavender"] .t-nav summary { border-radius: calc(var(--radius) * 0.75); }
|
|
230
|
+
html[data-style="lavender"] .region-header { border-bottom-color: transparent; }
|
|
231
|
+
html[data-style="lavender"] .region-header h2 { letter-spacing: -0.01em; }
|
|
232
|
+
html[data-style="lavender"] .btn-hot { border-radius: 999px; }
|
|
233
|
+
html[data-style="lavender"] :focus-visible { outline-color: var(--accent); }
|
|
146
234
|
|
|
147
235
|
* { box-sizing: border-box; }
|
|
148
236
|
html { -webkit-text-size-adjust: 100%; }
|
|
@@ -225,17 +313,18 @@ details.menu > summary::-webkit-details-marker { display: none; }
|
|
|
225
313
|
.t-nav ul ul { padding: 0 0 0 1.4rem; }
|
|
226
314
|
.t-nav a, .t-nav summary {
|
|
227
315
|
display: flex; align-items: center; gap: 0.65rem; padding: 0.5rem 0.65rem; border-radius: 6px;
|
|
228
|
-
color: var(--text); text-decoration: none; cursor: pointer; white-space: nowrap; font-size: 0.93rem;
|
|
316
|
+
color: var(--nav-text, var(--text)); text-decoration: none; cursor: pointer; white-space: nowrap; font-size: 0.93rem;
|
|
229
317
|
}
|
|
230
318
|
.t-nav summary { list-style: none; }
|
|
231
319
|
.t-nav summary::-webkit-details-marker { display: none; }
|
|
232
|
-
.t-nav summary::after { content: "›"; margin-left: auto; color: var(--muted); transition: transform 0.15s; }
|
|
320
|
+
.t-nav summary::after { content: "›"; margin-left: auto; color: var(--nav-muted, var(--muted)); transition: transform 0.15s; }
|
|
233
321
|
.t-nav details[open] > summary::after { transform: rotate(90deg); }
|
|
234
|
-
.t-nav a:hover, .t-nav summary:hover { background: var(--surface-3); }
|
|
235
|
-
|
|
236
|
-
.t-nav
|
|
237
|
-
.t-nav .
|
|
238
|
-
.t-nav
|
|
322
|
+
.t-nav a:hover, .t-nav summary:hover { background: var(--nav-hover, var(--surface-3)); }
|
|
323
|
+
/* (sprint 72) --nav-* let a style have a dark menu (Iris); without them the menu takes the page's colours */
|
|
324
|
+
.t-nav a[aria-current="page"] { background: var(--nav-active); color: var(--nav-current, var(--accent)); font-weight: 600; box-shadow: inset 3px 0 0 var(--accent); }
|
|
325
|
+
.t-nav .in-trail { color: var(--nav-current, var(--accent)); }
|
|
326
|
+
.t-nav .icon { color: var(--nav-muted, var(--muted)); }
|
|
327
|
+
.t-nav a[aria-current="page"] .icon, .t-nav .in-trail .icon { color: var(--nav-current, var(--accent)); }
|
|
239
328
|
|
|
240
329
|
/* ---------- main ---------- */
|
|
241
330
|
.t-main { min-width: 0; }
|
|
@@ -259,6 +348,9 @@ details.menu > summary::-webkit-details-marker { display: none; }
|
|
|
259
348
|
.region-hero { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 1rem 1.5rem; padding: 2rem 1.75rem; border-radius: var(--radius);
|
|
260
349
|
background: var(--accent-soft); color: var(--text); }
|
|
261
350
|
.region-hero-icon .icon { width: 3rem; height: 3rem; color: var(--accent); }
|
|
351
|
+
/* (097) the generated home page: the icon on a tile in the accent colour */
|
|
352
|
+
.region-hero-icon { display: grid; place-items: center; width: 4.5rem; height: 4.5rem; border-radius: calc(var(--radius) + 4px); background: var(--accent); }
|
|
353
|
+
.region-hero-icon .icon { width: 2.4rem; height: 2.4rem; color: var(--accent-text, #fff); }
|
|
262
354
|
.region-hero-text h2 { font-size: 1.75rem; margin: 0 0 0.35rem; }
|
|
263
355
|
.region-hero-buttons, .region-title-buttons, .region-buttons { display: flex; flex-wrap: wrap; gap: 0.5rem; }
|
|
264
356
|
@media (max-width: 640px) { .region-hero { grid-template-columns: 1fr; padding: 1.25rem 1rem; } }
|
|
@@ -401,7 +493,11 @@ details.btn-menu[open] > summary .btn-caret { transform: rotate(-90deg); }
|
|
|
401
493
|
table.report { width: 100%; border-collapse: collapse; font-size: 0.9rem; }
|
|
402
494
|
.report th, .report td { text-align: left; padding: 0.5rem 0.7rem; border-bottom: 1px solid var(--border); white-space: nowrap; }
|
|
403
495
|
.report tbody tr:last-child td { border-bottom: 0; }
|
|
404
|
-
.report th { font-size: 0.
|
|
496
|
+
.report th { font-size: 0.85rem; color: var(--muted); background: var(--surface-2); font-weight: 650; }
|
|
497
|
+
/* (097) the edit link of a row: a pencil in a narrow first column */
|
|
498
|
+
.report th.row-edit, .report td.row-edit { width: 2.25rem; text-align: center; padding-left: 0.4rem; padding-right: 0.4rem; }
|
|
499
|
+
.report td.row-edit a { display: inline-grid; place-items: center; width: 1.9rem; height: 1.9rem; border-radius: 6px; color: var(--accent); }
|
|
500
|
+
.report td.row-edit a:hover, .report td.row-edit a:focus-visible { background: var(--accent-soft); }
|
|
405
501
|
.report th a { color: inherit; text-decoration: none; display: inline-flex; gap: 0.3rem; }
|
|
406
502
|
.report th a:hover { color: var(--accent); }
|
|
407
503
|
.sort-ind { font-size: 0.7em; }
|
|
@@ -446,7 +542,19 @@ table.report { width: 100%; border-collapse: collapse; font-size: 0.9rem; }
|
|
|
446
542
|
.swatch { width: 10px; height: 10px; border-radius: 3px; background: var(--c); flex: none; }
|
|
447
543
|
|
|
448
544
|
/* horizontal bars */
|
|
449
|
-
.chart-bar { display: flex; flex-direction: column; gap: 0
|
|
545
|
+
.chart-bar { display: flex; flex-direction: column; gap: 0; }
|
|
546
|
+
.chart-bar > .bar-row { padding: 0.22rem 0; }
|
|
547
|
+
/* (097) gridlines behind the bars and a value axis under them */
|
|
548
|
+
.bar-stack { position: relative; }
|
|
549
|
+
.bar-grid { position: absolute; inset: -0.22rem 0; pointer-events: none; }
|
|
550
|
+
.bar-gridline { position: absolute; top: 0; bottom: 0; border-left: 1px solid var(--chart-grid); }
|
|
551
|
+
.bar-gridline.baseline { border-left-color: var(--chart-axis); }
|
|
552
|
+
.bar-stack > .bar-track { position: relative; }
|
|
553
|
+
.bar-axis { display: grid; grid-template-columns: minmax(70px, 30%) 1fr auto; gap: 0.75rem; font-size: 0.72rem; color: var(--muted); }
|
|
554
|
+
.bar-axis > :last-child { min-width: 3rem; }
|
|
555
|
+
.bar-ticks { position: relative; height: 1.1rem; }
|
|
556
|
+
.bar-ticks > span { position: absolute; top: 0.15rem; transform: translateX(-50%); font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
557
|
+
.bar-ticks > span:first-child { transform: none; }
|
|
450
558
|
.bar-row { display: grid; grid-template-columns: minmax(70px, 30%) 1fr auto; gap: 0.75rem; align-items: center; font-size: 0.88rem; }
|
|
451
559
|
.bar-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--muted); }
|
|
452
560
|
.bar-stack { display: flex; flex-direction: column; gap: 2px; }
|
|
@@ -822,7 +930,7 @@ pre.source { font-family: var(--mono); font-size: 0.82rem; background: var(--sur
|
|
|
822
930
|
.card.metric { grid-template-columns: 1fr; row-gap: 0.25rem; }
|
|
823
931
|
.metric-icon { grid-row: auto; width: 36px; height: 36px; }
|
|
824
932
|
.metric-value { font-size: 1.35rem; }
|
|
825
|
-
.bar-row { grid-template-columns: minmax(60px, 35%) 1fr auto; gap: 0.5rem; }
|
|
933
|
+
.bar-row, .bar-axis { grid-template-columns: minmax(60px, 35%) 1fr auto; gap: 0.5rem; }
|
|
826
934
|
.title-row .buttons { width: 100%; }
|
|
827
935
|
.title-row .buttons .btn { flex: 1 1 auto; }
|
|
828
936
|
|
|
@@ -842,7 +950,9 @@ pre.source { font-family: var(--mono); font-size: 0.82rem; background: var(--sur
|
|
|
842
950
|
display: grid; grid-template-columns: minmax(90px, 38%) minmax(0, 1fr); gap: 0.75rem;
|
|
843
951
|
border: 0; padding: 0.3rem 0.8rem; white-space: normal; max-width: none; text-align: left !important; overflow-wrap: anywhere;
|
|
844
952
|
}
|
|
845
|
-
.report-reflow td::before { content: attr(data-label); font-size: 0.
|
|
953
|
+
.report-reflow td::before { content: attr(data-label); font-size: 0.8rem; font-weight: 650; color: var(--muted); }
|
|
954
|
+
.report-reflow td.row-edit { width: auto; text-align: left; }
|
|
955
|
+
.report-reflow td.row-edit::before { content: none; }
|
|
846
956
|
.report-reflow td.empty { display: block; }
|
|
847
957
|
.report-reflow td.empty::before { content: none; }
|
|
848
958
|
.report-reflow td.pre { white-space: pre-wrap; }
|
|
@@ -1689,19 +1799,51 @@ dialog.crop-dialog::backdrop { background: rgb(15 20 30 / 55%); }
|
|
|
1689
1799
|
.crop-dialog .buttons { display: flex; justify-content: flex-end; gap: 0.5rem; }
|
|
1690
1800
|
|
|
1691
1801
|
/* (079) Developer toolbar: shown to developers with a builder session in the same browser.
|
|
1692
|
-
|
|
1693
|
-
.
|
|
1694
|
-
|
|
1695
|
-
.devbar
|
|
1696
|
-
|
|
1697
|
-
.
|
|
1698
|
-
.devbar
|
|
1699
|
-
.devbar
|
|
1700
|
-
|
|
1701
|
-
.devbar
|
|
1702
|
-
.devbar-
|
|
1802
|
+
(sprint 72) A compact bar of icons centred at the bottom (APEX's runtime developer toolbar);
|
|
1803
|
+
app.js docks it at the top, left or right and shrinks it to a slim tab when it is not used.
|
|
1804
|
+
The page keeps room for it, so it never covers the menu's last entry or a report's pager. */
|
|
1805
|
+
.devbar { --devbar-size: 2.75rem; position: fixed; z-index: 900; left: 50%; bottom: 0.5rem; transform: translateX(-50%);
|
|
1806
|
+
background: #1b2330; color: #fff; border-radius: 10px; box-shadow: 0 4px 16px rgb(0 0 0 / 25%); font-size: 0.8125rem;
|
|
1807
|
+
transition: transform 0.2s ease, opacity 0.2s ease; }
|
|
1808
|
+
.devbar-tools { display: flex; align-items: center; gap: 0.15rem; list-style: none; margin: 0; padding: 0.25rem; }
|
|
1809
|
+
.devbar .devbar-tool { display: inline-grid; place-items: center; width: 2.25rem; height: 2.25rem; border-radius: 7px; color: #dbe7ff;
|
|
1810
|
+
background: none; border: 0; cursor: pointer; list-style: none; padding: 0; }
|
|
1811
|
+
.devbar-tool::-webkit-details-marker { display: none; }
|
|
1812
|
+
.devbar .devbar-tool:hover, .devbar-menu[open] > .devbar-tool, .devbar-quick[aria-pressed="true"] { background: #2f3b4f; color: #fff; }
|
|
1813
|
+
.devbar-debug-menu.is-on > .devbar-tool { color: #ffd166; }
|
|
1814
|
+
.devbar a, .devbar-pop a { color: #9cc8ff; }
|
|
1815
|
+
.devbar a:focus-visible, .devbar button:focus-visible, .devbar select:focus-visible, .devbar summary:focus-visible, .devbar input:focus-visible { outline: 2px solid #9cc8ff; outline-offset: 2px; }
|
|
1816
|
+
.devbar-menu { position: relative; }
|
|
1817
|
+
.devbar-pop { position: absolute; bottom: calc(100% + 0.6rem); left: 50%; transform: translateX(-50%); min-width: 14rem; max-width: min(22rem, calc(100vw - 2rem));
|
|
1818
|
+
background: #1b2330; border-radius: 8px; box-shadow: 0 4px 16px rgb(0 0 0 / 30%); padding: 0.6rem 0.75rem; display: grid; gap: 0.5rem; }
|
|
1819
|
+
.devbar-debug { display: flex; flex-wrap: wrap; align-items: center; gap: 0.35rem; margin: 0; }
|
|
1820
|
+
.devbar-debug label { flex-basis: 100%; }
|
|
1703
1821
|
.devbar-debug select, .devbar-debug button { font: inherit; font-size: 0.8125rem; padding: 0.1rem 0.3rem; border-radius: 4px; border: 1px solid #5e6a7a; background: #2b3340; color: #fff; }
|
|
1704
1822
|
.devbar-debug button { cursor: pointer; }
|
|
1823
|
+
.devbar-dock { border: 0; margin: 0; padding: 0; display: grid; grid-template-columns: 1fr 1fr; gap: 0.25rem 0.75rem; }
|
|
1824
|
+
.devbar-dock legend { padding: 0; margin-bottom: 0.25rem; font-weight: 600; }
|
|
1825
|
+
.devbar-pop .devbar-facts { color: #fff; }
|
|
1826
|
+
/* docked at the top, or on the left or right edge (a column of icons) */
|
|
1827
|
+
.devbar[data-dock="top"] { bottom: auto; top: 0.5rem; }
|
|
1828
|
+
.devbar[data-dock="top"] .devbar-pop { bottom: auto; top: calc(100% + 0.6rem); }
|
|
1829
|
+
.devbar[data-dock="left"], .devbar[data-dock="right"] { left: 0.5rem; bottom: auto; top: 50%; transform: translateY(-50%); }
|
|
1830
|
+
.devbar[data-dock="right"] { left: auto; right: 0.5rem; }
|
|
1831
|
+
.devbar[data-dock="left"] .devbar-tools, .devbar[data-dock="right"] .devbar-tools { flex-direction: column; }
|
|
1832
|
+
.devbar[data-dock="left"] .devbar-pop { bottom: auto; top: 0; left: calc(100% + 0.6rem); transform: none; }
|
|
1833
|
+
.devbar[data-dock="right"] .devbar-pop { bottom: auto; top: 0; left: auto; right: calc(100% + 0.6rem); transform: none; }
|
|
1834
|
+
/* not used for a while: a slim tab at the edge; pointing at it or focusing it brings it back */
|
|
1835
|
+
.devbar.is-idle { opacity: 0.85; }
|
|
1836
|
+
.devbar.is-idle[data-dock="bottom"] { transform: translate(-50%, calc(100% - 6px)); }
|
|
1837
|
+
.devbar.is-idle[data-dock="top"] { transform: translate(-50%, calc(-100% + 6px)); }
|
|
1838
|
+
.devbar.is-idle[data-dock="left"] { transform: translate(calc(-100% + 6px), -50%); }
|
|
1839
|
+
.devbar.is-idle[data-dock="right"] { transform: translate(calc(100% - 6px), -50%); }
|
|
1840
|
+
.devbar.is-idle .devbar-tools { pointer-events: none; }
|
|
1841
|
+
@media (prefers-reduced-motion: reduce) { .devbar { transition: none; } }
|
|
1842
|
+
/* room for the bar: the menu and the page end above it */
|
|
1843
|
+
body:has(.devbar[data-dock="bottom"]) .t-main, body:has(.devbar[data-dock="bottom"]) .t-nav > ul { padding-bottom: 3.75rem; }
|
|
1844
|
+
body:has(.devbar[data-dock="top"]) .t-body { padding-top: 3.25rem; }
|
|
1845
|
+
body:has(.devbar[data-dock="left"]) .t-body { padding-left: 3.25rem; }
|
|
1846
|
+
body:has(.devbar[data-dock="right"]) .t-main { padding-right: 3.5rem; }
|
|
1705
1847
|
.devbar-page .t-main { max-width: 72rem; margin: 0 auto; padding: 1rem; }
|
|
1706
1848
|
.devbar-facts { display: grid; grid-template-columns: max-content 1fr; gap: 0.25rem 1rem; }
|
|
1707
1849
|
.devbar-facts dd { margin: 0; overflow-wrap: anywhere; }
|
package/public/app.js
CHANGED
|
@@ -828,6 +828,15 @@ const pgkilnUnsaved = (() => {
|
|
|
828
828
|
});
|
|
829
829
|
drawer.addEventListener('change', () => toggle.setAttribute('aria-expanded', String(navOpen())));
|
|
830
830
|
}
|
|
831
|
+
// (097) a long menu shows the current page's entry: the menu scrolls, not the page
|
|
832
|
+
{
|
|
833
|
+
const nav = document.getElementById('t-nav');
|
|
834
|
+
const current = nav?.querySelector('a[aria-current="page"]');
|
|
835
|
+
if (nav && current && nav.scrollHeight > nav.clientHeight) {
|
|
836
|
+
const top = current.getBoundingClientRect().top - nav.getBoundingClientRect().top;
|
|
837
|
+
if (top < 0 || top + current.offsetHeight > nav.clientHeight) nav.scrollTop += top - nav.clientHeight / 3;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
831
840
|
|
|
832
841
|
// ------------------------------------------------------------ modal dialogs
|
|
833
842
|
const POSITIONS = ['center', 'left', 'right', 'top', 'bottom'];
|
|
@@ -3606,6 +3615,77 @@ function areaFilter(map, f) {
|
|
|
3606
3615
|
check();
|
|
3607
3616
|
})();
|
|
3608
3617
|
|
|
3618
|
+
// (sprint 72) Install app (PWA): the header's button appears when the browser offers the installation.
|
|
3619
|
+
(() => {
|
|
3620
|
+
const button = document.querySelector('[data-pwa-install]');
|
|
3621
|
+
if (!button) return;
|
|
3622
|
+
let offer = null;
|
|
3623
|
+
window.addEventListener('beforeinstallprompt', (e) => {
|
|
3624
|
+
e.preventDefault();
|
|
3625
|
+
offer = e;
|
|
3626
|
+
button.hidden = false;
|
|
3627
|
+
});
|
|
3628
|
+
button.addEventListener('click', async () => {
|
|
3629
|
+
if (!offer) return;
|
|
3630
|
+
offer.prompt();
|
|
3631
|
+
await offer.userChoice.catch(() => null);
|
|
3632
|
+
offer = null;
|
|
3633
|
+
button.hidden = true;
|
|
3634
|
+
});
|
|
3635
|
+
window.addEventListener('appinstalled', () => { button.hidden = true; });
|
|
3636
|
+
})();
|
|
3637
|
+
|
|
3638
|
+
// (sprint 72) The developer toolbar: docked where the developer chose (bottom, top, left, right;
|
|
3639
|
+
// kept in this browser), shrunk to a slim tab after a few seconds without use (unless turned off),
|
|
3640
|
+
// back when pointed at or focused. Its panels close when another opens or on Escape.
|
|
3641
|
+
(() => {
|
|
3642
|
+
const bar = document.querySelector('.devbar');
|
|
3643
|
+
if (!bar) return;
|
|
3644
|
+
const KEY = 'pgkiln.devbar';
|
|
3645
|
+
let prefs = { dock: 'bottom', autohide: true };
|
|
3646
|
+
try { prefs = { ...prefs, ...JSON.parse(localStorage.getItem(KEY) || '{}') }; } catch {}
|
|
3647
|
+
if (!['bottom', 'top', 'left', 'right'].includes(prefs.dock)) prefs.dock = 'bottom';
|
|
3648
|
+
const save = () => { try { localStorage.setItem(KEY, JSON.stringify(prefs)); } catch {} };
|
|
3649
|
+
const settings = bar.querySelector('.devbar-settings');
|
|
3650
|
+
settings?.closest('li')?.removeAttribute('hidden');
|
|
3651
|
+
const apply = () => {
|
|
3652
|
+
bar.dataset.dock = prefs.dock;
|
|
3653
|
+
bar.querySelectorAll('input[name="devbar-dock"]').forEach((r) => { r.checked = r.value === prefs.dock; });
|
|
3654
|
+
const auto = bar.querySelector('input[name="devbar-autohide"]');
|
|
3655
|
+
if (auto) auto.checked = !!prefs.autohide;
|
|
3656
|
+
};
|
|
3657
|
+
apply();
|
|
3658
|
+
bar.addEventListener('change', (e) => {
|
|
3659
|
+
if (e.target.name === 'devbar-dock') prefs.dock = e.target.value;
|
|
3660
|
+
else if (e.target.name === 'devbar-autohide') prefs.autohide = e.target.checked;
|
|
3661
|
+
else return;
|
|
3662
|
+
save();
|
|
3663
|
+
apply();
|
|
3664
|
+
});
|
|
3665
|
+
const menus = [...bar.querySelectorAll('details.devbar-menu')];
|
|
3666
|
+
menus.forEach((m) => m.addEventListener('toggle', () => { if (m.open) menus.forEach((o) => { if (o !== m) o.open = false; }); }));
|
|
3667
|
+
document.addEventListener('keydown', (e) => {
|
|
3668
|
+
if (e.key === 'Escape' && menus.some((m) => m.open)) {
|
|
3669
|
+
const open = menus.find((m) => m.open);
|
|
3670
|
+
open.open = false;
|
|
3671
|
+
open.querySelector('summary')?.focus();
|
|
3672
|
+
}
|
|
3673
|
+
});
|
|
3674
|
+
document.addEventListener('click', (e) => { if (!bar.contains(e.target)) menus.forEach((m) => { m.open = false; }); });
|
|
3675
|
+
let timer = 0;
|
|
3676
|
+
const busy = () => bar.matches(':hover') || bar.contains(document.activeElement) || menus.some((m) => m.open) || !!bar.querySelector('[aria-pressed="true"]');
|
|
3677
|
+
const later = () => {
|
|
3678
|
+
clearTimeout(timer);
|
|
3679
|
+
if (prefs.autohide) timer = setTimeout(() => { if (!busy()) bar.classList.add('is-idle'); else later(); }, 3000);
|
|
3680
|
+
};
|
|
3681
|
+
const wake = () => { bar.classList.remove('is-idle'); later(); };
|
|
3682
|
+
bar.addEventListener('pointerenter', wake);
|
|
3683
|
+
bar.addEventListener('focusin', wake);
|
|
3684
|
+
bar.addEventListener('pointerleave', later);
|
|
3685
|
+
bar.addEventListener('change', wake);
|
|
3686
|
+
later();
|
|
3687
|
+
})();
|
|
3688
|
+
|
|
3609
3689
|
// (sprint 59) Quick edit from the developer toolbar (APEX: Quick Edit): while it is on, pointing at a
|
|
3610
3690
|
// region, item or button outlines it and a click opens it in the Page Designer (in the builder's tab);
|
|
3611
3691
|
// Esc or the button again turns it off. The toolbar is only there for developers of the application.
|
package/public/builder.css
CHANGED
|
@@ -768,7 +768,12 @@ button.is-busy { opacity: 0.7; cursor: progress; }
|
|
|
768
768
|
--font: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; --font-size: 15px; --shadow: 0 1px 2px rgb(16 24 40 / 6%), 0 1px 3px rgb(16 24 40 / 6%);
|
|
769
769
|
color-scheme: light; margin-bottom: 1rem; border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden;
|
|
770
770
|
background: var(--bg); color: var(--text); font: var(--font-size)/1.5 var(--font); }
|
|
771
|
-
.tr-preview-iris { --bg: #
|
|
771
|
+
.tr-preview-iris { --bg: #ffffff; --surface-2: #f7f6f5; --surface-3: #efedeb; --text: #1f1c1a; --muted: #645e59; --border: #e2dfdc;
|
|
772
|
+
--accent: #2b62c4; --accent-soft: #e9f0fb; --header: #3a3632; --header-text: #f7f5f3; --radius: 4px; --shadow: none;
|
|
773
|
+
--font: "Inter", "Segoe UI Variable Text", "Segoe UI", system-ui, -apple-system, Roboto, sans-serif; }
|
|
774
|
+
.tr-preview-iris .tr-preview-header { box-shadow: inset 0 3px 0 var(--accent); }
|
|
775
|
+
.tr-preview-iris .btn-hot { background: #2f2b28; border-color: #2f2b28; color: #fff; }
|
|
776
|
+
.tr-preview-lavender { --bg: #f6f6fb; --surface-2: #f8f8fc; --surface-3: #eeeef6; --text: #1d1b2e; --muted: #5f5d75; --border: #e3e2ef;
|
|
772
777
|
--accent: #5146d8; --accent-soft: #eeedfd; --header: #1e1a4d; --header-text: #f1f0ff; --radius: 12px;
|
|
773
778
|
--font: "Inter", "Segoe UI Variable Text", "Segoe UI", system-ui, -apple-system, Roboto, sans-serif; --shadow: 0 1px 2px rgb(30 27 75 / 5%), 0 2px 8px rgb(30 27 75 / 6%); }
|
|
774
779
|
.tr-preview-header { display: flex; align-items: center; gap: 0.6rem; padding: 0.6rem 1rem; background: var(--header); color: var(--header-text); font-weight: 700; }
|
|
@@ -779,7 +784,7 @@ button.is-busy { opacity: 0.7; cursor: progress; }
|
|
|
779
784
|
.tr-preview input { background: var(--surface); color: var(--text); border-color: var(--border); }
|
|
780
785
|
.tr-preview .btn { background: var(--surface); color: var(--text); border-color: var(--border); border-radius: calc(var(--radius) * 0.75); font: inherit; }
|
|
781
786
|
.tr-preview .btn-hot { background: var(--accent); border-color: var(--accent); color: var(--accent-text); }
|
|
782
|
-
.tr-preview-
|
|
787
|
+
.tr-preview-lavender .btn-hot { border-radius: 999px; }
|
|
783
788
|
|
|
784
789
|
/* icon picker (forms.ts iconPicker) */
|
|
785
790
|
.icon-picker > summary { display: inline-flex; align-items: center; gap: 0.5rem; min-height: 34px; padding: 0.3rem 0.6rem; border: 1px solid var(--border); border-radius: 6px; cursor: pointer; list-style: none; background: var(--surface); }
|
|
@@ -872,3 +877,26 @@ details.prop-group-more[open] > summary { margin-bottom: 8px; }
|
|
|
872
877
|
.gal-foot { display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: 8px; }
|
|
873
878
|
.gal-foot form { margin: 0; }
|
|
874
879
|
.ab-group select { min-width: 140px; }
|
|
880
|
+
|
|
881
|
+
/* (sprint 73) the Create Application wizard on one page */
|
|
882
|
+
.create-sources { display: grid; grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr)); gap: 0.5rem; margin: 0.75rem 0 1rem; }
|
|
883
|
+
.create-source { display: flex; gap: 0.6rem; align-items: flex-start; padding: 0.6rem 0.75rem; border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); color: var(--text); text-decoration: none; }
|
|
884
|
+
.create-source:hover, .create-source:focus-visible { border-color: var(--accent); }
|
|
885
|
+
.create-source .icon { color: var(--accent); margin-top: 0.15rem; }
|
|
886
|
+
.create-source span { display: grid; gap: 0.1rem; min-width: 0; overflow-wrap: anywhere; }
|
|
887
|
+
.create-app .region + form, .create-app form .region { margin-top: 1rem; }
|
|
888
|
+
.aw-pages { list-style: none; margin: 0 0 0.5rem; padding: 0; display: grid; gap: 0.4rem; }
|
|
889
|
+
.aw-pages li { display: grid; grid-template-columns: minmax(10rem, 1fr) minmax(12rem, 16rem); gap: 0.15rem 0.75rem; align-items: center; padding: 0.35rem 0; border-bottom: 1px solid var(--border); }
|
|
890
|
+
.aw-pages li > small, .aw-pages li > .muted:last-child { grid-column: 1 / -1; }
|
|
891
|
+
.aw-page-kind { display: inline-flex; align-items: center; gap: 0.4rem; font-weight: 600; overflow-wrap: anywhere; min-width: 0; }
|
|
892
|
+
@media (max-width: 560px) { .aw-pages li { grid-template-columns: 1fr; } }
|
|
893
|
+
.aw-features { display: grid; grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)); gap: 0 1rem; }
|
|
894
|
+
.aw-advanced > summary { cursor: pointer; list-style: none; }
|
|
895
|
+
.aw-advanced > summary::-webkit-details-marker { display: none; }
|
|
896
|
+
.aw-advanced .region-body { padding: 1rem; }
|
|
897
|
+
/* the overlay while a long form (Create application) is being processed */
|
|
898
|
+
.progress-overlay { position: fixed; inset: 0; z-index: 1000; display: grid; place-items: center; background: rgb(0 0 0 / 35%); }
|
|
899
|
+
.progress-box { display: flex; align-items: center; gap: 0.75rem; padding: 1rem 1.25rem; border-radius: var(--radius); background: var(--surface); color: var(--text); box-shadow: 0 8px 24px rgb(0 0 0 / 25%); }
|
|
900
|
+
.progress-spinner { width: 1.25rem; height: 1.25rem; border-radius: 50%; border: 3px solid var(--border); border-top-color: var(--accent); animation: progress-spin 0.8s linear infinite; }
|
|
901
|
+
@keyframes progress-spin { to { transform: rotate(360deg); } }
|
|
902
|
+
@media (prefers-reduced-motion: reduce) { .progress-spinner { animation: none; } }
|
package/public/builder.js
CHANGED
|
@@ -859,3 +859,34 @@ document.addEventListener('click', async (e) => {
|
|
|
859
859
|
show(e.target.closest?.('.pe [data-prop]'));
|
|
860
860
|
});
|
|
861
861
|
})();
|
|
862
|
+
|
|
863
|
+
// (sprint 73) a proposed value is selected on the first focus, so typing replaces it (data-select-once)
|
|
864
|
+
document.querySelectorAll('input[data-select-once]').forEach((el) => {
|
|
865
|
+
el.addEventListener('focus', () => {
|
|
866
|
+
el.select();
|
|
867
|
+
// the click that focused it would put the caret back: keep the selection
|
|
868
|
+
el.addEventListener('mouseup', (e) => e.preventDefault(), { once: true });
|
|
869
|
+
}, { once: true });
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
// (sprint 73) a form that takes a while (data-progress="Creating …"): an overlay with the message while the server works
|
|
873
|
+
document.addEventListener('submit', (e) => {
|
|
874
|
+
const form = e.target.closest?.('form[data-progress]');
|
|
875
|
+
if (!form || e.defaultPrevented || !form.checkValidity()) return;
|
|
876
|
+
const overlay = document.createElement('div');
|
|
877
|
+
overlay.className = 'progress-overlay';
|
|
878
|
+
overlay.setAttribute('role', 'status');
|
|
879
|
+
overlay.setAttribute('aria-live', 'polite');
|
|
880
|
+
const box = document.createElement('div');
|
|
881
|
+
box.className = 'progress-box';
|
|
882
|
+
const spin = document.createElement('span');
|
|
883
|
+
spin.className = 'progress-spinner';
|
|
884
|
+
spin.setAttribute('aria-hidden', 'true');
|
|
885
|
+
const text = document.createElement('span');
|
|
886
|
+
text.textContent = form.dataset.progress;
|
|
887
|
+
box.append(spin, text);
|
|
888
|
+
overlay.append(box);
|
|
889
|
+
document.body.append(overlay);
|
|
890
|
+
// back from the browser's cache (the back button): no overlay
|
|
891
|
+
window.addEventListener('pageshow', (ev) => { if (ev.persisted) overlay.remove(); }, { once: true });
|
|
892
|
+
});
|
|
@@ -21,7 +21,7 @@ import type { Session } from '../session.ts';
|
|
|
21
21
|
import { MAX_MB, preview, resultHtml } from './dataload.ts';
|
|
22
22
|
import { buildAppFromSheets, MAX_SHEETS, parseBook, sheetsForm, type BuiltBook, type NamedSheet } from './appsheets.ts';
|
|
23
23
|
import { appWizardRoutes, PASTED } from './appwizard.ts';
|
|
24
|
-
import { checkNewApp, createApp, createAppError, reservedSchema, type CheckedApp } from './newapp.ts';
|
|
24
|
+
import { finishGeneratedApp, checkNewApp, createApp, createAppError, reservedSchema, type CheckedApp } from './newapp.ts';
|
|
25
25
|
import { currentWorkspace } from './workspaces.ts';
|
|
26
26
|
import { BASE, csrf, developer, input, region, select, send, shell, type Body, type Req } from './ui.ts';
|
|
27
27
|
|
|
@@ -149,9 +149,9 @@ export async function buildAppFromFile(checked: CheckedApp, sheet: Sheet, b: Bod
|
|
|
149
149
|
const generate = async (kind: string, page: number, options: Record<string, unknown>) => {
|
|
150
150
|
await c.query('select meta.generate_page($1, $2, $3::regclass, $4, $5::jsonb)', [checked.alias, kind, T, page, JSON.stringify(options)]);
|
|
151
151
|
};
|
|
152
|
-
const label = (await c.query(
|
|
153
|
-
await generate('report_form', 2, { form_page: 3
|
|
154
|
-
pages.push({ page: 2, label }, { page: 3, label: `${
|
|
152
|
+
const { label, singular } = (await c.query('select meta.wizard_table_label($1::regclass) as label, meta.wizard_table_label($1::regclass, false) as singular', [T])).rows[0] as { label: string; singular: string };
|
|
153
|
+
await generate('report_form', 2, { form_page: 3 });
|
|
154
|
+
pages.push({ page: 2, label }, { page: 3, label: `${singular} form` });
|
|
155
155
|
let next = 4;
|
|
156
156
|
// the statistics tell which columns group the rows: a few values, repeated (not a name per row)
|
|
157
157
|
const rows = result.inserted;
|
|
@@ -178,6 +178,7 @@ export async function buildAppFromFile(checked: CheckedApp, sheet: Sheet, b: Bod
|
|
|
178
178
|
pages.push({ page: next++, label: `Faceted search (${facets.join(', ')})` });
|
|
179
179
|
}
|
|
180
180
|
}
|
|
181
|
+
await finishGeneratedApp(c, app.id, { description: checked.description });
|
|
181
182
|
return { app: { id: app.id, alias: checked.alias, existingAccount: app.existingAccount }, table: T, result, pages, notes };
|
|
182
183
|
});
|
|
183
184
|
}
|
package/src/builder/appsheets.ts
CHANGED
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
type Sheet,
|
|
20
20
|
} from '../dataload.ts';
|
|
21
21
|
import { preview } from './dataload.ts';
|
|
22
|
-
import { createApp, createAppError, type CheckedApp } from './newapp.ts';
|
|
22
|
+
import { finishGeneratedApp, createApp, createAppError, type CheckedApp } from './newapp.ts';
|
|
23
23
|
import { groupColumn } from './appfromfile.ts';
|
|
24
24
|
import { region, type Body } from './ui.ts';
|
|
25
25
|
|
|
@@ -278,14 +278,21 @@ interface Catalog {
|
|
|
278
278
|
* A dashboard page: a chart per table (at most six) with the number of rows per
|
|
279
279
|
* parent row (the first foreign key), or per value of a text, yes/no or date
|
|
280
280
|
* column whose values repeat (from the statistics). Null when no table has one.
|
|
281
|
+
* (097) Tables with row level security are left out (the application's role may
|
|
282
|
+
* see none of their rows: an empty chart); the charts are titled "Employees by
|
|
283
|
+
* Manager", and the Dashboard comes right after Home in the navigation menu.
|
|
281
284
|
*/
|
|
282
|
-
export async function addDashboard(c: Client, alias: string, tables: string[], page: number): Promise<{ page: number; label: string } | null> {
|
|
285
|
+
export async function addDashboard(c: Client, alias: string, tables: string[], page: number, skipped: string[] = []): Promise<{ page: number; label: string } | null> {
|
|
283
286
|
const picks: { table: string; group: string }[] = [];
|
|
284
287
|
for (const t of tables) {
|
|
285
288
|
if (picks.length >= MAX_CHARTS) break;
|
|
289
|
+
const rel = (await c.query('select relrowsecurity as rls, greatest(reltuples, 0) as n from pg_class where oid = $1::regclass', [t])).rows[0];
|
|
290
|
+
if (rel.rls) {
|
|
291
|
+
skipped.push(t);
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
286
294
|
const catalog = (await c.query<Catalog>('select column_name, kind, is_pk, distinct_values, fk_table::text from meta.wizard_catalog($1::regclass)', [t])).rows;
|
|
287
|
-
const
|
|
288
|
-
const group = catalog.find((x) => x.fk_table && !x.is_pk)?.column_name ?? groupColumn(catalog, rows);
|
|
295
|
+
const group = catalog.find((x) => x.fk_table && !x.is_pk)?.column_name ?? groupColumn(catalog, Number(rel.n));
|
|
289
296
|
if (group) picks.push({ table: t, group });
|
|
290
297
|
}
|
|
291
298
|
if (!picks.length) return null;
|
|
@@ -295,7 +302,7 @@ export async function addDashboard(c: Client, alias: string, tables: string[], p
|
|
|
295
302
|
JSON.stringify({ label: 'Dashboard', icon: 'chart', nav, label_column: group, function: 'count', value_column: null }),
|
|
296
303
|
]);
|
|
297
304
|
const title = async (table: string, group: string) =>
|
|
298
|
-
(await c.query(`select meta.
|
|
305
|
+
(await c.query(`select meta.wizard_table_label($1::regclass) || ' by ' || meta.wizard_column_label($1::regclass, $2) as t`, [table, group])).rows[0].t as string;
|
|
299
306
|
const dash = (await generate(picks[0].table, picks[0].group, page, true)).rows[0].id;
|
|
300
307
|
await c.query('update meta.region set title = $2 where page_id = $1', [dash, await title(picks[0].table, picks[0].group)]);
|
|
301
308
|
for (const [n, p] of picks.slice(1).entries()) {
|
|
@@ -306,7 +313,13 @@ export async function addDashboard(c: Client, alias: string, tables: string[], p
|
|
|
306
313
|
await c.query('delete from meta.page where id = $1', [tmp]);
|
|
307
314
|
}
|
|
308
315
|
if (picks.length > 1) await c.query('update meta.region set columns = 6 where page_id = $1', [dash]);
|
|
309
|
-
|
|
316
|
+
// right after Home (the first top-level entry)
|
|
317
|
+
await c.query(
|
|
318
|
+
`update meta.nav_entry e set seq = coalesce((select min(seq) from meta.nav_entry h where h.app_id = e.app_id and h.parent_id is null and h.target_page = a.home_page), 0) + 1
|
|
319
|
+
from meta.app a where a.alias = $1 and e.app_id = a.id and e.parent_id is null and e.target_page = $2`,
|
|
320
|
+
[alias, page],
|
|
321
|
+
);
|
|
322
|
+
return { page, label: `Dashboard (${(await Promise.all(picks.map((p) => title(p.table, p.group)))).join(', ')})` };
|
|
310
323
|
}
|
|
311
324
|
|
|
312
325
|
/** Create the application, the tables, the rows, the foreign keys and the pages, in one transaction. */
|
|
@@ -371,9 +384,9 @@ export async function buildAppFromSheets(checked: CheckedApp, sheets: NamedSheet
|
|
|
371
384
|
let next = 2;
|
|
372
385
|
for (const p of plans) {
|
|
373
386
|
const T = qualified.get(p.k)!;
|
|
374
|
-
const label = (await c.query('select meta.
|
|
375
|
-
await c.query('select meta.generate_page($1, $2, $3::regclass, $4, $5::jsonb)', [checked.alias, 'report_form', T, next, JSON.stringify({ form_page: next + 1
|
|
376
|
-
pages.push({ page: next, label }, { page: next + 1, label: `${
|
|
387
|
+
const { label, singular } = (await c.query('select meta.wizard_table_label($1::regclass) as label, meta.wizard_table_label($1::regclass, false) as singular', [T])).rows[0] as { label: string; singular: string };
|
|
388
|
+
await c.query('select meta.generate_page($1, $2, $3::regclass, $4, $5::jsonb)', [checked.alias, 'report_form', T, next, JSON.stringify({ form_page: next + 1 })]);
|
|
389
|
+
pages.push({ page: next, label }, { page: next + 1, label: `${singular} form` });
|
|
377
390
|
next += 2;
|
|
378
391
|
}
|
|
379
392
|
if (b.chart === 'true') {
|
|
@@ -381,6 +394,7 @@ export async function buildAppFromSheets(checked: CheckedApp, sheets: NamedSheet
|
|
|
381
394
|
if (dash) pages.push(dash);
|
|
382
395
|
else notes.push('No dashboard: no table has a foreign key or a text, yes/no or date column with a few values that repeat, to count the rows by.');
|
|
383
396
|
}
|
|
397
|
+
await finishGeneratedApp(c, app.id, { description: checked.description });
|
|
384
398
|
return { app: { id: app.id, alias: checked.alias, existingAccount: app.existingAccount }, tables, foreignKeys, pages, notes };
|
|
385
399
|
});
|
|
386
400
|
}
|