pgkiln 0.45.0 → 0.47.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/106_form_designer.sql +102 -0
- package/db/migrations/107_app_version.sql +9 -0
- package/db/migrations/108_sql_history.sql +25 -0
- package/docs/apex-feature-parity.md +33 -4
- package/docs/guide/03-builder.md +123 -28
- package/docs/guide/04-pages-and-regions.md +74 -2
- package/docs/guide/05-items.md +14 -2
- package/docs/guide/08-security.md +1 -1
- package/docs/guide/09-reference.md +1 -1
- package/docs/guide/10-tutorial.md +1 -1
- package/docs/guide/11-from-apex.md +2 -1
- package/docs/guide/12-development.md +26 -3
- package/docs/guide/16-files.md +2 -2
- package/examples/hr/hr_61_form_designer.sql +80 -0
- package/package.json +1 -1
- package/public/app.css +62 -2
- package/public/app.js +244 -35
- package/public/builder.css +21 -1
- package/public/builder.js +67 -0
- package/public/sw.js +2 -1
- package/public/worksheet.css +244 -0
- package/public/worksheet.js +665 -0
- package/src/app.ts +9 -1
- package/src/builder/ai-builder.ts +3 -3
- package/src/builder/appwizard.ts +22 -6
- package/src/builder/arrange.ts +33 -12
- package/src/builder/code-editor.ts +1 -1
- package/src/builder/components.ts +1 -1
- package/src/builder/designer.ts +44 -17
- package/src/builder/errors.ts +65 -0
- package/src/builder/form-designer.ts +229 -0
- package/src/builder/locks.ts +6 -0
- package/src/builder/newapp.ts +2 -1
- package/src/builder/object-actions.ts +166 -0
- package/src/builder/object-browser.ts +111 -33
- package/src/builder/querybuilder.ts +1 -1
- package/src/builder/region-help.ts +2 -0
- package/src/builder/region-settings.ts +2 -0
- package/src/builder/reporter.ts +1 -1
- package/src/builder/routes.ts +7 -5
- package/src/builder/sampledata.ts +2 -2
- package/src/builder/scripts.ts +3 -3
- package/src/builder/search.ts +3 -3
- package/src/builder/shared.ts +2 -2
- package/src/builder/sql.ts +15 -73
- package/src/builder/table-editor.ts +219 -0
- package/src/builder/ui.ts +5 -3
- package/src/builder/unload.ts +8 -4
- package/src/builder/worksheet.ts +569 -0
- package/src/builder/workspaces.ts +1 -1
- package/src/cli/replace.ts +10 -0
- package/src/i18n/ar.ts +62 -0
- package/src/i18n/cs.ts +62 -0
- package/src/i18n/da.ts +62 -0
- package/src/i18n/de.ts +62 -0
- package/src/i18n/el.ts +62 -0
- package/src/i18n/es.ts +62 -0
- package/src/i18n/fi.ts +62 -0
- package/src/i18n/fr.ts +62 -0
- package/src/i18n/he.ts +62 -0
- package/src/i18n/it.ts +62 -0
- package/src/i18n/ja.ts +62 -0
- package/src/i18n/ko.ts +62 -0
- package/src/i18n/nb.ts +62 -0
- package/src/i18n/pl.ts +62 -0
- package/src/i18n/pt.ts +62 -0
- package/src/i18n/ru.ts +62 -0
- package/src/i18n/sv.ts +62 -0
- package/src/i18n/tr.ts +62 -0
- package/src/i18n/uk.ts +62 -0
- package/src/i18n/zh.ts +62 -0
- package/src/i18n.ts +124 -0
- package/src/metadata.ts +4 -2
- package/src/runtime/devbar.ts +1 -1
- package/src/runtime/facet-state.ts +10 -5
- package/src/runtime/facets.ts +29 -5
- package/src/runtime/form-designer.ts +676 -0
- package/src/runtime/grid.ts +15 -8
- package/src/runtime/items.ts +33 -21
- package/src/runtime/lists.ts +1 -1
- package/src/runtime/pdf.ts +2 -1
- package/src/runtime/regions.ts +4 -0
- package/src/runtime/render.ts +7 -3
- package/src/runtime/report.ts +2 -2
- package/src/runtime/routes.ts +3 -1
- package/src/runtime/smart-filters.ts +2 -1
- package/src/runtime/tasks.ts +6 -3
- package/src/sqlformat.ts +208 -0
- package/src/sqlplan.ts +133 -0
- package/src/sqlscript.ts +6 -2
- package/src/sqlworksheet.ts +278 -0
- package/src/tabledef.ts +226 -0
- package/src/unload.ts +14 -3
package/public/app.js
CHANGED
|
@@ -4,6 +4,23 @@
|
|
|
4
4
|
// inline scripts.
|
|
5
5
|
document.documentElement.classList.add('js');
|
|
6
6
|
|
|
7
|
+
// (sprint 86) Values a browser kept under pgkiln's old name (pgapex.*, pgapex-push:*) move to
|
|
8
|
+
// pgkiln.* / pgkiln-push:* once: the navigation drawer, the builder's tabs and panes, the region
|
|
9
|
+
// display selector's tab, a device's push notifications. A value already kept under the new name wins.
|
|
10
|
+
for (const store of ['localStorage', 'sessionStorage']) {
|
|
11
|
+
try {
|
|
12
|
+
const s = window[store];
|
|
13
|
+
for (const k of Object.keys(s)) {
|
|
14
|
+
if (!/^pgapex[.-]/.test(k)) continue;
|
|
15
|
+
const neu = `pgkiln${k.slice(6)}`;
|
|
16
|
+
if (s.getItem(neu) === null) s.setItem(neu, s.getItem(k));
|
|
17
|
+
s.removeItem(k);
|
|
18
|
+
}
|
|
19
|
+
} catch {
|
|
20
|
+
/* storage off (private mode, a blocked cookie setting): nothing kept, nothing to move */
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
7
24
|
// Push notifications (migration 074): the page of an app with notifications on carries
|
|
8
25
|
// the app's public key (data-push) and the user (data-push-user). Subscribing needs a
|
|
9
26
|
// click (My account → Notifications, or the dynamic action push_subscribe); the
|
|
@@ -3492,23 +3509,10 @@ function areaFilter(map, f) {
|
|
|
3492
3509
|
draw();
|
|
3493
3510
|
}
|
|
3494
3511
|
|
|
3495
|
-
// Date range: the end date can't be before the start date
|
|
3496
|
-
function range(box) {
|
|
3497
|
-
const [from, to] = box.querySelectorAll('input[type=date]');
|
|
3498
|
-
if (!from || !to) return;
|
|
3499
|
-
const limit = () => {
|
|
3500
|
-
to.min = from.value;
|
|
3501
|
-
from.max = to.value;
|
|
3502
|
-
};
|
|
3503
|
-
from.addEventListener('change', limit);
|
|
3504
|
-
to.addEventListener('change', limit);
|
|
3505
|
-
}
|
|
3506
|
-
|
|
3507
3512
|
function enhance(root) {
|
|
3508
3513
|
root.querySelectorAll('[data-richtext]').forEach(richText);
|
|
3509
3514
|
root.querySelectorAll('[data-markdown]').forEach(markdown);
|
|
3510
3515
|
root.querySelectorAll('[data-tags]').forEach(tags);
|
|
3511
|
-
root.querySelectorAll('[data-range]').forEach(range);
|
|
3512
3516
|
root.querySelectorAll('[data-reveal]').forEach((b) => (b.hidden = false));
|
|
3513
3517
|
}
|
|
3514
3518
|
enhance(document);
|
|
@@ -3931,11 +3935,42 @@ document.addEventListener('change', (e) => {
|
|
|
3931
3935
|
if (e.target.matches?.('select[data-go]')) e.target.form?.submit();
|
|
3932
3936
|
});
|
|
3933
3937
|
|
|
3934
|
-
// (sprint 76) A date
|
|
3935
|
-
// picker
|
|
3938
|
+
// (sprint 76, 84; APEX: the date picker) A date field in a date mask (items.ts dateField): its calendar button
|
|
3939
|
+
// opens pgkiln's date picker, a month of days under the field; the chosen date is written into the text field
|
|
3940
|
+
// in the mask. Keys: arrows (a day, a week), Page Up/Down (a month; with Shift a year), Home/End (the week),
|
|
3941
|
+
// Enter or Space (choose), Escape (close).
|
|
3936
3942
|
(() => {
|
|
3943
|
+
const meta = (() => { try { return JSON.parse(document.getElementById('pgkiln-meta')?.textContent || '{}'); } catch { return {}; } })();
|
|
3944
|
+
const t = (k, d) => (meta.texts && meta.texts[k]) || d;
|
|
3945
|
+
const lang = document.documentElement.lang || 'en';
|
|
3937
3946
|
const pad = (n) => String(n).padStart(2, '0');
|
|
3938
|
-
const
|
|
3947
|
+
const isoOf = (d) => `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())}`;
|
|
3948
|
+
const dateOf = (iso) => { const [y, m, d] = iso.split('-').map(Number); return new Date(Date.UTC(y, m - 1, d)); };
|
|
3949
|
+
const addDays = (iso, n) => { const d = dateOf(iso); d.setUTCDate(d.getUTCDate() + n); return isoOf(d); };
|
|
3950
|
+
const addMonths = (iso, n) => {
|
|
3951
|
+
const d = dateOf(iso);
|
|
3952
|
+
const day = d.getUTCDate();
|
|
3953
|
+
d.setUTCDate(1);
|
|
3954
|
+
d.setUTCMonth(d.getUTCMonth() + n);
|
|
3955
|
+
const last = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + 1, 0)).getUTCDate();
|
|
3956
|
+
d.setUTCDate(Math.min(day, last));
|
|
3957
|
+
return isoOf(d);
|
|
3958
|
+
};
|
|
3959
|
+
const today = () => { const n = new Date(); return `${n.getFullYear()}-${pad(n.getMonth() + 1)}-${pad(n.getDate())}`; };
|
|
3960
|
+
// the week starts on the language's first day (Monday where the browser doesn't say)
|
|
3961
|
+
const weekStart = (() => {
|
|
3962
|
+
try {
|
|
3963
|
+
const l = new Intl.Locale(lang);
|
|
3964
|
+
const w = (l.getWeekInfo && l.getWeekInfo()) || l.weekInfo;
|
|
3965
|
+
if (w && w.firstDay) return w.firstDay % 7;
|
|
3966
|
+
} catch {}
|
|
3967
|
+
return /^en-US$|^(ja|ko|zh|he|ar|pt-BR)/i.test(lang) ? 0 : 1;
|
|
3968
|
+
})();
|
|
3969
|
+
const fmt = (o) => { try { return new Intl.DateTimeFormat(lang, { ...o, timeZone: 'UTC' }); } catch { return new Intl.DateTimeFormat('en', { ...o, timeZone: 'UTC' }); } };
|
|
3970
|
+
const dayShort = fmt({ weekday: 'short' }), dayLong = fmt({ weekday: 'long' }), full = fmt({ dateStyle: 'full' }), title = fmt({ month: 'long', year: 'numeric' });
|
|
3971
|
+
|
|
3972
|
+
const TOKEN = /"[^"]*"|YYYY|YY|MONTH|MON|MM|DD|DAY|DY/gi;
|
|
3973
|
+
const monthsOf = (field) => { try { return JSON.parse(field.dataset.months || '{}'); } catch { return {}; } };
|
|
3939
3974
|
const format = (iso, mask, months) => {
|
|
3940
3975
|
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(iso);
|
|
3941
3976
|
if (!m) return '';
|
|
@@ -3948,31 +3983,205 @@ document.addEventListener('change', (e) => {
|
|
|
3948
3983
|
if (up === 'YY') return pad(y % 100);
|
|
3949
3984
|
if (up === 'MM') return pad(mo);
|
|
3950
3985
|
if (up === 'DD') return pad(d);
|
|
3951
|
-
if (up === 'MONTH') return cased(months.long[mo - 1]);
|
|
3952
|
-
if (up === 'MON') return cased(months.short[mo - 1]);
|
|
3986
|
+
if (up === 'MONTH') return cased((months.long || [])[mo - 1] || '');
|
|
3987
|
+
if (up === 'MON') return cased((months.short || [])[mo - 1] || '');
|
|
3988
|
+
if (up === 'DAY') return cased(dayLong.format(dateOf(iso)));
|
|
3989
|
+
if (up === 'DY') return cased(dayShort.format(dateOf(iso)));
|
|
3953
3990
|
return tok;
|
|
3954
3991
|
});
|
|
3955
3992
|
};
|
|
3993
|
+
// the text read back in the mask (as format.ts parseDateMask does), or as YYYY-MM-DD; null when it isn't a date
|
|
3994
|
+
const parse = (text, mask, months) => {
|
|
3995
|
+
const v = (text || '').trim();
|
|
3996
|
+
let y, mo, d;
|
|
3997
|
+
const iso = /^(\d{4})-(\d{2})-(\d{2})$/.exec(v);
|
|
3998
|
+
if (iso) [y, mo, d] = iso.slice(1).map(Number);
|
|
3999
|
+
else {
|
|
4000
|
+
const esc = (x) => x.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
4001
|
+
const names = (list) => `(${(list || []).map(esc).join('|') || '\\p{L}+'})`;
|
|
4002
|
+
const groups = [];
|
|
4003
|
+
let last = 0, re = '';
|
|
4004
|
+
for (const m of mask.matchAll(TOKEN)) {
|
|
4005
|
+
re += esc(mask.slice(last, m.index));
|
|
4006
|
+
last = m.index + m[0].length;
|
|
4007
|
+
if (m[0].startsWith('"')) { re += esc(m[0].slice(1, -1)); continue; }
|
|
4008
|
+
const up = m[0].toUpperCase();
|
|
4009
|
+
re += { YYYY: '(\\d{4})', YY: '(\\d{2})', MM: '(\\d{1,2})', DD: '(\\d{1,2})', MONTH: names(months.long), MON: names(months.short), DAY: '(\\p{L}+)', DY: '(\\p{L}+)' }[up];
|
|
4010
|
+
groups.push(up);
|
|
4011
|
+
}
|
|
4012
|
+
re += esc(mask.slice(last));
|
|
4013
|
+
const m = new RegExp(`^${re}$`, 'iu').exec(v);
|
|
4014
|
+
if (!m) return null;
|
|
4015
|
+
groups.forEach((g, i) => {
|
|
4016
|
+
const x = m[i + 1];
|
|
4017
|
+
if (g === 'YYYY') y = Number(x);
|
|
4018
|
+
else if (g === 'YY') y = 2000 + Number(x);
|
|
4019
|
+
else if (g === 'MM') mo = Number(x);
|
|
4020
|
+
else if (g === 'DD') d = Number(x);
|
|
4021
|
+
else if (g === 'MONTH' || g === 'MON') mo = ((g === 'MONTH' ? months.long : months.short) || []).findIndex((n) => n.toLowerCase() === x.toLowerCase()) + 1;
|
|
4022
|
+
});
|
|
4023
|
+
}
|
|
4024
|
+
if (!y || !mo || !d) return null;
|
|
4025
|
+
const out = `${String(y).padStart(4, '0')}-${pad(mo)}-${pad(d)}`;
|
|
4026
|
+
return isoOf(dateOf(out)) === out ? out : null;
|
|
4027
|
+
};
|
|
4028
|
+
|
|
4029
|
+
let open = null; // { box, field, btn, focus, view }
|
|
4030
|
+
function close(back) {
|
|
4031
|
+
if (!open) return;
|
|
4032
|
+
const { box, btn } = open;
|
|
4033
|
+
open = null;
|
|
4034
|
+
box.remove();
|
|
4035
|
+
btn.setAttribute('aria-expanded', 'false');
|
|
4036
|
+
if (back) btn.focus();
|
|
4037
|
+
}
|
|
4038
|
+
function choose(iso) {
|
|
4039
|
+
const { field } = open;
|
|
4040
|
+
field.value = iso ? format(iso, field.dataset.dateMask || 'YYYY-MM-DD', monthsOf(field)) : '';
|
|
4041
|
+
close(false);
|
|
4042
|
+
field.dispatchEvent(new Event('input', { bubbles: true }));
|
|
4043
|
+
field.dispatchEvent(new Event('change', { bubbles: true }));
|
|
4044
|
+
field.focus();
|
|
4045
|
+
}
|
|
4046
|
+
// a date range's to is on or after its from (and the from on or before the to)
|
|
4047
|
+
const allowed = (iso) => {
|
|
4048
|
+
const { field } = open;
|
|
4049
|
+
if (!field.closest('.date-range')) return true;
|
|
4050
|
+
const isTo = field.id.endsWith('_TO');
|
|
4051
|
+
const other = document.getElementById(isTo ? field.id.slice(0, -3) : `${field.id}_TO`);
|
|
4052
|
+
const bound = other ? parse(other.value, other.dataset.dateMask || 'YYYY-MM-DD', monthsOf(other)) : null;
|
|
4053
|
+
return !bound || (isTo ? iso >= bound : iso <= bound);
|
|
4054
|
+
};
|
|
4055
|
+
function render() {
|
|
4056
|
+
const { box, field } = open;
|
|
4057
|
+
const sel = parse(field.value, field.dataset.dateMask || 'YYYY-MM-DD', monthsOf(field));
|
|
4058
|
+
const now = today();
|
|
4059
|
+
const view = open.view;
|
|
4060
|
+
const first = dateOf(`${view.slice(0, 7)}-01`);
|
|
4061
|
+
const start = addDays(isoOf(first), -((first.getUTCDay() - weekStart + 7) % 7));
|
|
4062
|
+
box.textContent = '';
|
|
4063
|
+
const head = document.createElement('div');
|
|
4064
|
+
head.className = 'date-picker-head';
|
|
4065
|
+
const nav = (label, n, glyph) => {
|
|
4066
|
+
const b = document.createElement('button');
|
|
4067
|
+
b.type = 'button';
|
|
4068
|
+
b.className = 'btn btn-sm';
|
|
4069
|
+
b.setAttribute('aria-label', label);
|
|
4070
|
+
b.textContent = glyph;
|
|
4071
|
+
b.addEventListener('click', () => { open.focus = addMonths(open.focus, n); open.view = open.focus; render(); });
|
|
4072
|
+
return b;
|
|
4073
|
+
};
|
|
4074
|
+
const caption = document.createElement('div');
|
|
4075
|
+
caption.className = 'date-picker-title';
|
|
4076
|
+
caption.setAttribute('aria-live', 'polite');
|
|
4077
|
+
caption.id = `${field.id}_dp_title`;
|
|
4078
|
+
caption.textContent = title.format(first);
|
|
4079
|
+
head.append(nav(t('calendar.previous', 'Previous month'), -1, '‹'), caption, nav(t('calendar.next', 'Next month'), 1, '›'));
|
|
4080
|
+
const table = document.createElement('table');
|
|
4081
|
+
table.className = 'date-picker-grid';
|
|
4082
|
+
table.setAttribute('role', 'grid');
|
|
4083
|
+
table.setAttribute('aria-labelledby', caption.id);
|
|
4084
|
+
const hr = table.createTHead().insertRow();
|
|
4085
|
+
for (let i = 0; i < 7; i++) {
|
|
4086
|
+
const th = document.createElement('th');
|
|
4087
|
+
const day = addDays(start, i);
|
|
4088
|
+
th.scope = 'col';
|
|
4089
|
+
th.abbr = dayLong.format(dateOf(day));
|
|
4090
|
+
th.textContent = dayShort.format(dateOf(day));
|
|
4091
|
+
hr.append(th);
|
|
4092
|
+
}
|
|
4093
|
+
const body = table.createTBody();
|
|
4094
|
+
for (let w = 0; w < 6; w++) {
|
|
4095
|
+
const tr = body.insertRow();
|
|
4096
|
+
for (let i = 0; i < 7; i++) {
|
|
4097
|
+
const iso = addDays(start, w * 7 + i);
|
|
4098
|
+
const td = tr.insertCell();
|
|
4099
|
+
const b = document.createElement('button');
|
|
4100
|
+
b.type = 'button';
|
|
4101
|
+
b.dataset.iso = iso;
|
|
4102
|
+
b.textContent = String(Number(iso.slice(8)));
|
|
4103
|
+
b.setAttribute('aria-label', full.format(dateOf(iso)));
|
|
4104
|
+
b.tabIndex = iso === open.focus ? 0 : -1;
|
|
4105
|
+
if (iso.slice(0, 7) !== view.slice(0, 7)) b.classList.add('date-other');
|
|
4106
|
+
if (iso === now) { b.classList.add('date-today'); b.setAttribute('aria-current', 'date'); }
|
|
4107
|
+
if (iso === sel) { b.classList.add('date-selected'); b.setAttribute('aria-pressed', 'true'); }
|
|
4108
|
+
if (!allowed(iso)) b.disabled = true;
|
|
4109
|
+
td.append(b);
|
|
4110
|
+
}
|
|
4111
|
+
}
|
|
4112
|
+
const foot = document.createElement('div');
|
|
4113
|
+
foot.className = 'date-picker-foot';
|
|
4114
|
+
const act = (label, fn) => { const b = document.createElement('button'); b.type = 'button'; b.className = 'btn btn-sm'; b.textContent = label; b.addEventListener('click', fn); return b; };
|
|
4115
|
+
foot.append(act(t('calendar.today', 'Today'), () => (allowed(now) ? choose(now) : null)), act(t('facets.clear_short', 'Clear'), () => choose('')));
|
|
4116
|
+
box.append(head, table, foot);
|
|
4117
|
+
box.querySelector('button[tabindex="0"]')?.focus({ preventScroll: true });
|
|
4118
|
+
}
|
|
4119
|
+
// fixed under the field (above it when there is no room), so a scrolling table or grid cell doesn't clip it
|
|
4120
|
+
function place() {
|
|
4121
|
+
if (!open) return;
|
|
4122
|
+
const { box, field } = open;
|
|
4123
|
+
const r = field.getBoundingClientRect();
|
|
4124
|
+
const h = box.offsetHeight, w = box.offsetWidth;
|
|
4125
|
+
box.style.position = 'fixed';
|
|
4126
|
+
box.style.top = `${Math.round(r.bottom + 4 + h > window.innerHeight - 8 && r.top - h - 4 > 8 ? r.top - h - 4 : r.bottom + 4)}px`;
|
|
4127
|
+
box.style.left = `${Math.round(Math.max(8, Math.min(r.left, window.innerWidth - w - 8)))}px`;
|
|
4128
|
+
}
|
|
4129
|
+
window.addEventListener('scroll', place, { passive: true, capture: true });
|
|
4130
|
+
window.addEventListener('resize', place);
|
|
4131
|
+
function show(field, btn) {
|
|
4132
|
+
close(false);
|
|
4133
|
+
const box = document.createElement('div');
|
|
4134
|
+
box.className = 'date-picker';
|
|
4135
|
+
box.setAttribute('role', 'dialog');
|
|
4136
|
+
box.setAttribute('aria-label', t('item.choose_date', 'Choose a date'));
|
|
4137
|
+
const sel = parse(field.value, field.dataset.dateMask || 'YYYY-MM-DD', monthsOf(field));
|
|
4138
|
+
open = { box, field, btn, focus: sel || today(), view: sel || today() };
|
|
4139
|
+
btn.setAttribute('aria-expanded', 'true');
|
|
4140
|
+
(field.closest('.date-field') || field.parentElement).append(box);
|
|
4141
|
+
render();
|
|
4142
|
+
place();
|
|
4143
|
+
box.addEventListener('keydown', (e) => {
|
|
4144
|
+
const day = e.target.dataset?.iso;
|
|
4145
|
+
if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); close(true); return; }
|
|
4146
|
+
if (!day) return;
|
|
4147
|
+
const step = { ArrowLeft: -1, ArrowRight: 1, ArrowUp: -7, ArrowDown: 7 }[e.key];
|
|
4148
|
+
let next = null;
|
|
4149
|
+
if (step) next = addDays(day, step);
|
|
4150
|
+
else if (e.key === 'PageUp' || e.key === 'PageDown') next = addMonths(day, (e.key === 'PageUp' ? -1 : 1) * (e.shiftKey ? 12 : 1));
|
|
4151
|
+
else if (e.key === 'Home') next = addDays(day, -((dateOf(day).getUTCDay() - weekStart + 7) % 7));
|
|
4152
|
+
else if (e.key === 'End') next = addDays(day, 6 - ((dateOf(day).getUTCDay() - weekStart + 7) % 7));
|
|
4153
|
+
else if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); if (allowed(day)) choose(day); return; }
|
|
4154
|
+
if (!next) return;
|
|
4155
|
+
e.preventDefault();
|
|
4156
|
+
open.focus = next;
|
|
4157
|
+
if (next.slice(0, 7) !== open.view.slice(0, 7)) { open.view = next; render(); }
|
|
4158
|
+
else {
|
|
4159
|
+
box.querySelectorAll('button[data-iso]').forEach((b) => (b.tabIndex = b.dataset.iso === next ? 0 : -1));
|
|
4160
|
+
box.querySelector(`button[data-iso="${next}"]`)?.focus({ preventScroll: true });
|
|
4161
|
+
}
|
|
4162
|
+
});
|
|
4163
|
+
box.addEventListener('click', (e) => {
|
|
4164
|
+
const b = e.target.closest('button[data-iso]');
|
|
4165
|
+
if (b && !b.disabled) choose(b.dataset.iso);
|
|
4166
|
+
});
|
|
4167
|
+
}
|
|
3956
4168
|
document.addEventListener('click', (e) => {
|
|
3957
4169
|
const btn = e.target.closest?.('[data-date-pick]');
|
|
3958
|
-
if (
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
4170
|
+
if (btn) {
|
|
4171
|
+
const field = document.getElementById(btn.dataset.datePick);
|
|
4172
|
+
if (!field) return;
|
|
4173
|
+
e.preventDefault();
|
|
4174
|
+
if (open && open.btn === btn) close(true);
|
|
4175
|
+
else show(field, btn);
|
|
4176
|
+
return;
|
|
3965
4177
|
}
|
|
4178
|
+
if (open && !e.target.closest?.('.date-picker')) close(false);
|
|
3966
4179
|
});
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
if (
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
try { months = JSON.parse(text.dataset.months || '{}'); } catch {}
|
|
3974
|
-
text.value = format(native.value, text.dataset.dateMask || 'YYYY-MM-DD', months);
|
|
3975
|
-
text.dispatchEvent(new Event('change', { bubbles: true }));
|
|
3976
|
-
text.focus();
|
|
4180
|
+
// Alt+Down in the field opens the picker (as a native date field does)
|
|
4181
|
+
document.addEventListener('keydown', (e) => {
|
|
4182
|
+
if (e.altKey && e.key === 'ArrowDown' && e.target.matches?.('input[data-date-mask]')) {
|
|
4183
|
+
const btn = e.target.parentElement?.querySelector('[data-date-pick]');
|
|
4184
|
+
if (btn) { e.preventDefault(); show(e.target, btn); }
|
|
4185
|
+
}
|
|
3977
4186
|
});
|
|
3978
4187
|
})();
|
package/public/builder.css
CHANGED
|
@@ -368,7 +368,7 @@ a.pd-node:hover { background: var(--surface-3); }
|
|
|
368
368
|
.js .pd-gal { cursor: grab; }
|
|
369
369
|
.pd-gal:hover { background: var(--surface-3); }
|
|
370
370
|
.pd-gal .icon { width: 22px; height: 22px; color: var(--muted); stroke-width: 1.5; }
|
|
371
|
-
.pd-gal span { min-width: 0; overflow
|
|
371
|
+
.pd-gal span { min-width: 0; overflow-wrap: anywhere; line-height: 1.2; }
|
|
372
372
|
|
|
373
373
|
/* ------------------------------------------------------------ help and search tabs */
|
|
374
374
|
.pd-help, .pd-search { padding: 12px 16px; max-width: 60rem; }
|
|
@@ -1012,3 +1012,23 @@ details.prop-group-more[open] > summary { margin-bottom: 8px; }
|
|
|
1012
1012
|
.pd-goto-no input { width: 5.5em; font-variant-numeric: tabular-nums; }
|
|
1013
1013
|
/* on narrower screens the previous / next arrows and the page list do it */
|
|
1014
1014
|
@media (max-width: 1279px) { .pd-goto-no { display: none; } }
|
|
1015
|
+
|
|
1016
|
+
/* (sprint 89, SQL-4) the Object Browser: a table's tabs on one row with More, and the Columns toolbar */
|
|
1017
|
+
.ob-tabs { flex-wrap: nowrap; align-items: stretch; }
|
|
1018
|
+
.ob-tabs > a { white-space: nowrap; }
|
|
1019
|
+
.ob-more { position: relative; }
|
|
1020
|
+
.ob-more > summary { display: flex; align-items: center; gap: 0.25rem; height: 100%; padding: 0.5rem 0.85rem; color: var(--muted); cursor: pointer; white-space: nowrap; }
|
|
1021
|
+
.ob-more > summary:hover, .ob-more[open] > summary { color: var(--text); }
|
|
1022
|
+
.ob-more .menu-panel { right: 0; left: auto; min-width: 12rem; }
|
|
1023
|
+
.ob-more .icon { width: 0.9rem; height: 0.9rem; }
|
|
1024
|
+
.ob-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; margin-bottom: 0.75rem; }
|
|
1025
|
+
.ob-tool { position: relative; }
|
|
1026
|
+
.ob-tool > summary { list-style: none; }
|
|
1027
|
+
.ob-tool .menu-panel { left: 0; min-width: 18rem; padding: 0.75rem; }
|
|
1028
|
+
.ob-tool .ob-action { flex-direction: column; align-items: stretch; }
|
|
1029
|
+
.ob-tool .ob-action input:not([type=checkbox]), .ob-tool .ob-action select { width: 100%; }
|
|
1030
|
+
.ob-toolbar-note { font-size: 0.85rem; }
|
|
1031
|
+
@media (max-width: 640px) { .ob-tabs { overflow-x: auto; } .ob-tool .menu-panel { min-width: min(18rem, 90vw); } }
|
|
1032
|
+
|
|
1033
|
+
/* (sprint 90, PD-4) a commented-out component in the tree (APEX: Comment Out): struck through and muted */
|
|
1034
|
+
.pd-node[data-commented] .pd-label { text-decoration: line-through; color: var(--muted); }
|
package/public/builder.js
CHANGED
|
@@ -270,6 +270,11 @@
|
|
|
270
270
|
list.push(['Duplicate', () => submit(`${base}/layout/duplicate`, { kind, id })], ['Copy to other page…', copyDialog]);
|
|
271
271
|
if (kind !== 'region') list.push(['Create dynamic action', go('new=dynamic_action')]);
|
|
272
272
|
if (node.classList.contains('has-kids')) list.push(null, ['Expand all below', () => setAll(node, true)], ['Collapse all below', () => setAll(node, false)]);
|
|
273
|
+
// (sprint 90; APEX: Comment Out) leave a component out of the page without deleting it (build option COMMENTED_OUT)
|
|
274
|
+
if (['region', 'item', 'button', 'dynamic_action', 'validation', 'process', 'computation', 'branch'].includes(kind))
|
|
275
|
+
list.push(null, node.hasAttribute('data-commented')
|
|
276
|
+
? ['Uncomment', () => submit(`${base}/layout/comment`, { kind, id, on: 'false' })]
|
|
277
|
+
: ['Comment out', () => submit(`${base}/layout/comment`, { kind, id, on: 'true' })]);
|
|
273
278
|
list.push(null, ['Delete', () => submit(`${base}/c/${kind}/${id}/delete`, {}, `Delete ${kind} ${label}?`), 'danger']);
|
|
274
279
|
return list;
|
|
275
280
|
};
|
|
@@ -1167,3 +1172,65 @@ document.querySelectorAll('input[data-filter-tree]').forEach((el) => {
|
|
|
1167
1172
|
});
|
|
1168
1173
|
});
|
|
1169
1174
|
});
|
|
1175
|
+
|
|
1176
|
+
// (sprint 87) The create application wizard:
|
|
1177
|
+
// - an alias follows its name (data-derive-from) until the developer types in it, like aliasFor() on the server;
|
|
1178
|
+
// - choosing a schema under Data reloads the Pages list (and the proposed name and alias, while untouched)
|
|
1179
|
+
// without leaving the page; without JavaScript, "Show its tables" does the same with a reload;
|
|
1180
|
+
// - First user and Password are hidden while Authentication is None (the server ignores them then).
|
|
1181
|
+
(() => {
|
|
1182
|
+
const slug = (v) => {
|
|
1183
|
+
let a = v.normalize('NFKD').replace(/\p{M}/gu, '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 40);
|
|
1184
|
+
if (a && !/^[a-z]/.test(a)) a = `app-${a}`;
|
|
1185
|
+
return a;
|
|
1186
|
+
};
|
|
1187
|
+
document.querySelectorAll('input[data-derive-from]').forEach((alias) => {
|
|
1188
|
+
const name = document.getElementById(alias.dataset.deriveFrom);
|
|
1189
|
+
if (!name) return;
|
|
1190
|
+
alias.addEventListener('input', () => alias.setAttribute('data-edited', ''));
|
|
1191
|
+
name.addEventListener('input', () => {
|
|
1192
|
+
if (!alias.hasAttribute('data-edited')) alias.value = slug(name.value);
|
|
1193
|
+
});
|
|
1194
|
+
});
|
|
1195
|
+
|
|
1196
|
+
document.querySelectorAll('select[name="authentication"]').forEach((auth) => {
|
|
1197
|
+
const form = auth.form;
|
|
1198
|
+
const fields = ['admin_user', 'admin_password'].map((n) => form?.elements.namedItem(n)?.closest?.('.field')).filter(Boolean);
|
|
1199
|
+
if (!fields.length) return;
|
|
1200
|
+
const sync = () => fields.forEach((f) => (f.hidden = auth.value === 'none'));
|
|
1201
|
+
auth.addEventListener('change', sync);
|
|
1202
|
+
sync();
|
|
1203
|
+
});
|
|
1204
|
+
|
|
1205
|
+
const picker = document.querySelector('form.create-schema select[name="schema"]');
|
|
1206
|
+
const pages = document.getElementById('aw-pages');
|
|
1207
|
+
const hidden = document.getElementById('aw-schema-fields');
|
|
1208
|
+
if (!picker || !pages || !hidden) return;
|
|
1209
|
+
const name = document.getElementById('f_name');
|
|
1210
|
+
let nameTouched = false;
|
|
1211
|
+
name?.addEventListener('input', () => (nameTouched = true));
|
|
1212
|
+
picker.addEventListener('change', async () => {
|
|
1213
|
+
const url = `${picker.form.action}?schema=${encodeURIComponent(picker.value)}`;
|
|
1214
|
+
pages.setAttribute('aria-busy', 'true');
|
|
1215
|
+
try {
|
|
1216
|
+
const res = await fetch(url, { headers: { accept: 'text/html' }, credentials: 'same-origin' });
|
|
1217
|
+
if (!res.ok) throw new Error(String(res.status));
|
|
1218
|
+
const doc = new DOMParser().parseFromString(await res.text(), 'text/html');
|
|
1219
|
+
const nextPages = doc.getElementById('aw-pages');
|
|
1220
|
+
const nextHidden = doc.getElementById('aw-schema-fields');
|
|
1221
|
+
if (!nextPages || !nextHidden) throw new Error('unexpected page');
|
|
1222
|
+
pages.replaceWith(nextPages);
|
|
1223
|
+
hidden.replaceWith(nextHidden);
|
|
1224
|
+
// the proposals for the new schema, unless the developer already typed a name or alias
|
|
1225
|
+
const alias = document.querySelector('input[data-derive-from]');
|
|
1226
|
+
if (!nameTouched && name) name.value = doc.getElementById('f_name')?.value ?? name.value;
|
|
1227
|
+
if (alias && !alias.hasAttribute('data-edited')) alias.value = doc.getElementById('f_alias')?.value ?? alias.value;
|
|
1228
|
+
history.replaceState(null, '', url);
|
|
1229
|
+
document.getElementById('aw-pages')?.removeAttribute('aria-busy');
|
|
1230
|
+
} catch {
|
|
1231
|
+
picker.form.submit();
|
|
1232
|
+
}
|
|
1233
|
+
});
|
|
1234
|
+
// the button is for pages without JavaScript
|
|
1235
|
+
picker.form.querySelector('button')?.setAttribute('hidden', '');
|
|
1236
|
+
})();
|
package/public/sw.js
CHANGED
|
@@ -15,11 +15,12 @@ self.addEventListener('install', (event) => {
|
|
|
15
15
|
event.waitUntil(caches.open(STATIC).then((c) => c.addAll(SHELL)).then(() => self.skipWaiting()));
|
|
16
16
|
});
|
|
17
17
|
|
|
18
|
+
// older versions' static files, and (sprint 86) the caches of pgkiln's old name (pgapex-*)
|
|
18
19
|
self.addEventListener('activate', (event) => {
|
|
19
20
|
event.waitUntil(
|
|
20
21
|
caches
|
|
21
22
|
.keys()
|
|
22
|
-
.then((keys) => Promise.all(keys.filter((k) => k.startsWith('pgkiln-static-') && k !== STATIC).map((k) => caches.delete(k))))
|
|
23
|
+
.then((keys) => Promise.all(keys.filter((k) => (k.startsWith('pgkiln-static-') && k !== STATIC) || k.startsWith('pgapex-')).map((k) => caches.delete(k))))
|
|
23
24
|
.then(() => self.clients.claim()),
|
|
24
25
|
);
|
|
25
26
|
});
|