pgkiln 0.53.0 → 0.55.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/SECURITY.md +2 -0
- package/db/migrations/127_builder_language.sql +16 -0
- package/docs/apex-feature-parity.md +7 -7
- package/docs/guide/03-builder.md +62 -8
- package/docs/guide/12-development.md +50 -3
- package/examples/gallery/README.md +37 -6
- package/examples/gallery/gallery.json +23 -1
- package/examples/gallery/samples/sample-calendar.sql +172 -0
- package/examples/gallery/samples/sample-cards.sql +157 -0
- package/examples/gallery/samples/sample-charts.sql +265 -0
- package/examples/gallery/samples/sample-dynamic-actions.sql +177 -0
- package/examples/gallery/samples/sample-grids.sql +151 -0
- package/examples/gallery/samples/sample-reporting.sql +151 -0
- package/examples/gallery/samples/ux-patterns.sql +295 -0
- package/examples/gallery/utilities/announcements.json +24 -0
- package/examples/gallery/utilities/lookup-lists.json +28 -0
- package/examples/gallery/utilities/release-notes.json +41 -0
- package/package.json +1 -1
- package/public/app.css +2 -0
- package/public/builder.css +39 -1
- package/public/worksheet.css +3 -2
- package/src/builder/advisor.ts +1 -1
- package/src/builder/ai-agents.ts +5 -5
- package/src/builder/ai-builder.ts +3 -3
- package/src/builder/api.ts +1 -1
- package/src/builder/autorest.ts +2 -2
- package/src/builder/availability.ts +1 -1
- package/src/builder/backups.ts +1 -1
- package/src/builder/changelog.ts +1 -1
- package/src/builder/data-modeler.ts +4 -4
- package/src/builder/data-security.ts +1 -1
- package/src/builder/dataload.ts +1 -1
- package/src/builder/datapump.ts +1 -1
- package/src/builder/ddl-tools.ts +2 -2
- package/src/builder/designer.ts +134 -124
- package/src/builder/diagnostics.ts +2 -2
- package/src/builder/errors.ts +10 -12
- package/src/builder/feedback.ts +1 -1
- package/src/builder/gallery.ts +94 -47
- package/src/builder/globalization.ts +1 -1
- package/src/builder/home.ts +59 -57
- package/src/builder/i18n/de.ts +675 -0
- package/src/builder/i18n/en.ts +678 -0
- package/src/builder/i18n/es.ts +675 -0
- package/src/builder/i18n/fr.ts +675 -0
- package/src/builder/i18n/nl.ts +675 -0
- package/src/builder/i18n/pt.ts +675 -0
- package/src/builder/i18n.ts +116 -0
- package/src/builder/language.ts +74 -0
- package/src/builder/launchpad.ts +13 -8
- package/src/builder/object-browser.ts +1 -1
- package/src/builder/plugins.ts +1 -1
- package/src/builder/querybuilder.ts +1 -1
- package/src/builder/quicksql.ts +1 -1
- package/src/builder/rest-modules.ts +2 -2
- package/src/builder/routes.ts +88 -66
- package/src/builder/sampledata.ts +1 -1
- package/src/builder/scheduling.ts +1 -1
- package/src/builder/scripts.ts +1 -1
- package/src/builder/search.ts +1 -1
- package/src/builder/shared.ts +5 -5
- package/src/builder/sql-charts.ts +2 -2
- package/src/builder/sql-monitor.ts +2 -2
- package/src/builder/sql-tools.ts +6 -0
- package/src/builder/sql-utilities.ts +3 -3
- package/src/builder/sql.ts +24 -22
- package/src/builder/static-files.ts +1 -1
- package/src/builder/subscriptions.ts +1 -1
- package/src/builder/supporting.ts +1 -1
- package/src/builder/table-editor.ts +1 -1
- package/src/builder/team.ts +7 -5
- package/src/builder/themeroller.ts +1 -1
- package/src/builder/top-sql.ts +1 -1
- package/src/builder/ui.ts +87 -77
- package/src/builder/unload.ts +1 -1
- package/src/builder/wizards.ts +1 -1
- package/src/builder/workflow-migrate.ts +1 -1
- package/src/builder/workingcopies.ts +2 -2
- package/src/builder/worksheet.ts +1 -1
- package/src/builder/workspace-home.ts +62 -59
- package/src/builder/workspaces.ts +2 -1
- package/src/runtime/charts.ts +4 -1
- package/src/runtime/regions.ts +8 -5
package/src/builder/designer.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { icon } from '../icons.ts';
|
|
|
6
6
|
import { COMPONENTS } from './components.ts';
|
|
7
7
|
import { back, BASE, bicon, csrf, designerUrl, developer, flash, input, select, send, shell, type Req } from './ui.ts';
|
|
8
8
|
import { wantsHtml } from './errors.ts';
|
|
9
|
+
import { tr } from './i18n.ts';
|
|
9
10
|
import type { Session } from '../session.ts';
|
|
10
11
|
import { buildOptionChoices, componentForm, lookups, saveComponent } from './forms.ts';
|
|
11
12
|
import { regionConfigHelp } from './region-help.ts';
|
|
@@ -51,7 +52,7 @@ export async function designerRoutes(app: FastifyInstance) {
|
|
|
51
52
|
const no = /^\d{1,9}$/.test(req.query.page ?? '') ? Number(req.query.page) : -1;
|
|
52
53
|
const p = appId === null ? null : await owner.one('select id from meta.page where app_id = $1 and page_no = $2', [appId, no]);
|
|
53
54
|
if (p) return reply.redirect(designerUrl(appId!, no), 303);
|
|
54
|
-
flash(s,
|
|
55
|
+
flash(s, tr(s)('pd.no_page', { page: req.query.page ?? '' }), 'error');
|
|
55
56
|
return back(reply, s, appId === null ? BASE : `${BASE}/apps/${appId}`);
|
|
56
57
|
});
|
|
57
58
|
|
|
@@ -93,6 +94,12 @@ export async function designerRoutes(app: FastifyInstance) {
|
|
|
93
94
|
]).then((r) => r.map((x) => x.rows)),
|
|
94
95
|
]);
|
|
95
96
|
const sel = req.query.c ?? '';
|
|
97
|
+
// (sprint 121) the toolbar, menus, tree headings, pane titles and gallery in the builder language; the property
|
|
98
|
+
// editor's fields and their help stay English (docs/guide/12-development.md)
|
|
99
|
+
const tx = tr(s);
|
|
100
|
+
const kindLabel = (k: string) => tx.dyn(`comp.${k}`, COMPONENTS[k].label);
|
|
101
|
+
const posLabel = (pos: string) => tx.dyn(`pos.${pos}`, POSITION_LABELS[pos] ?? pos);
|
|
102
|
+
const slotLabel = (sl: string) => tx.dyn(`slot.${sl}`, SLOT_LABELS[sl] ?? sl);
|
|
96
103
|
const newKind = PAGE_KINDS.includes(req.query.new ?? '') ? req.query.new : '';
|
|
97
104
|
const [kind, cid] = sel.split('-');
|
|
98
105
|
const url = (q: string) => designerUrl(p.app_id, p.page_no, q);
|
|
@@ -100,7 +107,7 @@ export async function designerRoutes(app: FastifyInstance) {
|
|
|
100
107
|
const post = `${BASE}/pages/${p.id}`;
|
|
101
108
|
const isSel = (key: string) => sel === key || (key === 'page' && !sel && !newKind);
|
|
102
109
|
const cur = (key: string) => (isSel(key) ? raw(' aria-current="true"') : '');
|
|
103
|
-
const badges = (r: any) => html`${r.authz ? html`<span class="pd-tag" title="
|
|
110
|
+
const badges = (r: any) => html`${r.authz ? html`<span class="pd-tag" title="${tx('pd.badge.authz', { name: r.authz })}">${icon('shield')}<span class="sr-only">${tx('pd.badge.authz', { name: r.authz })}</span></span>` : ''}${r.condition || r.readonly_condition || r.condition_type ? html`<span class="pd-tag" title="${tx('pd.badge.condition')}">${icon('filter')}<span class="sr-only">${tx('pd.badge.condition')}</span></span>` : ''}${r.build_option ? html`<span class="pd-tag" title="${tx('pd.badge.build_option', { name: r.build_option })}">${icon('settings')}<span class="sr-only">${tx('pd.badge.build_option', { name: r.build_option })}</span></span>` : ''}`;
|
|
104
111
|
const regionTitle = (r: any) => r.title ?? `(${r.type})`;
|
|
105
112
|
|
|
106
113
|
// ------------------------------------------------------------ left: component tree
|
|
@@ -119,8 +126,8 @@ export async function designerRoutes(app: FastifyInstance) {
|
|
|
119
126
|
const buttons = rows.button.filter((b) => b.region_id === r.id);
|
|
120
127
|
// (sprint 77, APEX: region slots) buttons by their position in the region
|
|
121
128
|
const bySlot = ['region', ...TOP_SLOTS, ...BOTTOM_SLOTS].map((sl) => [sl, buttons.filter((b) => (b.slot ?? 'region') === sl)] as const).filter(([, list]) => list.length);
|
|
122
|
-
const kids = [...(items.length ? [folder('
|
|
123
|
-
...bySlot.map(([sl, list]) => folder(sl === 'region' ? '
|
|
129
|
+
const kids = [...(items.length ? [folder(tx('pd.tree.items'), items.map(itemLeaf))] : []),
|
|
130
|
+
...bySlot.map(([sl, list]) => folder(sl === 'region' ? tx('pd.tree.buttons') : tx('pd.tree.buttons_at', { slot: slotLabel(sl) }), list.map(buttonLeaf)))];
|
|
124
131
|
return html`<li><a class="pd-node" href="${url(`c=region-${r.id}`)}"${cur(`region-${r.id}`)} data-kind="region" data-id="${r.id}" data-position="${r.position ?? ''}" data-next="${next?.id ?? ''}"${commented(r)}>${bicon(regionIcon(r.type))}<span class="pd-label">${regionTitle(r)}</span>${badges(r)}<span class="pd-note">${r.type}</span></a>${kids.length ? html`<ul>${kids}</ul>` : ''}</li>`;
|
|
125
132
|
};
|
|
126
133
|
// (sprint 77, APEX: the rendering tree by display position) the page template's positions, and others in use
|
|
@@ -128,7 +135,7 @@ export async function designerRoutes(app: FastifyInstance) {
|
|
|
128
135
|
const regionFolders = ['banner', 'breadcrumb', 'left', 'body', 'right', 'footer'].map((pos) => {
|
|
129
136
|
const list = rows.region.filter((r) => (r.position ?? 'body') === pos);
|
|
130
137
|
if (!list.length && pos !== 'body' && pos !== treeSide) return '';
|
|
131
|
-
return folder(
|
|
138
|
+
return folder(posLabel(pos), list.length ? list.map((r, i) => regionLeaf(r, list[i + 1])) : none(tx(pos === 'body' ? 'pd.tree.no_regions_yet' : 'pd.tree.no_regions')), list.length > 0);
|
|
132
139
|
});
|
|
133
140
|
const pageItems = rows.item.filter((i) => i.region_id === null);
|
|
134
141
|
const pageButtons = rows.button.filter((b) => b.region_id === null);
|
|
@@ -137,65 +144,65 @@ export async function designerRoutes(app: FastifyInstance) {
|
|
|
137
144
|
const headerComps = rows.computation.filter((x) => x.point === 'before_header');
|
|
138
145
|
const submitComps = rows.computation.filter((x) => x.point !== 'before_header');
|
|
139
146
|
const headerBranches = rows.branch.filter((x) => x.point === 'before_header');
|
|
140
|
-
const branchNote = (b: any) => `${b.when_button ? `${b.when_button}: ` : ''}→ ${b.target_type === 'url' ? b.target_url :
|
|
147
|
+
const branchNote = (b: any) => `${b.when_button ? `${b.when_button}: ` : ''}→ ${b.target_type === 'url' ? b.target_url : tx('pd.page_n', { n: b.target_page ?? p.page_no })}`;
|
|
141
148
|
const compLeaf = (x: any) => leaf('computation', x, icon('activity'), x.item_name, x.type);
|
|
142
149
|
const branchLeaf = (x: any) => leaf('branch', x, bicon('right'), x.name, branchNote(x));
|
|
143
150
|
const preRendering = [...headerBranches.map(branchLeaf), ...headerComps.map(compLeaf), ...loadProcs.map((x) => leaf('process', x, icon('code'), x.name))];
|
|
144
151
|
const createBar = (links: [string, string][]) =>
|
|
145
152
|
html`<div class="pd-treebar">${links.map(([k, label]) => html`<a class="tb-btn tb-text" href="${url(`new=${k}`)}">${icon('plus')}<span>${label}</span></a>`)}</div>`;
|
|
146
153
|
|
|
147
|
-
const rendering = html`${createBar([['region', '
|
|
148
|
-
<ul class="pd-tree" aria-label="
|
|
149
|
-
<li><a class="pd-node" href="${url('c=page')}"${cur('page')}>${icon('file')}<span class="pd-label"
|
|
154
|
+
const rendering = html`${createBar([['region', kindLabel('region')], ['item', kindLabel('item')], ['button', kindLabel('button')]])}
|
|
155
|
+
<ul class="pd-tree" aria-label="${tx('pd.tab.rendering')}" data-tree-menu data-page-id="${p.id}">
|
|
156
|
+
<li><a class="pd-node" href="${url('c=page')}"${cur('page')}>${icon('file')}<span class="pd-label">${tx('pd.page_title', { n: p.page_no, name: p.name })}</span>${badges(p)}</a>
|
|
150
157
|
<ul>
|
|
151
|
-
${folder('
|
|
158
|
+
${folder(tx('pd.tree.pre_rendering'), preRendering.length ? preRendering : none(tx('pd.tree.no_pre_rendering')), preRendering.length > 0)}
|
|
152
159
|
${regionFolders}
|
|
153
|
-
${pageItems.length ? folder('
|
|
154
|
-
${pageButtons.length ? folder('
|
|
160
|
+
${pageItems.length ? folder(tx('pd.tree.page_items'), pageItems.map(itemLeaf)) : ''}
|
|
161
|
+
${pageButtons.length ? folder(tx('pd.tree.page_buttons'), pageButtons.map(buttonLeaf)) : ''}
|
|
155
162
|
</ul></li>
|
|
156
163
|
</ul>
|
|
157
164
|
<dialog class="pd-dialog" id="pd-copy-dialog" aria-labelledby="pd-copy-title">
|
|
158
165
|
<form method="post" action="${post}/layout/copy">${csrf(s)}<input type="hidden" name="kind" value=""><input type="hidden" name="id" value="">
|
|
159
|
-
<h2 id="pd-copy-title"
|
|
166
|
+
<h2 id="pd-copy-title">${tx('pd.copy.title')}</h2>
|
|
160
167
|
<p class="muted" data-copy-what></p>
|
|
161
|
-
<div class="field"><label class="label" for="pd-copy-page"
|
|
168
|
+
<div class="field"><label class="label" for="pd-copy-page">${tx('pd.copy.page')}</label>
|
|
162
169
|
<select id="pd-copy-page" name="page">${pages.map((x) => html`<option value="${x.id}"${x.id === p.id ? raw(' selected') : ''}>${x.page_no}. ${x.name}</option>`)}</select>
|
|
163
|
-
<small class="help"
|
|
164
|
-
<div class="pd-dialog-buttons"><button type="button" class="btn" data-dialog-close
|
|
170
|
+
<small class="help">${tx('pd.copy.help', { prefix: `P${p.page_no}_…` })}</small></div>
|
|
171
|
+
<div class="pd-dialog-buttons"><button type="button" class="btn" data-dialog-close>${tx('common.cancel')}</button><button class="btn btn-primary">${tx('pd.copy.copy')}</button></div>
|
|
165
172
|
</form>
|
|
166
173
|
</dialog>`;
|
|
167
|
-
const events: [string, string][] = [['change', '
|
|
174
|
+
const events: [string, string][] = [['change', tx('pd.event.change')], ['click', tx('pd.event.click')], ['load', tx('pd.event.load')], ['menu_entry', tx('pd.event.menu_entry')]];
|
|
168
175
|
const daLeaf = (d: (typeof rows.dynamic_action)[number]) => leaf('dynamic_action', d, icon('bolt'), d.name, `${d.trigger_element ?? ''} → ${d.action}`);
|
|
169
176
|
// (sprint 115) the other events (focus, custom, before_submit, …) in one folder, when there are any
|
|
170
177
|
const others = rows.dynamic_action.filter((d) => !events.some(([ev]) => ev === d.event));
|
|
171
|
-
const dynamicActions = html`${createBar([['dynamic_action', '
|
|
172
|
-
<ul class="pd-tree" aria-label="
|
|
178
|
+
const dynamicActions = html`${createBar([['dynamic_action', kindLabel('dynamic_action')]])}
|
|
179
|
+
<ul class="pd-tree" aria-label="${tx('pd.tab.da')}">
|
|
173
180
|
${events.map(([ev, label]) => {
|
|
174
181
|
const list = rows.dynamic_action.filter((d) => d.event === ev);
|
|
175
|
-
return folder(
|
|
182
|
+
return folder(tx('pd.tree.events', { event: label }), list.length ? list.map(daLeaf) : none(tx('common.none')), list.length > 0);
|
|
176
183
|
})}
|
|
177
|
-
${others.length ? folder('
|
|
184
|
+
${others.length ? folder(tx('pd.tree.events', { event: tx('pd.event.other') }), others.map((d) => leaf('dynamic_action', d, icon('bolt'), d.name, `${d.event}: ${d.trigger_element ?? ''} → ${d.action}`)), true) : ''}
|
|
178
185
|
</ul>`;
|
|
179
186
|
// after processing: the branches in sequence, then (when none applies) the pressed button's target page
|
|
180
187
|
const afterBranches = rows.branch.filter((x) => x.point !== 'before_header');
|
|
181
188
|
const buttonTargets = rows.button.filter((b) => b.target_page && b.action === 'submit');
|
|
182
|
-
const afterProcessing = [...afterBranches.map(branchLeaf), ...buttonTargets.map((b) => leaf('button', b, bicon('right'), `${b.name} →
|
|
183
|
-
const processing = html`${createBar([['computation', '
|
|
184
|
-
<ul class="pd-tree" aria-label="
|
|
185
|
-
${folder('
|
|
186
|
-
${folder('
|
|
187
|
-
${folder('
|
|
188
|
-
${folder('
|
|
189
|
+
const afterProcessing = [...afterBranches.map(branchLeaf), ...buttonTargets.map((b) => leaf('button', b, bicon('right'), `${b.name} → ${tx('pd.page_n', { n: b.target_page })}`, tx('pd.tree.button_target')))];
|
|
190
|
+
const processing = html`${createBar([['computation', kindLabel('computation')], ['validation', kindLabel('validation')], ['process', kindLabel('process')], ['branch', kindLabel('branch')]])}
|
|
191
|
+
<ul class="pd-tree" aria-label="${tx('pd.tab.processing')}">
|
|
192
|
+
${folder(tx('pd.tree.after_submit'), submitComps.length ? submitComps.map(compLeaf) : none(tx('pd.tree.no_computations')), submitComps.length > 0)}
|
|
193
|
+
${folder(tx('pd.tree.validating'), rows.validation.length ? rows.validation.map((v) => leaf('validation', v, icon('check'), v.name, v.when_button ?? '')) : none(tx('pd.tree.no_validations')))}
|
|
194
|
+
${folder(tx('pd.tab.processing'), submitProcs.length ? submitProcs.map((x) => leaf('process', x, icon('code'), x.name, x.when_button ?? '')) : none(tx('pd.tree.no_processes')))}
|
|
195
|
+
${folder(tx('pd.tree.after_processing'), afterProcessing.length ? afterProcessing : none(tx('pd.tree.no_branches')), afterProcessing.length > 0)}
|
|
189
196
|
</ul>`;
|
|
190
197
|
const sharedLink = (k: string, id: number, ic: string, label: string) =>
|
|
191
198
|
html`<li><a class="pd-node" href="${BASE}/apps/${p.app_id}/shared?c=${k}-${id}">${icon(ic)}<span class="pd-label">${label}</span></a></li>`;
|
|
192
199
|
const [lovs, schemes, navs, appItems] = shared;
|
|
193
|
-
const sharedTree = html`<div class="pd-treebar"><a class="tb-btn tb-text" href="${BASE}/apps/${p.app_id}/shared">${bicon('shapes')}<span
|
|
194
|
-
<ul class="pd-tree" aria-label="
|
|
195
|
-
${folder('
|
|
196
|
-
${folder('
|
|
197
|
-
${folder('
|
|
198
|
-
${folder('
|
|
200
|
+
const sharedTree = html`<div class="pd-treebar"><a class="tb-btn tb-text" href="${BASE}/apps/${p.app_id}/shared">${bicon('shapes')}<span>${tx('pd.shared.all')}</span></a></div>
|
|
201
|
+
<ul class="pd-tree" aria-label="${tx('pd.tab.shared')}">
|
|
202
|
+
${folder(tx('pd.shared.lovs'), lovs.length ? lovs.map((l) => sharedLink('lov', l.id, 'list', l.name)) : none(tx('common.none')), lovs.length > 0)}
|
|
203
|
+
${folder(tx('pd.shared.authz'), schemes.length ? schemes.map((a) => sharedLink('authz_scheme', a.id, 'shield', a.name)) : none(tx('common.none')), false)}
|
|
204
|
+
${folder(tx('pd.shared.nav'), navs.length ? navs.map((n) => sharedLink('nav_entry', n.id, 'menu', n.label)) : none(tx('common.none')), false)}
|
|
205
|
+
${folder(tx('pd.shared.app_items'), appItems.length ? appItems.map((n) => sharedLink('app_item', n.id, 'edit', n.name)) : none(tx('common.none')), false)}
|
|
199
206
|
</ul>`;
|
|
200
207
|
const selProcess = kind === 'process' ? rows.process.find((x) => String(x.id) === cid) : null;
|
|
201
208
|
const selComp = kind === 'computation' ? rows.computation.find((x) => String(x.id) === cid) : null;
|
|
@@ -203,18 +210,18 @@ export async function designerRoutes(app: FastifyInstance) {
|
|
|
203
210
|
const leftTab = kind === 'dynamic_action' || newKind === 'dynamic_action' ? 'da'
|
|
204
211
|
: kind === 'validation' || ['validation', 'process', 'computation', 'branch'].includes(newKind ?? '') || (selProcess && selProcess.point !== 'load')
|
|
205
212
|
|| (selComp && selComp.point !== 'before_header') || (selBranch && selBranch.point !== 'before_header') ? 'proc' : 'rend';
|
|
206
|
-
const left = html`<div class="pd-tabs pd-tabs-icons" data-tabs="pd-left" aria-label="
|
|
207
|
-
${tab('pd-l-rendering', '
|
|
208
|
-
${tab('pd-l-da', '
|
|
209
|
-
${tab('pd-l-processing', '
|
|
210
|
-
${tab('pd-l-shared', '
|
|
213
|
+
const left = html`<div class="pd-tabs pd-tabs-icons" data-tabs="pd-left" aria-label="${tx('pd.components')}">
|
|
214
|
+
${tab('pd-l-rendering', tx('pd.tab.rendering'), rendering, leftTab === 'rend', 'rendering')}
|
|
215
|
+
${tab('pd-l-da', tx('pd.tab.da'), dynamicActions, leftTab === 'da', 'bolt')}
|
|
216
|
+
${tab('pd-l-processing', tx('pd.tab.processing'), processing, leftTab === 'proc', 'processing')}
|
|
217
|
+
${tab('pd-l-shared', tx('pd.tab.shared'), sharedTree, false, 'shapes')}
|
|
211
218
|
</div>`;
|
|
212
219
|
|
|
213
220
|
// ------------------------------------------------------------ center: layout + gallery
|
|
214
221
|
const chip = (k: 'item' | 'button', r: any) =>
|
|
215
222
|
html`<a class="pd-chip pd-chip-${k}${isSel(`${k}-${r.id}`) ? ' is-selected' : ''}${k === 'button' && r.hot ? ' is-hot' : ''}" href="${url(`c=${k}-${r.id}`)}" draggable="true" data-kind="${k}" data-id="${r.id}"${cur(`${k}-${r.id}`)} aria-describedby="pd-kbd">${k === 'item' ? bicon(itemIcon(r.type)) : ''}<span>${r.label || r.name}</span>${k === 'item' ? html`<small>${r.name}</small>` : ''}</a>`;
|
|
216
223
|
const slot = (k: 'item' | 'button', regionId: number | null, list: any[], buttonSlot = '') =>
|
|
217
|
-
html`<div class="pd-slot pd-slot-${k}${buttonSlot && buttonSlot !== 'region' && !list.length ? ' pd-slot-spare' : ''}" data-drop="${k}" data-region="${regionId ?? ''}"${buttonSlot ? raw(` data-slot="${buttonSlot}"`) : ''}${buttonSlot && buttonSlot !== 'region' ?
|
|
224
|
+
html`<div class="pd-slot pd-slot-${k}${buttonSlot && buttonSlot !== 'region' && !list.length ? ' pd-slot-spare' : ''}" data-drop="${k}" data-region="${regionId ?? ''}"${buttonSlot ? raw(` data-slot="${buttonSlot}"`) : ''}${buttonSlot && buttonSlot !== 'region' ? html` title="${tx('pd.slot_title', { slot: slotLabel(buttonSlot) })}"` : ''}>${list.length ? list.map((r) => chip(k, r)) : html`<span class="pd-slot-hint">${k === 'item' ? tx('pd.tree.items') : buttonSlot && buttonSlot !== 'region' ? slotLabel(buttonSlot) : tx('pd.tree.buttons')}</span>`}</div>`;
|
|
218
225
|
// (sprint 77, APEX: region slots) a region's buttons by position: the header's, the region's own, the bottom bar's
|
|
219
226
|
const buttonSlots = (r: any) => {
|
|
220
227
|
const mine = rows.button.filter((b) => b.region_id === r.id);
|
|
@@ -224,17 +231,17 @@ export async function designerRoutes(app: FastifyInstance) {
|
|
|
224
231
|
};
|
|
225
232
|
const block = (r: any) => html`<div class="pd-region pd-span-${r.columns}${isSel(`region-${r.id}`) ? ' is-selected' : ''}" data-kind="region" data-id="${r.id}" data-span="${r.columns}">
|
|
226
233
|
<div class="pd-region-head" draggable="true" data-kind="region" data-id="${r.id}">
|
|
227
|
-
<span class="pd-grip" title="
|
|
234
|
+
<span class="pd-grip" title="${tx('pd.drag_move')}">${bicon('grip')}</span>
|
|
228
235
|
<a class="pd-region-link" href="${url(`c=region-${r.id}`)}"${cur(`region-${r.id}`)} aria-describedby="pd-kbd">${bicon(regionIcon(r.type))}<span>${regionTitle(r)}</span></a>
|
|
229
236
|
<span class="pd-region-meta">${r.columns}/12</span>
|
|
230
237
|
</div>
|
|
231
238
|
<div class="pd-region-body">
|
|
232
|
-
<div class="pd-region-type">${REGION_LABELS[r.type]?.[0] ?? r.type}${r.template !== 'standard' ? ` · ${r.template}` : ''}</div>
|
|
239
|
+
<div class="pd-region-type">${tx.dyn(`gallery.region.${r.type}`, REGION_LABELS[r.type]?.[0] ?? r.type)}${r.template !== 'standard' ? ` · ${r.template}` : ''}</div>
|
|
233
240
|
${r.source || r.table_name ? html`<code class="pd-region-src">${firstLine(r.table_name ?? r.source)}</code>` : ''}
|
|
234
241
|
${slot('item', r.id, rows.item.filter((i) => i.region_id === r.id))}
|
|
235
242
|
${buttonSlots(r)}
|
|
236
243
|
</div>
|
|
237
|
-
<span class="pd-resize" title="
|
|
244
|
+
<span class="pd-resize" title="${tx('pd.drag_span')}" aria-hidden="true"></span>
|
|
238
245
|
</div>`;
|
|
239
246
|
// (sprint 77, APEX: the page template's display positions) a drop zone per position of the page template, and
|
|
240
247
|
// for any other position that has regions (they show in the body when the page runs)
|
|
@@ -243,11 +250,11 @@ export async function designerRoutes(app: FastifyInstance) {
|
|
|
243
250
|
const zone = (pos: string) => {
|
|
244
251
|
const list = byPos(pos);
|
|
245
252
|
const off = (pos === 'left' || pos === 'right') && pos !== side;
|
|
246
|
-
return html`<section class="pd-zone pd-zone-${pos}${list.length ? '' : ' is-empty'}" aria-label="${
|
|
247
|
-
<div class="pd-zone-label">${
|
|
253
|
+
return html`<section class="pd-zone pd-zone-${pos}${list.length ? '' : ' is-empty'}" aria-label="${posLabel(pos)}">
|
|
254
|
+
<div class="pd-zone-label">${posLabel(pos)}${off ? html` <small>${tx('pd.zone_off')}</small>` : ''}</div>
|
|
248
255
|
<div class="pd-grid" data-drop="region" data-position="${pos}">
|
|
249
256
|
${list.map(block)}
|
|
250
|
-
<div class="pd-grid-end" data-end><a href="${url(`new=region${pos === 'body' ? '' : `&position=${pos}`}`)}">${icon('plus')}
|
|
257
|
+
<div class="pd-grid-end" data-end><a href="${url(`new=region${pos === 'body' ? '' : `&position=${pos}`}`)}">${icon('plus')} ${kindLabel('region')}${pos === 'body' ? '' : html`<span class="sr-only"> ${tx('pd.in_position', { position: posLabel(pos) })}</span>`}</a></div>
|
|
251
258
|
</div></section>`;
|
|
252
259
|
};
|
|
253
260
|
const extra = (pos: string) => pos !== side && byPos(pos).length > 0;
|
|
@@ -256,28 +263,31 @@ export async function designerRoutes(app: FastifyInstance) {
|
|
|
256
263
|
${side === 'left' || extra('left') ? zone('left') : ''}${zone('body')}${side === 'right' || extra('right') ? zone('right') : ''}
|
|
257
264
|
</div>${zone('footer')}`;
|
|
258
265
|
const gallery = (k: 'region' | 'item' | 'button', types: string[], labels: Record<string, [string, string]>) =>
|
|
259
|
-
html`<ul class="pd-gallery-list" aria-label="${COMPONENTS[k].plural}">${types.map((t) =>
|
|
266
|
+
html`<ul class="pd-gallery-list" aria-label="${tx.dyn(`comp.${k}.plural`, COMPONENTS[k].plural)}">${types.map((t) => {
|
|
267
|
+
const name = tx.dyn(`gallery.${k}.${t}`, labels[t]?.[0] ?? t);
|
|
268
|
+
return html`<li><a class="pd-gal" href="${url(`new=${k}&type=${t}`)}" draggable="true" data-new="${k}" data-type="${t}" title="${tx('pd.gallery_title', { name })}">${bicon(labels[t]?.[1] ?? k)}<span>${name}</span></a></li>`;
|
|
269
|
+
})}</ul>`;
|
|
260
270
|
const layout = html`
|
|
261
|
-
<div class="pd-canvas-bar" role="toolbar" aria-label="
|
|
262
|
-
<button type="button" class="tb-btn" data-zoom="-1" title="
|
|
263
|
-
<button type="button" class="tb-btn" data-zoom="1" title="
|
|
264
|
-
<button type="button" class="tb-btn" data-maximize title="
|
|
265
|
-
<span class="pd-canvas-hint" id="pd-kbd"
|
|
271
|
+
<div class="pd-canvas-bar" role="toolbar" aria-label="${tx('pd.layout')}">
|
|
272
|
+
<button type="button" class="tb-btn" data-zoom="-1" title="${tx('pd.zoom_out')}" hidden>${bicon('zoom-out')}<span class="sr-only">${tx('pd.zoom_out')}</span></button>
|
|
273
|
+
<button type="button" class="tb-btn" data-zoom="1" title="${tx('pd.zoom_in')}" hidden>${bicon('zoom-in')}<span class="sr-only">${tx('pd.zoom_in')}</span></button>
|
|
274
|
+
<button type="button" class="tb-btn" data-maximize title="${tx('pd.maximize')}" aria-pressed="false" hidden>${bicon('expand')}<span class="sr-only">${tx('pd.maximize')}</span></button>
|
|
275
|
+
<span class="pd-canvas-hint" id="pd-kbd">${tx('pd.kbd_hint')}</span>
|
|
266
276
|
</div>
|
|
267
277
|
<div class="pd-canvas" id="pd-layout" data-page-id="${p.id}">
|
|
268
|
-
<div class="pd-page-label">${icon('file')}
|
|
278
|
+
<div class="pd-page-label">${icon('file')} ${tx('pd.page_title', { n: p.page_no, name: p.name })}${p.mode === 'modal' ? ` ${tx('pd.modal')}` : ''}</div>
|
|
269
279
|
${zones}
|
|
270
280
|
<div class="pd-pagelevel">
|
|
271
|
-
<div class="pd-pagelevel-title"
|
|
281
|
+
<div class="pd-pagelevel-title">${tx('pd.page_level')}</div>
|
|
272
282
|
${slot('item', null, pageItems)}
|
|
273
283
|
${slot('button', null, pageButtons)}
|
|
274
284
|
</div>
|
|
275
285
|
</div>
|
|
276
286
|
<div class="pd-gallery">
|
|
277
|
-
<div class="pd-tabs" data-tabs="pd-gallery" data-tabs-remember aria-label="
|
|
278
|
-
${tab('pd-g-regions', '
|
|
279
|
-
${tab('pd-g-items', '
|
|
280
|
-
${tab('pd-g-buttons', '
|
|
287
|
+
<div class="pd-tabs" data-tabs="pd-gallery" data-tabs-remember aria-label="${tx('pd.gallery')}">
|
|
288
|
+
${tab('pd-g-regions', tx('pd.gallery.regions'), gallery('region', REGION_TYPES(), REGION_LABELS), true)}
|
|
289
|
+
${tab('pd-g-items', tx('pd.tree.items'), gallery('item', ITEM_TYPES(), ITEM_LABELS))}
|
|
290
|
+
${tab('pd-g-buttons', tx('pd.tree.buttons'), gallery('button', BUTTON_ACTIONS(), BUTTON_LABELS))}
|
|
281
291
|
</div>
|
|
282
292
|
</div>`;
|
|
283
293
|
const help = html`<div class="pd-help">
|
|
@@ -297,8 +307,8 @@ export async function designerRoutes(app: FastifyInstance) {
|
|
|
297
307
|
<p>A PL/pgSQL <code>raise exception 'Message' using column = 'sal'</code> shows the message on the item whose source column is <code>sal</code>.</p>
|
|
298
308
|
</div>`;
|
|
299
309
|
const pageSearch = html`<form method="get" action="${BASE}/apps/${p.app_id}/search" class="pd-search" role="search">
|
|
300
|
-
<label class="label" for="pd-q"
|
|
301
|
-
<div class="u-row"><input id="pd-q" name="q" type="search" placeholder="
|
|
310
|
+
<label class="label" for="pd-q">${tx('pd.search_label')}</label>
|
|
311
|
+
<div class="u-row"><input id="pd-q" name="q" type="search" placeholder="${tx('pd.search_placeholder', { prefix: `P${p.page_no}_` })}"><button class="btn btn-sm">${icon('search')} ${tx('common.search')}</button></div>
|
|
302
312
|
</form>`;
|
|
303
313
|
// a lock of another developer (on this page or the whole application) makes the page read-only
|
|
304
314
|
const locks = (await appLocks(p.app_id)).filter((l) => l.page_no === 0 || l.page_no === p.page_no);
|
|
@@ -308,12 +318,12 @@ export async function designerRoutes(app: FastifyInstance) {
|
|
|
308
318
|
const blocking = runOnly ?? locks.find((l) => l.locked_by !== s.username);
|
|
309
319
|
const mine = locks.find((l) => l.locked_by === s.username);
|
|
310
320
|
const commentCount = (await owner.one('select count(*)::int as n from meta.dev_comment where app_id = $1 and page_no = $2', [p.app_id, p.page_no])).n;
|
|
311
|
-
const center = html`${blocking ? html`<div class="alert alert-error pd-lock" role="status">${icon('key')} ${lockText(blocking)}
|
|
312
|
-
<div class="pd-tabs pd-tabs-center" data-tabs="pd-center" aria-label="
|
|
313
|
-
${tab('pd-c-layout', '
|
|
314
|
-
${tab('pd-c-search', '
|
|
315
|
-
${tab('pd-c-notes',
|
|
316
|
-
${tab('pd-c-help', '
|
|
321
|
+
const center = html`${blocking ? html`<div class="alert alert-error pd-lock" role="status">${icon('key')} ${lockText(blocking)} ${tx('pd.lock_refused')}</div>` : ''}${await pageIssuesNote(p.app_id, p.page_no)}
|
|
322
|
+
<div class="pd-tabs pd-tabs-center" data-tabs="pd-center" aria-label="${tx('pd.copy.page')}">
|
|
323
|
+
${tab('pd-c-layout', tx('pd.layout'), layout, true)}
|
|
324
|
+
${tab('pd-c-search', tx('pd.page_search'), pageSearch)}
|
|
325
|
+
${tab('pd-c-notes', `${tx('pd.lock_comments')}${commentCount ? ` (${commentCount})` : ''}${mine || blocking ? ` · ${tx('pd.locked')}` : ''}`, await lockPanel(s, p.app_id, p.page_no))}
|
|
326
|
+
${tab('pd-c-help', tx('rail.help'), help)}
|
|
317
327
|
</div>`;
|
|
318
328
|
|
|
319
329
|
// ------------------------------------------------------------ right: property editor
|
|
@@ -322,7 +332,7 @@ export async function designerRoutes(app: FastifyInstance) {
|
|
|
322
332
|
let arrange: Raw | '' = '';
|
|
323
333
|
const formId = 'pd-form';
|
|
324
334
|
let hasForm = true;
|
|
325
|
-
const peTabs = (panels: Raw[]) => html`<div class="pd-tabs" data-tabs="pd-right" data-tabs-remember aria-label="
|
|
335
|
+
const peTabs = (panels: Raw[]) => html`<div class="pd-tabs" data-tabs="pd-right" data-tabs-remember aria-label="${tx('pd.properties')}">${panels}</div>`;
|
|
326
336
|
const moveForm = (k: string, id: number, fields: Raw, button: Raw, title: string) =>
|
|
327
337
|
html`<form method="post" action="${post}/layout/move" class="pe-arrange-form">${csrf(s)}<input type="hidden" name="kind" value="${k}"><input type="hidden" name="id" value="${id}">${fields}<button class="tb-btn" title="${title}">${button}</button></form>`;
|
|
328
338
|
if (newKind) {
|
|
@@ -332,43 +342,43 @@ export async function designerRoutes(app: FastifyInstance) {
|
|
|
332
342
|
const presetType = spec.fields.find((f) => f.name === typeField)?.options?.includes(req.query.type ?? '') ? { [typeField]: req.query.type } : {};
|
|
333
343
|
const presetPlace = newKind === 'region' && POSITIONS().includes(req.query.position ?? '') ? { position: req.query.position } : {};
|
|
334
344
|
const defaults = { seq: lastSeq + 10, ...spec.defaults, ...presetType, ...presetPlace, region_id: /^\d+$/.test(req.query.region ?? '') ? Number(req.query.region) : null };
|
|
335
|
-
heading = html`${icon(spec.icon)}<span
|
|
336
|
-
props = peTabs([tab('pd-r-props',
|
|
345
|
+
heading = html`${icon(spec.icon)}<span>${tx.dyn(`pd.new.${newKind}`, `New ${spec.label.toLowerCase()}`)}</span>`;
|
|
346
|
+
props = peTabs([tab('pd-r-props', kindLabel(newKind), componentForm(spec, newKind, defaults, lk, `${BASE}/pages/${p.id}/c/${newKind}`, s, tx.dyn(`pd.create.${newKind}`, `Create ${spec.label.toLowerCase()}`), { id: formId }), true)]);
|
|
337
347
|
} else if (kind && kind !== 'page' && PAGE_KINDS.includes(kind)) {
|
|
338
348
|
const spec = COMPONENTS[kind];
|
|
339
349
|
const row = rows[kind].find((r) => String(r.id) === cid);
|
|
340
350
|
if (row) {
|
|
341
|
-
heading = html`${kind === 'region' ? bicon(regionIcon(row.type)) : kind === 'item' ? bicon(itemIcon(row.type)) : kind === 'button' ? bicon('button') : icon(spec.icon)}<span>${
|
|
351
|
+
heading = html`${kind === 'region' ? bicon(regionIcon(row.type)) : kind === 'item' ? bicon(itemIcon(row.type)) : kind === 'button' ? bicon('button') : icon(spec.icon)}<span>${kindLabel(kind)}: ${spec.summary(row)}</span>`;
|
|
342
352
|
const settings = kind === 'region' ? await regionSettingsForm(p.id, p.app_id, row, s) : '';
|
|
343
353
|
props = html`${peTabs([
|
|
344
|
-
tab('pd-r-props',
|
|
345
|
-
...(settings ? [tab('pd-r-attrs', '
|
|
346
|
-
...(kind === 'process' && row.type === 'chain' && row.config?.background ? [tab('pd-r-jobs', '
|
|
354
|
+
tab('pd-r-props', kindLabel(kind), componentForm(spec, kind, row, lk, `${BASE}/pages/${p.id}/c/${kind}/${row.id}`, s, tx('common.save'), { id: formId }), true),
|
|
355
|
+
...(settings ? [tab('pd-r-attrs', tx('pd.attributes'), settings)] : []),
|
|
356
|
+
...(kind === 'process' && row.type === 'chain' && row.config?.background ? [tab('pd-r-jobs', tx('pd.jobs'), await processJobsPanel(row.id))] : []),
|
|
347
357
|
])}
|
|
348
358
|
${await usedInPanel(p.app_id, kind, row)}
|
|
349
359
|
<form method="post" action="${BASE}/pages/${p.id}/c/${kind}/${row.id}/delete" class="danger-zone">${csrf(s)}
|
|
350
|
-
<button class="btn btn-sm btn-danger" data-confirm="Delete this ${spec.label.toLowerCase()}
|
|
360
|
+
<button class="btn btn-sm btn-danger" data-confirm="${tx.dyn(`pd.delete_confirm.${kind}`, `Delete this ${spec.label.toLowerCase()}?`)}">${tx.dyn(`pd.delete.${kind}`, `Delete ${spec.label.toLowerCase()}`)}</button></form>`;
|
|
351
361
|
if (kind === 'region' || kind === 'item' || kind === 'button') {
|
|
352
|
-
const regionOptions = html`<option value=""
|
|
353
|
-
arrange = html`<div class="pe-arrange" role="group" aria-label="
|
|
354
|
-
<span class="pe-arrange-label"
|
|
355
|
-
${moveForm(kind, row.id, html`<input type="hidden" name="dir" value="up">`, html`${bicon('arrow-up')}<span class="sr-only"
|
|
356
|
-
${moveForm(kind, row.id, html`<input type="hidden" name="dir" value="down">`, html`${bicon('arrow-down')}<span class="sr-only"
|
|
362
|
+
const regionOptions = html`<option value="">${tx('pd.arrange.page_level')}</option>${rows.region.map((r) => html`<option value="${r.id}"${row.region_id === r.id ? raw(' selected') : ''}>${regionTitle(r)}</option>`)}`;
|
|
363
|
+
arrange = html`<div class="pe-arrange" role="group" aria-label="${tx('pd.arrange')}">
|
|
364
|
+
<span class="pe-arrange-label">${tx('pd.arrange')}</span>
|
|
365
|
+
${moveForm(kind, row.id, html`<input type="hidden" name="dir" value="up">`, html`${bicon('arrow-up')}<span class="sr-only">${tx('pd.arrange.up')}</span>`, tx('pd.arrange.up_title'))}
|
|
366
|
+
${moveForm(kind, row.id, html`<input type="hidden" name="dir" value="down">`, html`${bicon('arrow-down')}<span class="sr-only">${tx('pd.arrange.down')}</span>`, tx('pd.arrange.down_title'))}
|
|
357
367
|
${kind === 'region'
|
|
358
|
-
? html`<form method="post" action="${post}/layout/span" class="pe-arrange-form">${csrf(s)}<input type="hidden" name="id" value="${row.id}"><input type="hidden" name="delta" value="-1"><button class="tb-btn" title="
|
|
359
|
-
<span class="pe-span" title="
|
|
360
|
-
<form method="post" action="${post}/layout/span" class="pe-arrange-form">${csrf(s)}<input type="hidden" name="id" value="${row.id}"><input type="hidden" name="delta" value="1"><button class="tb-btn" title="
|
|
361
|
-
: moveForm(kind, row.id, html`<label class="sr-only" for="pe-move-region"
|
|
368
|
+
? html`<form method="post" action="${post}/layout/span" class="pe-arrange-form">${csrf(s)}<input type="hidden" name="id" value="${row.id}"><input type="hidden" name="delta" value="-1"><button class="tb-btn" title="${tx('pd.arrange.narrower_title')}"${row.columns <= 1 ? raw(' disabled') : ''}>${bicon('narrow')}<span class="sr-only">${tx('pd.arrange.narrower')}</span></button></form>
|
|
369
|
+
<span class="pe-span" title="${tx('pd.arrange.span')}">${row.columns}/12</span>
|
|
370
|
+
<form method="post" action="${post}/layout/span" class="pe-arrange-form">${csrf(s)}<input type="hidden" name="id" value="${row.id}"><input type="hidden" name="delta" value="1"><button class="tb-btn" title="${tx('pd.arrange.wider_title')}"${row.columns >= 12 ? raw(' disabled') : ''}>${bicon('wide')}<span class="sr-only">${tx('pd.arrange.wider')}</span></button></form>`
|
|
371
|
+
: moveForm(kind, row.id, html`<label class="sr-only" for="pe-move-region">${tx('pd.arrange.to_region')}</label><select id="pe-move-region" name="region">${regionOptions}</select>`, html`<span>${tx('pd.arrange.move')}</span>`, tx('pd.arrange.move_title'))}
|
|
362
372
|
</div>`;
|
|
363
373
|
}
|
|
364
374
|
} else {
|
|
365
|
-
heading = html`<span
|
|
366
|
-
props = html`<p class="muted"
|
|
375
|
+
heading = html`<span>${tx('error.label_not_found')}</span>`;
|
|
376
|
+
props = html`<p class="muted">${tx('pd.not_found')}</p>`;
|
|
367
377
|
hasForm = false;
|
|
368
378
|
}
|
|
369
379
|
} else {
|
|
370
|
-
heading = html`${icon('file')}<span
|
|
371
|
-
props = html`${peTabs([tab('pd-r-props', '
|
|
380
|
+
heading = html`${icon('file')}<span>${tx('pd.page_title', { n: p.page_no, name: p.name })}</span>`;
|
|
381
|
+
props = html`${peTabs([tab('pd-r-props', tx('pd.copy.page'), html`
|
|
372
382
|
<form method="post" action="${BASE}/pages/${p.id}" class="component-form" id="${formId}">${csrf(s)}
|
|
373
383
|
<fieldset class="prop-group"><legend>Identification</legend><div class="form-grid">
|
|
374
384
|
${input('page_no', 'Page number', p.page_no, { type: 'number', required: true })}
|
|
@@ -394,11 +404,11 @@ export async function designerRoutes(app: FastifyInstance) {
|
|
|
394
404
|
${select('build_option', 'Build option', p.build_option ?? '', buildOptionChoices(lk, p.build_option),
|
|
395
405
|
'While the option is excluded (or "Not": included) the page does not exist in the running application.')}
|
|
396
406
|
</div></fieldset>
|
|
397
|
-
<div class="buttons"><button class="btn btn-hot"
|
|
407
|
+
<div class="buttons"><button class="btn btn-hot">${tx('pd.save_page')}</button></div>
|
|
398
408
|
</form>`, true)])}
|
|
399
409
|
${await usedInPanel(p.app_id, 'page', p)}
|
|
400
410
|
<form method="post" action="${BASE}/pages/${p.id}/delete" class="danger-zone">${csrf(s)}
|
|
401
|
-
<button class="btn btn-sm btn-danger" data-confirm="
|
|
411
|
+
<button class="btn btn-sm btn-danger" data-confirm="${tx('pd.delete_page_confirm', { n: p.page_no })}">${tx('pd.delete_page')}</button></form>`;
|
|
402
412
|
}
|
|
403
413
|
// (sprint 59) the help pane: the guide section of the selected component's type
|
|
404
414
|
const guide = (() => {
|
|
@@ -410,13 +420,13 @@ export async function designerRoutes(app: FastifyInstance) {
|
|
|
410
420
|
})();
|
|
411
421
|
const right = html`
|
|
412
422
|
<div class="pe-head"><div class="pe-title">${heading}</div>
|
|
413
|
-
<div class="pe-filter-row" hidden><label class="sr-only" for="pe-filter"
|
|
423
|
+
<div class="pe-filter-row" hidden><label class="sr-only" for="pe-filter">${tx('pd.filter_label')}</label>${icon('filter')}<input id="pe-filter" class="pe-filter" type="search" placeholder="${tx('pd.filter')}" autocomplete="off"></div>
|
|
414
424
|
</div>
|
|
415
425
|
${arrange}
|
|
416
426
|
<div class="pe">${props}</div>
|
|
417
427
|
<details class="pe-help" open${guide ? raw(` data-guide="${guide}"`) : ''}>
|
|
418
|
-
<summary>${icon('help')}
|
|
419
|
-
<div class="pe-help-body" aria-live="polite" data-pe-help><p class="muted"
|
|
428
|
+
<summary>${icon('help')} ${tx('rail.help')}</summary>
|
|
429
|
+
<div class="pe-help-body" aria-live="polite" data-pe-help><p class="muted">${tx('pd.help_hint')}</p></div>
|
|
420
430
|
</details>`;
|
|
421
431
|
|
|
422
432
|
// ------------------------------------------------------------ toolbar
|
|
@@ -424,54 +434,54 @@ export async function designerRoutes(app: FastifyInstance) {
|
|
|
424
434
|
const prev = pages[idx - 1];
|
|
425
435
|
const next = pages[idx + 1];
|
|
426
436
|
const h = undoState(s, p.id);
|
|
427
|
-
const tbForm = (op: string, ic: string, label: string, title: string | null) =>
|
|
428
|
-
html`<form method="post" action="${post}/layout/${op}" class="tb-form">${csrf(s)}<button class="tb-btn" title="${title ? `${label}: ${title}` :
|
|
437
|
+
const tbForm = (op: string, ic: string, label: string, nothing: string, title: string | null) =>
|
|
438
|
+
html`<form method="post" action="${post}/layout/${op}" class="tb-form">${csrf(s)}<button class="tb-btn" title="${title ? `${label}: ${title}` : nothing}"${title ? '' : raw(' disabled')}>${bicon(ic)}<span class="sr-only">${label}</span></button></form>`;
|
|
429
439
|
const createMenu = html`<details class="menu tb-menu">
|
|
430
|
-
<summary class="tb-btn" title="
|
|
440
|
+
<summary class="tb-btn" title="${tx('pd.menu.create')}">${icon('plus')}${bicon('down', 'icon tb-caret')}<span class="sr-only">${tx('pd.menu.create')}</span></summary>
|
|
431
441
|
<div class="menu-panel align-right"><div class="menu-section menu-links">
|
|
432
|
-
${[
|
|
433
|
-
html`<a href="${url(`new=${k}`)}">${icon(COMPONENTS[k].icon)} ${
|
|
442
|
+
${['region', 'item', 'button', 'dynamic_action', 'computation', 'validation', 'process', 'branch'].map((k) =>
|
|
443
|
+
html`<a href="${url(`new=${k}`)}">${icon(COMPONENTS[k].icon)} ${k === 'item' ? tx('pd.menu.page_item') : kindLabel(k)}</a>`)}
|
|
434
444
|
</div><div class="menu-section menu-links">
|
|
435
|
-
<a href="${BASE}/apps/${p.app_id}#create-page">${icon('file')}
|
|
445
|
+
<a href="${BASE}/apps/${p.app_id}#create-page">${icon('file')} ${tx('pd.menu.page')}</a>
|
|
436
446
|
</div></div></details>`;
|
|
437
447
|
const utilMenu = html`<details class="menu tb-menu">
|
|
438
|
-
<summary class="tb-btn" title="
|
|
448
|
+
<summary class="tb-btn" title="${tx('pd.menu.utilities')}">${bicon('wrench')}${bicon('down', 'icon tb-caret')}<span class="sr-only">${tx('pd.menu.utilities')}</span></summary>
|
|
439
449
|
<div class="menu-panel align-right"><div class="menu-section menu-links">
|
|
440
|
-
<a href="${BASE}/apps/${p.app_id}/advisor">${icon('check')}
|
|
441
|
-
<a href="${BASE}/apps/${p.app_id}/search?q=P${p.page_no}_">${icon('search')}
|
|
442
|
-
<a href="${BASE}/apps/${p.app_id}/shared">${bicon('shapes')}
|
|
443
|
-
<a href="${BASE}/apps/${p.app_id}">${bicon('pages')}
|
|
444
|
-
<a href="${BASE}/apps/${p.app_id}/export">${icon('download')}
|
|
450
|
+
<a href="${BASE}/apps/${p.app_id}/advisor">${icon('check')} ${tx('app.tab.advisor')}</a>
|
|
451
|
+
<a href="${BASE}/apps/${p.app_id}/search?q=P${p.page_no}_">${icon('search')} ${tx('pd.menu.search_items')}</a>
|
|
452
|
+
<a href="${BASE}/apps/${p.app_id}/shared">${bicon('shapes')} ${tx('pd.tab.shared')}</a>
|
|
453
|
+
<a href="${BASE}/apps/${p.app_id}">${bicon('pages')} ${tx('pd.menu.all_pages')}</a>
|
|
454
|
+
<a href="${BASE}/apps/${p.app_id}/export">${icon('download')} ${tx('pd.menu.export')}</a>
|
|
445
455
|
</div></div></details>`;
|
|
446
456
|
const toolbar = html`
|
|
447
|
-
<div class="tb-group pd-pagenav" role="group" aria-label="
|
|
448
|
-
${prev ? html`<a class="tb-btn" href="${designerUrl(p.app_id, prev.page_no)}" title="
|
|
457
|
+
<div class="tb-group pd-pagenav" role="group" aria-label="${tx('pd.copy.page')}">
|
|
458
|
+
${prev ? html`<a class="tb-btn" href="${designerUrl(p.app_id, prev.page_no)}" title="${tx('pd.prev_title', { n: prev.page_no, name: prev.name })}">${bicon('left')}<span class="sr-only">${tx('pd.prev')}</span></a>` : html`<span class="tb-btn" aria-disabled="true">${bicon('left')}</span>`}
|
|
449
459
|
<form method="get" action="${BASE}/apps/${p.app_id}/goto" class="tb-form pd-goto-no">
|
|
450
|
-
<label class="sr-only" for="pd-goto-no"
|
|
451
|
-
<input id="pd-goto-no" name="page" type="number" min="0" max="999999999" step="1" value="${p.page_no}" inputmode="numeric" title="
|
|
460
|
+
<label class="sr-only" for="pd-goto-no">${tx('pd.goto_no')}</label>
|
|
461
|
+
<input id="pd-goto-no" name="page" type="number" min="0" max="999999999" step="1" value="${p.page_no}" inputmode="numeric" title="${tx('pd.goto_no_title')}">
|
|
452
462
|
</form>
|
|
453
463
|
<form method="get" action="${BASE}/apps/${p.app_id}/goto" class="tb-form pd-goto">
|
|
454
|
-
<label class="sr-only" for="pd-goto"
|
|
464
|
+
<label class="sr-only" for="pd-goto">${tx('pd.goto')}</label>
|
|
455
465
|
<select id="pd-goto" name="page" data-autosubmit>${pages.map((x) => html`<option value="${x.page_no}"${x.id === p.id ? raw(' selected') : ''}>${x.page_no} · ${x.name}</option>`)}</select>
|
|
456
|
-
<button class="tb-btn tb-text pd-goto-go"
|
|
466
|
+
<button class="tb-btn tb-text pd-goto-go">${tx('common.go')}</button>
|
|
457
467
|
</form>
|
|
458
|
-
${next ? html`<a class="tb-btn" href="${designerUrl(p.app_id, next.page_no)}" title="
|
|
468
|
+
${next ? html`<a class="tb-btn" href="${designerUrl(p.app_id, next.page_no)}" title="${tx('pd.next_title', { n: next.page_no, name: next.name })}">${bicon('right')}<span class="sr-only">${tx('pd.next')}</span></a>` : html`<span class="tb-btn" aria-disabled="true">${bicon('right')}</span>`}
|
|
459
469
|
</div>
|
|
460
|
-
<div class="tb-group tb-undo" role="group" aria-label="
|
|
470
|
+
<div class="tb-group tb-undo" role="group" aria-label="${tx('pd.history')}">${tbForm('undo', 'undo', tx('pd.undo'), tx('pd.nothing_undo'), h.undo)}${tbForm('redo', 'redo', tx('pd.redo'), tx('pd.nothing_redo'), h.redo)}</div>
|
|
461
471
|
<div class="tb-group tb-menus">${createMenu}${utilMenu}</div>
|
|
462
472
|
<div class="tb-group tb-main">
|
|
463
|
-
${hasForm ? html`<button class="btn btn-sm tb-save" form="${formId}" title="
|
|
464
|
-
<a class="btn btn-sm btn-run" href="/a/${p.alias}/${p.page_no}" target="_blank" rel="noopener" title="
|
|
473
|
+
${hasForm ? html`<button class="btn btn-sm tb-save" form="${formId}" title="${tx('pd.save_title')}">${bicon('save')}<span>${tx('common.save')}</span></button>` : ''}
|
|
474
|
+
<a class="btn btn-sm btn-run" href="/a/${p.alias}/${p.page_no}" target="_blank" rel="noopener" title="${tx('pd.run_title', { n: p.page_no })}">${icon('play')}<span>${tx('common.run')}</span></a>
|
|
465
475
|
</div>`;
|
|
466
476
|
|
|
467
477
|
const pane = newKind || sel ? 'pd-p-props' : 'pd-p-layout';
|
|
468
|
-
const main = html`<h1 class="sr-only"
|
|
469
|
-
<div class="pd" data-tabs="pd-panes" data-tabs-media="(max-width: 1023px)" aria-label="
|
|
470
|
-
<section class="pd-pane pd-left tab-panel" id="pd-p-tree" data-tab><h2 class="tab-title pd-pane-title"><span
|
|
471
|
-
<section class="pd-pane pd-center tab-panel" id="pd-p-layout" data-tab${pane === 'pd-p-layout' ? raw(' data-tab-active') : ''}><h2 class="tab-title pd-pane-title"><span
|
|
472
|
-
<section class="pd-pane pd-right tab-panel" id="pd-p-props" data-tab${pane === 'pd-p-props' ? raw(' data-tab-active') : ''}><h2 class="tab-title pd-pane-title"><span
|
|
478
|
+
const main = html`<h1 class="sr-only">${tx('pd.h1', { n: p.page_no, name: p.name })}</h1>
|
|
479
|
+
<div class="pd" data-tabs="pd-panes" data-tabs-media="(max-width: 1023px)" aria-label="${tx('pd.designer')}">
|
|
480
|
+
<section class="pd-pane pd-left tab-panel" id="pd-p-tree" data-tab><h2 class="tab-title pd-pane-title"><span>${tx('pd.pane.tree')}</span></h2>${left}</section>
|
|
481
|
+
<section class="pd-pane pd-center tab-panel" id="pd-p-layout" data-tab${pane === 'pd-p-layout' ? raw(' data-tab-active') : ''}><h2 class="tab-title pd-pane-title"><span>${tx('pd.layout')}</span></h2>${center}</section>
|
|
482
|
+
<section class="pd-pane pd-right tab-panel" id="pd-p-props" data-tab${pane === 'pd-p-props' ? raw(' data-tab-active') : ''}><h2 class="tab-title pd-pane-title"><span>${tx('pd.properties')}</span></h2>${right}</section>
|
|
473
483
|
</div>`;
|
|
474
|
-
return send(reply, s, shell(s,
|
|
484
|
+
return send(reply, s, shell(s, tx('pd.page_n_title', { n: p.page_no }), [['App Builder', BASE], [p.app_name, `${BASE}/apps/${p.app_id}`], [tx('pd.page_title', { n: p.page_no, name: p.name })]], main, 'apps', { toolbar, full: true }));
|
|
475
485
|
}
|
|
476
486
|
|
|
477
487
|
// (sprint 59) Quick edit from the developer toolbar: an item or button of the page by its name
|
|
@@ -503,7 +513,7 @@ export async function designerRoutes(app: FastifyInstance) {
|
|
|
503
513
|
if (before && before.page_no !== Number(b.page_no))
|
|
504
514
|
for (const t of ['builder_lock', 'dev_comment'])
|
|
505
515
|
await owner.query(`update meta.${t} set page_no = $3 where app_id = $1 and page_no = $2`, [before.app_id, before.page_no, Number(b.page_no)]);
|
|
506
|
-
flash(s, '
|
|
516
|
+
flash(s, tr(s)('pd.page_saved'));
|
|
507
517
|
} catch (e) {
|
|
508
518
|
flash(s, (e as Error).message, 'error');
|
|
509
519
|
}
|
|
@@ -515,7 +525,7 @@ export async function designerRoutes(app: FastifyInstance) {
|
|
|
515
525
|
if (!s) return;
|
|
516
526
|
const p = await owner.one('delete from meta.page where id = $1 returning app_id, page_no', [req.params.pid]);
|
|
517
527
|
if (p) for (const t of ['builder_lock', 'dev_comment']) await owner.query(`delete from meta.${t} where app_id = $1 and page_no = $2`, [p.app_id, p.page_no]);
|
|
518
|
-
flash(s, '
|
|
528
|
+
flash(s, tr(s)('pd.page_deleted'));
|
|
519
529
|
return back(reply, s, p ? `${BASE}/apps/${p.app_id}` : BASE);
|
|
520
530
|
});
|
|
521
531
|
|
|
@@ -526,7 +536,7 @@ export async function designerRoutes(app: FastifyInstance) {
|
|
|
526
536
|
if (!PAGE_KINDS.includes(kind)) return reply.code(404).send('Unknown component type');
|
|
527
537
|
try {
|
|
528
538
|
const id = await saveComponent(kind, 'page_id', pid, cid, req.body ?? {});
|
|
529
|
-
flash(s, `${COMPONENTS[kind].label} saved.`);
|
|
539
|
+
flash(s, tr(s).dyn(`pd.saved.${kind}`, `${COMPONENTS[kind].label} saved.`));
|
|
530
540
|
return back(reply, s, `${BASE}/pages/${pid}?c=${kind}-${id}`);
|
|
531
541
|
} catch (e) {
|
|
532
542
|
flash(s, (e as Error).message, 'error');
|
|
@@ -540,7 +550,7 @@ export async function designerRoutes(app: FastifyInstance) {
|
|
|
540
550
|
const { pid, kind, cid } = req.params;
|
|
541
551
|
if (!PAGE_KINDS.includes(kind)) return reply.code(404).send('Unknown component type');
|
|
542
552
|
await owner.query(`delete from ${COMPONENTS[kind].table} where id = $1 and page_id = $2`, [cid, pid]);
|
|
543
|
-
flash(s, `${COMPONENTS[kind].label} deleted.`);
|
|
553
|
+
flash(s, tr(s).dyn(`pd.deleted.${kind}`, `${COMPONENTS[kind].label} deleted.`));
|
|
544
554
|
return back(reply, s, `${BASE}/pages/${pid}`);
|
|
545
555
|
});
|
|
546
556
|
}
|
|
@@ -64,7 +64,7 @@ export async function diagnosticsRoutes(app: FastifyInstance) {
|
|
|
64
64
|
for (const [k, v] of Object.entries(extra)) q.set(k, v);
|
|
65
65
|
return q.toString();
|
|
66
66
|
};
|
|
67
|
-
const main = html`${appHeader(a, 'activity')}
|
|
67
|
+
const main = html`${appHeader(a, 'activity', s)}
|
|
68
68
|
<div class="columns">
|
|
69
69
|
${region('Debug level', html`
|
|
70
70
|
<p class="muted u-mt0">${a.debug_level
|
|
@@ -131,7 +131,7 @@ export async function diagnosticsRoutes(app: FastifyInstance) {
|
|
|
131
131
|
)
|
|
132
132
|
).rows;
|
|
133
133
|
const slowest = Math.max(0, ...entries.map((e) => e.dur ?? 0));
|
|
134
|
-
const main = html`${appHeader(a, 'activity')}
|
|
134
|
+
const main = html`${appHeader(a, 'activity', s)}
|
|
135
135
|
${region(`Page view ${v.id}`, html`
|
|
136
136
|
<div class="stat-grid">
|
|
137
137
|
<div class="stat"><b>${v.ms ?? '?'} ms</b><span>total time</span></div>
|