pgkiln 0.45.0 → 0.46.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/docs/apex-feature-parity.md +6 -3
- package/docs/guide/03-builder.md +36 -12
- package/docs/guide/04-pages-and-regions.md +74 -2
- package/docs/guide/05-items.md +14 -2
- package/docs/guide/09-reference.md +1 -1
- package/docs/guide/11-from-apex.md +2 -1
- package/docs/guide/12-development.md +6 -1
- 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/src/app.ts +9 -1
- 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-browser.ts +81 -18
- 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/search.ts +3 -3
- package/src/builder/shared.ts +2 -2
- package/src/builder/ui.ts +2 -0
- 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/builder/newapp.ts
CHANGED
|
@@ -268,7 +268,8 @@ export async function createApp(c: Client, a: CheckedApp): Promise<{ id: number;
|
|
|
268
268
|
if (base !== 'standard') theme.base = base;
|
|
269
269
|
if (a.topNav) theme.nav = 'top';
|
|
270
270
|
if (a.accent) theme.accent = a.accent;
|
|
271
|
-
|
|
271
|
+
// (107) a new application starts at Release 1.0, like APEX's
|
|
272
|
+
const r = await c.query(`insert into meta.app (alias, name, authentication, db_role, theme, language, version) values ($1, $2, $3, $4, $5, $6, 'Release 1.0') returning id`, [a.alias, a.name, a.authentication, a.role, theme, a.language ?? 'en']);
|
|
272
273
|
const appId = r.rows[0].id as number;
|
|
273
274
|
await placeApp(c, appId, a.workspaceId);
|
|
274
275
|
const p = await c.query(`insert into meta.page (app_id, page_no, name, title) values ($1, 1, 'Home', 'Home') returning id`, [appId]);
|
|
@@ -4,7 +4,9 @@ import { html, raw, type Raw } from '../html.ts';
|
|
|
4
4
|
import { icon } from '../icons.ts';
|
|
5
5
|
import { readSchema } from '../ddl.ts';
|
|
6
6
|
import { back, BASE, csrf, developer, flash, region, send, shell, workshopTabs, type Req } from './ui.ts';
|
|
7
|
-
import { sessionSchema, workshopDb } from './workspaces.ts';
|
|
7
|
+
import { currentWorkspace, inWorkspaceSql, sessionSchema, workshopDb } from './workspaces.ts';
|
|
8
|
+
import { owner } from '../db.ts';
|
|
9
|
+
import type { Session } from '../session.ts';
|
|
8
10
|
|
|
9
11
|
// (sprint 75) The SQL Workshop's Object Browser like APEX's: a schema in the header (the session's
|
|
10
12
|
// schema by default), a tree with a folder per object type, a filter, a + menu that starts a CREATE
|
|
@@ -61,6 +63,23 @@ export function createTemplate(kind: string, schema: string): string | null {
|
|
|
61
63
|
}
|
|
62
64
|
|
|
63
65
|
const PAGE_SIZE = 25;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* (sprint 89, SQL-4) The parsing schema of the application changed last in the developer's workspace: the
|
|
69
|
+
* schema its database role may use (not public, pgkiln's or the system's). APEX opens the Object Browser
|
|
70
|
+
* on the workspace's parsing schema.
|
|
71
|
+
*/
|
|
72
|
+
async function parsingSchema(s: Session): Promise<string | null> {
|
|
73
|
+
const row = await owner.one<{ n: string }>(
|
|
74
|
+
`select n.nspname as n
|
|
75
|
+
from meta.app a join pg_roles r on r.rolname = a.db_role
|
|
76
|
+
join pg_namespace n on has_schema_privilege(r.oid, n.oid, 'USAGE')
|
|
77
|
+
where ${inWorkspaceSql('a', 1)} and n.nspname !~ '^pg_' and n.nspname not in ('information_schema', 'public') and not (n.nspname = any ($2))
|
|
78
|
+
order by a.updated_at desc, a.id desc, n.nspname limit 1`,
|
|
79
|
+
[currentWorkspace(s), SYSTEM_SCHEMAS],
|
|
80
|
+
).catch(() => undefined);
|
|
81
|
+
return row?.n ?? null;
|
|
82
|
+
}
|
|
64
83
|
/** A column type in an action: a type name with an optional size and array brackets. */
|
|
65
84
|
const TYPE_RE = /^[a-z][a-z0-9_ ]{0,40}(\(\d{1,4}(,\s?\d{1,4})?\))?(\[\])?$/i;
|
|
66
85
|
const NAME_RE = /^[A-Za-z_][A-Za-z0-9_$]{0,62}$/;
|
|
@@ -81,8 +100,15 @@ export async function objectBrowserRoutes(app: FastifyInstance) {
|
|
|
81
100
|
[system, SYSTEM_SCHEMAS],
|
|
82
101
|
)
|
|
83
102
|
).rows.map((r) => r.n);
|
|
84
|
-
|
|
85
|
-
|
|
103
|
+
// (sprint 89, SQL-4) the schema asked for, else the last one shown (remembered per session), else the session's
|
|
104
|
+
// schema of SQL Commands, else the parsing schema of the application changed last, else the first
|
|
105
|
+
const usable = (v: unknown): v is string => typeof v === 'string' && schemas.includes(v);
|
|
106
|
+
const asked = usable(q.schema) ? q.schema : null;
|
|
107
|
+
const last = s.state.__OBSCHEMA;
|
|
108
|
+
const own = asked || usable(last) ? null : await sessionSchema(s, db);
|
|
109
|
+
const parsing = asked || usable(last) || usable(own) ? null : await parsingSchema(s);
|
|
110
|
+
const schema = asked ?? (usable(last) ? last : usable(own) ? own : usable(parsing) ? parsing : (schemas[0] ?? ''));
|
|
111
|
+
if (schema && schema !== last) s.state.__OBSCHEMA = schema;
|
|
86
112
|
const filter = typeof q.q === 'string' ? q.q.trim().slice(0, 100) : '';
|
|
87
113
|
const objects = schema ? await listObjects(db, schema, system) : [];
|
|
88
114
|
const shown = objects.filter((o) => !filter || o.name.toLowerCase().includes(filter.toLowerCase()));
|
|
@@ -161,6 +187,24 @@ export async function objectBrowserRoutes(app: FastifyInstance) {
|
|
|
161
187
|
if (!cols.includes(col)) problem = 'Choose a column of the table.';
|
|
162
188
|
else if (!NAME_RE.test(to) || cols.includes(to)) problem = 'The new name must be a valid name that the table doesn\'t have yet.';
|
|
163
189
|
else sql = `alter table ${table.q} rename column ${ident(col)} to ${ident(to)};`;
|
|
190
|
+
} else if (b.action === 'modify') {
|
|
191
|
+
// (sprint 89, SQL-4) type, nulls and default in one ALTER TABLE; a default is one expression, never a statement
|
|
192
|
+
const type = String(b.type ?? '').trim();
|
|
193
|
+
const expr = String(b.default ?? '').trim();
|
|
194
|
+
const parts: string[] = [];
|
|
195
|
+
const c = ident(col);
|
|
196
|
+
if (!cols.includes(col)) problem = 'Choose a column of the table.';
|
|
197
|
+
else if (type && !TYPE_RE.test(type)) problem = 'A type is a name with an optional size, e.g. text, numeric(9,2), varchar(40), int[].';
|
|
198
|
+
else if (b.default_action === 'set' && (!expr || expr.length > 200 || /;|--|\/\*/.test(expr))) problem = 'A default is one expression (at most 200 characters, without ; or comments), e.g. now(), 0 or \'new\'.';
|
|
199
|
+
else {
|
|
200
|
+
if (type) parts.push(`alter column ${c} type ${type} using ${c}::${type}`);
|
|
201
|
+
if (b.nullable === 'not_null') parts.push(`alter column ${c} set not null`);
|
|
202
|
+
else if (b.nullable === 'null') parts.push(`alter column ${c} drop not null`);
|
|
203
|
+
if (b.default_action === 'set') parts.push(`alter column ${c} set default ${expr}`);
|
|
204
|
+
else if (b.default_action === 'drop') parts.push(`alter column ${c} drop default`);
|
|
205
|
+
if (!parts.length) problem = 'Nothing to change: choose a new type, nulls or a default.';
|
|
206
|
+
else sql = `alter table ${table.q}\n ${parts.join(',\n ')};`;
|
|
207
|
+
}
|
|
164
208
|
} else if (b.action === 'drop') {
|
|
165
209
|
if (!cols.includes(col)) problem = 'Choose a column of the table.';
|
|
166
210
|
else sql = `alter table ${table.q} drop column ${ident(col)};`;
|
|
@@ -210,7 +254,7 @@ async function objectDetail(db: Awaited<ReturnType<typeof workshopDb>>, s: { csr
|
|
|
210
254
|
const table = (rows: Record<string, unknown>[], empty = 'None.') =>
|
|
211
255
|
rows.length
|
|
212
256
|
? html`<div class="table-wrap"><table class="report"><thead><tr>${Object.keys(rows[0]).map((k) => html`<th scope="col">${k}</th>`)}</tr></thead>
|
|
213
|
-
<tbody>${rows.map((r) => html`<tr>${Object.values(r).map((v) => html`<td>${v === null ?
|
|
257
|
+
<tbody>${rows.map((r) => html`<tr>${Object.values(r).map((v) => html`<td>${v === null ? '' : typeof v === 'object' ? JSON.stringify(v) : String(v)}</td>`)}</tr>`)}</tbody></table></div>`
|
|
214
258
|
: html`<p class="muted">${empty}</p>`;
|
|
215
259
|
const ddlOf = async (match: (x: { kind: string; name: string; table?: string }) => boolean) => {
|
|
216
260
|
const objects = await readSchema(db.pool as pg.Pool, schema).catch(() => []);
|
|
@@ -247,7 +291,14 @@ async function objectDetail(db: Awaited<ReturnType<typeof workshopDb>>, s: { csr
|
|
|
247
291
|
const isTable = o.type === 'table';
|
|
248
292
|
const tabs = TABLE_TABS.filter(([k]) => isTable || !['indexes', 'constraints', 'triggers', 'rls'].includes(k) || (o.type === 'matview' && k === 'indexes'));
|
|
249
293
|
const tab = tabs.some(([k]) => k === tabAsked) ? tabAsked : 'columns';
|
|
250
|
-
|
|
294
|
+
// (sprint 89, SQL-4) the tabs on one row: the main ones and the current one, the rest under More
|
|
295
|
+
const MAIN = ['columns', 'data', 'indexes', 'constraints', 'ddl'];
|
|
296
|
+
const tabLink = ([k, label]: readonly [string, string]) => html`<a href="${link({ k: o.type, o: o.ref, tab: k })}"${k === tab ? raw(' aria-current="page"') : ''}>${label}</a>`;
|
|
297
|
+
const shownTabs = tabs.filter(([k]) => MAIN.includes(k) || k === tab);
|
|
298
|
+
const moreTabs = tabs.filter(([k]) => !shownTabs.some(([x]) => x === k));
|
|
299
|
+
const nav = html`<nav class="settings-tabs ob-tabs" aria-label="${o.name}">${shownTabs.map(tabLink)}${moreTabs.length
|
|
300
|
+
? html`<details class="menu ob-more"><summary>More ${icon('chevron-down')}</summary><div class="menu-panel"><div class="menu-section menu-links">${moreTabs.map(tabLink)}</div></div></details>`
|
|
301
|
+
: ''}</nav>`;
|
|
251
302
|
let body: Raw;
|
|
252
303
|
switch (tab) {
|
|
253
304
|
case 'columns': {
|
|
@@ -262,20 +313,32 @@ async function objectDetail(db: Awaited<ReturnType<typeof workshopDb>>, s: { csr
|
|
|
262
313
|
).rows;
|
|
263
314
|
const names = cols.map((c) => String(c.column));
|
|
264
315
|
const hidden = html`${raw(`<input type="hidden" name="__csrf" value="${s.csrf_token}">`)}<input type="hidden" name="table" value="${o.ref}"><input type="hidden" name="schema" value="${schema}">`;
|
|
265
|
-
const pick = (id: string) => html`<label class="
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
<
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
<
|
|
275
|
-
<
|
|
276
|
-
|
|
316
|
+
const pick = (id: string) => html`<label class="label" for="${id}">Column</label><select id="${id}" name="column">${names.map((n) => html`<option>${n}</option>`)}</select>`;
|
|
317
|
+
// (sprint 89, SQL-4; APEX: the Columns toolbar) Add, Modify, Rename and Drop above the columns, each a small form
|
|
318
|
+
body = html`${isTable
|
|
319
|
+
? html`<div class="ob-toolbar" role="toolbar" aria-label="Columns">
|
|
320
|
+
<details class="menu ob-tool"><summary class="btn btn-sm">${icon('plus')} Add column</summary><div class="menu-panel">
|
|
321
|
+
<form method="post" action="${BASE}/sql/objects/alter" class="ob-action">${hidden}<input type="hidden" name="action" value="add">
|
|
322
|
+
<label class="label" for="ob-add-name">Name</label><input id="ob-add-name" name="column" placeholder="name" required>
|
|
323
|
+
<label class="label" for="ob-add-type">Type</label><input id="ob-add-type" name="type" placeholder="text" required>
|
|
324
|
+
<label class="check"><input type="checkbox" name="not_null" value="true"> not null</label><button class="btn btn-sm btn-hot">Show the SQL</button></form></div></details>
|
|
325
|
+
<details class="menu ob-tool"><summary class="btn btn-sm">${icon('edit')} Modify column</summary><div class="menu-panel">
|
|
326
|
+
<form method="post" action="${BASE}/sql/objects/alter" class="ob-action">${hidden}<input type="hidden" name="action" value="modify">
|
|
327
|
+
${pick('ob-mod-col')}
|
|
328
|
+
<label class="label" for="ob-mod-type">New type</label><input id="ob-mod-type" name="type" placeholder="the same">
|
|
329
|
+
<label class="label" for="ob-mod-null">Nulls</label><select id="ob-mod-null" name="nullable"><option value="">the same</option><option value="null">allowed</option><option value="not_null">not null</option></select>
|
|
330
|
+
<label class="label" for="ob-mod-def">Default</label><select id="ob-mod-def" name="default_action"><option value="">the same</option><option value="set">set to</option><option value="drop">none</option></select>
|
|
331
|
+
<label class="sr-only" for="ob-mod-expr">Default expression</label><input id="ob-mod-expr" name="default" placeholder="now(), 0, 'x'">
|
|
332
|
+
<button class="btn btn-sm btn-hot">Show the SQL</button></form></div></details>
|
|
333
|
+
<details class="menu ob-tool"><summary class="btn btn-sm">${icon('tag')} Rename column</summary><div class="menu-panel">
|
|
334
|
+
<form method="post" action="${BASE}/sql/objects/alter" class="ob-action">${hidden}<input type="hidden" name="action" value="rename">
|
|
335
|
+
${pick('ob-ren-col')}<label class="label" for="ob-ren-to">New name</label><input id="ob-ren-to" name="new_name" placeholder="new name" required><button class="btn btn-sm btn-hot">Show the SQL</button></form></div></details>
|
|
336
|
+
<details class="menu ob-tool"><summary class="btn btn-sm">${icon('trash')} Drop column</summary><div class="menu-panel">
|
|
337
|
+
<form method="post" action="${BASE}/sql/objects/alter" class="ob-action">${hidden}<input type="hidden" name="action" value="drop">
|
|
338
|
+
${pick('ob-drop-col')}<button class="btn btn-sm btn-danger">Show the SQL</button></form></div></details>
|
|
339
|
+
<span class="muted ob-toolbar-note">Each writes the ALTER TABLE statement into SQL Commands: review it there and press Run.</span>
|
|
277
340
|
</div>`
|
|
278
|
-
: ''}`;
|
|
341
|
+
: ''}${table(cols)}`;
|
|
279
342
|
break;
|
|
280
343
|
}
|
|
281
344
|
case 'data': {
|
|
@@ -21,6 +21,7 @@ export const REGION_CONFIG_HELP: [string[], string][] = [
|
|
|
21
21
|
[['*'], '{"display_selector":true} puts it in the page\'s display selector'],
|
|
22
22
|
[['report', 'chart', 'cards', 'dynamic', 'tree', 'template_component'], '{"lazy":true} loads it after the page shows, {"cache":{"scope":"user"|"session"|"all","seconds":300}} keeps its HTML (a submit of the page empties it)'],
|
|
23
23
|
[['data_reporter'], '{"sources":[{"id":"orders","label":"Orders","description":"…","schema":"sales","table":"order_v","columns":[{"name":"total","label":"Total","format":"FML999G990D00"}]}],"sharing":true,"share_authz":"SCHEME","page_size":25} (the Data Reporter settings below edit it)'],
|
|
24
|
+
[['form_designer'], '{"sources":[{"id":"orders","label":"Orders","description":"…","schema":"sales","table":"orders","key":"id","columns":[{"name":"customer","label":"Customer"},{"name":"status","lov":"LOV:ORDER_STATUS"}],"insert":true,"update":true,"delete":false}],"design_authz":"SCHEME","page_size":25} (the Form Designer region\'s Settings; users that pass design_authz design forms)'],
|
|
24
25
|
[['ai_assistant'], '{"service":"NAME","system":"…","welcome":"…","context":[{"name":"policy","sql":"select … :AI_PROMPT …"}],"tools":[{"name":"my_leave","description":"…","sql":"select … where status = :STATUS","parameters":{"STATUS":{"type":"string","enum":["PENDING"]}}},{"name":"dept","type":"rest","source":"DEPARTMENT","description":"…"}],"max_rounds":5,"max_turns":20} (the AI assistant settings below edit it)'],
|
|
25
26
|
[['report'], '{"ai_filter":{"service":"NAME"}} adds an "Ask in your own words" box (natural-language filters)'],
|
|
26
27
|
[['plugin'], '{"plugin":"name","attributes":{"NAME":"value"}} (a region plug-in; a Source SELECT feeds its template)'],
|
|
@@ -45,6 +46,7 @@ const ANCHORS: Record<string, string> = {
|
|
|
45
46
|
tree: 'tree',
|
|
46
47
|
list: 'list-lists',
|
|
47
48
|
data_reporter: 'data-reporter-data-reporter',
|
|
49
|
+
form_designer: 'form_designer-form-designer',
|
|
48
50
|
ai_assistant: 'ai-assistant-ai-assistant',
|
|
49
51
|
template_component: 'template-components',
|
|
50
52
|
static: 'static-and-dynamic-content',
|
|
@@ -16,6 +16,7 @@ import { LIST_TEMPLATES } from '../runtime/lists.ts';
|
|
|
16
16
|
import { MAX_EXTRA_LAYERS } from '../runtime/maps.ts';
|
|
17
17
|
import { postgis } from '../runtime/spatial.ts';
|
|
18
18
|
import { reporterSettingsForm } from './reporter.ts';
|
|
19
|
+
import { formDesignerSettingsForm } from './form-designer.ts';
|
|
19
20
|
|
|
20
21
|
// Page designer → a region → Settings: the region's "config" JSON as a form
|
|
21
22
|
// for grid, chart, cards, calendar and faceted search regions (report
|
|
@@ -732,6 +733,7 @@ export async function regionSettingsForm(pageId: number, appId: number, r: Regio
|
|
|
732
733
|
if (r.type === 'ai_assistant') return assistantSettingsForm(pageId, appId, r, s);
|
|
733
734
|
if (r.type === 'template_component') return templateRegionForm(pageId, appId, r, s);
|
|
734
735
|
if (r.type === 'data_reporter') return reporterSettingsForm(pageId, appId, r, s);
|
|
736
|
+
if (r.type === 'form_designer') return formDesignerSettingsForm(pageId, appId, r, s);
|
|
735
737
|
if (!(SETTINGS_TYPES as readonly string[]).includes(r.type)) return '';
|
|
736
738
|
const cfg = r.config ?? {};
|
|
737
739
|
const id = (n: string) => `rg_${r.id}_${n}`;
|
package/src/builder/reporter.ts
CHANGED
|
@@ -43,7 +43,7 @@ async function objectByOid(oid: number): Promise<DbObject | null> {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
/** The tables and views to choose from, with whether the application's role may read them. */
|
|
46
|
-
async function objectList(dbRole: string | null) {
|
|
46
|
+
export async function objectList(dbRole: string | null) {
|
|
47
47
|
return (
|
|
48
48
|
await owner.query<{ oid: number; name: string; access: boolean }>(
|
|
49
49
|
`select c.oid::int as oid, format('%I.%I', n.nspname, c.relname) as name,
|
package/src/builder/routes.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { documentShell } from '../layout.ts';
|
|
|
13
13
|
import { passwordProblem } from '../accounts.ts';
|
|
14
14
|
import { clientIp, createSession, destroySession, getSession, loginThrottled, logActivity, saveState, takeFlash } from '../session.ts';
|
|
15
15
|
import { loginSettingsFields, saveLoginSettings } from './login-page.ts';
|
|
16
|
-
import { APP_COLORS, appHeader, back, BASE, builderHead, csrf, developer, flash, input, region, select, send, shell, THEME_COOKIE, validTheme, type Req } from './ui.ts';
|
|
16
|
+
import { APP_COLORS, appHeader, back, BASE, designerUrl, builderHead, csrf, developer, flash, input, region, select, send, shell, THEME_COOKIE, validTheme, type Req } from './ui.ts';
|
|
17
17
|
import { appOr404 } from './forms.ts';
|
|
18
18
|
import { docToFiles, filesToZip } from '../appfiles.ts';
|
|
19
19
|
import { homeRoutes, rememberApp } from './home.ts';
|
|
@@ -278,11 +278,11 @@ export async function builderRoutes(app: FastifyInstance) {
|
|
|
278
278
|
<select id="f_group" name="group">${[['', 'All pages'], ...groups.map((g) => [g, g]), ['-', 'Without a group']].map(([v, l]) => html`<option value="${v}"${v === groupFilter ? raw(' selected') : ''}>${l}</option>`)}</select></div>
|
|
279
279
|
<button class="btn u-selfend">Show</button></form>`
|
|
280
280
|
: ''}${icons
|
|
281
|
-
? html`<ul class="page-icons" id="pages-rows">${shownPages.map((p) => html`<li><a href="${
|
|
281
|
+
? html`<ul class="page-icons" id="pages-rows">${shownPages.map((p) => html`<li><a href="${designerUrl(a.id, p.page_no)}" class="page-icon"><span class="page-icon-no">${p.page_no}</span><b>${p.name}</b><small class="muted">${p.mode}, ${p.regions} region${p.regions === 1 ? '' : 's'}</small></a></li>`)}</ul>`
|
|
282
282
|
: html`<div class="table-wrap"><table class="report">
|
|
283
283
|
<thead><tr><th class="num">Page</th><th>Name</th>${grouped ? html`<th>Group</th>` : ''}<th>Mode</th><th class="num">Regions</th><th class="num">Items</th><th class="num">DAs</th><th class="num">Processes</th><th>Authorization</th><th>Protection</th><th>Lock</th><th></th></tr></thead>
|
|
284
284
|
<tbody id="pages-rows">${shownPages.map((p) => html`<tr>
|
|
285
|
-
<td class="num">${p.page_no}</td><td><a href="${
|
|
285
|
+
<td class="num">${p.page_no}</td><td><a href="${designerUrl(a.id, p.page_no)}">${p.name}</a>${commentsPerPage.get(p.page_no) ? html` <span class="tag" title="Developer comments">${commentsPerPage.get(p.page_no)} comment${commentsPerPage.get(p.page_no) === 1 ? '' : 's'}</span>` : ''}</td>${grouped ? html`<td>${p.page_group ?? '—'}</td>` : ''}<td>${p.mode}</td>
|
|
286
286
|
<td class="num">${p.regions}</td><td class="num">${p.items}</td><td class="num">${p.das}</td><td class="num">${p.processes}</td>
|
|
287
287
|
<td>${p.requires_auth ? (p.authz ?? '—') : 'public'}</td><td>${p.protection}</td>
|
|
288
288
|
<td>${((l) => (l ? html`<span class="lock-tag" title="${lockText(l)}">${icon('key')}<span>${l.locked_by === s.username ? 'you' : l.locked_by}</span></span>` : '—'))(locks.get(p.page_no))}</td>
|
|
@@ -401,6 +401,7 @@ export async function builderRoutes(app: FastifyInstance) {
|
|
|
401
401
|
<div class="form-grid">
|
|
402
402
|
${input('name', 'Name', a.name, { required: true })}
|
|
403
403
|
${input('alias', 'Alias', a.alias, { required: true })}
|
|
404
|
+
${input('version', 'Version', a.version ?? '', { placeholder: 'Release 1.0', help: 'Shown in the footer of the application\'s pages; empty: none.' })}
|
|
404
405
|
${input('home_page', 'Home page', a.home_page, { type: 'number' })}
|
|
405
406
|
${select('app_type', 'Application type', a.app_type ?? 'standard', APP_TYPES.map((t): [string, string] => [t, APP_TYPE_LABELS[t]]),
|
|
406
407
|
'Theme and library applications offer their theme or shared components to other applications (Shared Components → Subscriptions); Create application can start from a boilerplate application.')}
|
|
@@ -546,7 +547,7 @@ export async function builderRoutes(app: FastifyInstance) {
|
|
|
546
547
|
date_format = $14, timestamp_format = $15, remember_me_days = $16, ldap_directories = $17,
|
|
547
548
|
header_name = $18, header_auto_create = $19, logout_url = $20, db_auth_roles = $21, db_auth_member_of = $22,
|
|
548
549
|
custom_auth_function = $23, custom_auth_code = $24, custom_auth_post_code = $25, nav_list = $26, navbar_list = $27,
|
|
549
|
-
app_type = $28, session_group = $29, updated_at = now() where id = $1`,
|
|
550
|
+
app_type = $28, session_group = $29, version = $30, updated_at = now() where id = $1`,
|
|
550
551
|
[req.params.id, b.name?.trim(), b.alias?.trim().toLowerCase(), Number(b.home_page) || 1, b.authentication, b.db_role?.trim() || null, b.debug === 'true',
|
|
551
552
|
JSON.stringify({
|
|
552
553
|
base: baseStyleOf({ base: b.base }) === 'standard' ? undefined : baseStyleOf({ base: b.base }),
|
|
@@ -579,7 +580,8 @@ export async function builderRoutes(app: FastifyInstance) {
|
|
|
579
580
|
b.nav_list?.trim().toUpperCase() || null,
|
|
580
581
|
b.navbar_list?.trim().toUpperCase() || null,
|
|
581
582
|
(APP_TYPES as readonly string[]).includes(b.app_type ?? '') ? b.app_type : 'standard',
|
|
582
|
-
/^[a-z][a-z0-9_]{0,29}$/.test(b.session_group?.trim().toLowerCase() ?? '') ? b.session_group!.trim().toLowerCase() : null
|
|
583
|
+
/^[a-z][a-z0-9_]{0,29}$/.test(b.session_group?.trim().toLowerCase() ?? '') ? b.session_group!.trim().toLowerCase() : null,
|
|
584
|
+
(b.version ?? '').trim().slice(0, 40) || null],
|
|
583
585
|
);
|
|
584
586
|
await saveTimeZoneSettings(req.params.id, b);
|
|
585
587
|
await saveLoginSettings(req.params.id, b);
|
package/src/builder/search.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { owner } from '../db.ts';
|
|
|
3
3
|
import { html, type Raw } from '../html.ts';
|
|
4
4
|
import { icon } from '../icons.ts';
|
|
5
5
|
import { COMPONENTS, type FieldKind } from './components.ts';
|
|
6
|
-
import { appHeader, BASE, developer, region, send, shell, type Req } from './ui.ts';
|
|
6
|
+
import { appHeader, BASE, designerUrl, developer, region, send, shell, type Req } from './ui.ts';
|
|
7
7
|
|
|
8
8
|
// App Builder → Search and "Where used": every component of an application
|
|
9
9
|
// (pages, regions, items, buttons, dynamic actions, validations, processes
|
|
@@ -46,7 +46,7 @@ export async function appEntries(appId: number, query: (sql: string, params?: un
|
|
|
46
46
|
const pageById = new Map(pages.map((p) => [p.id, p]));
|
|
47
47
|
const out: Entry[] = pages.map((p) => ({
|
|
48
48
|
kind: 'page', id: p.id, label: `Page ${p.page_no}: ${p.name}`, kindLabel: 'Page', icon: 'file', pageNo: p.page_no,
|
|
49
|
-
url:
|
|
49
|
+
url: designerUrl(appId, p.page_no, 'c=page'),
|
|
50
50
|
fields: PAGE_FIELDS.map((f) => ({ ...f, value: asText(p[f.name]) })),
|
|
51
51
|
}));
|
|
52
52
|
for (const [kind, spec] of Object.entries(COMPONENTS)) {
|
|
@@ -57,7 +57,7 @@ export async function appEntries(appId: number, query: (sql: string, params?: un
|
|
|
57
57
|
const page = spec.scope === 'page' ? pageById.get(row.page_id) : null;
|
|
58
58
|
out.push({
|
|
59
59
|
kind, id: row.id, label: spec.summary(row), kindLabel: spec.label, icon: spec.icon, pageNo: page?.page_no ?? null,
|
|
60
|
-
url: page ?
|
|
60
|
+
url: page ? designerUrl(appId, page.page_no, `c=${kind}-${row.id}`) : `${BASE}/apps/${appId}/shared?c=${kind}-${row.id}`,
|
|
61
61
|
fields: spec.fields.filter((f) => TEXT_KINDS.has(f.kind)).map((f) => ({ name: f.name, label: f.label, kind: f.kind, value: asText(row[f.name]) })),
|
|
62
62
|
});
|
|
63
63
|
}
|
package/src/builder/shared.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { layoutExtras } from './layouts.ts';
|
|
|
13
13
|
import { templateExtras, templateImport } from './templates.ts';
|
|
14
14
|
import { credentialExtras, needsSecret, restSourceExtras } from './websources.ts';
|
|
15
15
|
import { dataLoadDefExtras } from './dataload.ts';
|
|
16
|
-
import { appHeader, back, BASE, csrf, developer, flash, input, region, select, send, shell, type Req } from './ui.ts';
|
|
16
|
+
import { appHeader, back, BASE, designerUrl, csrf, developer, flash, input, region, select, send, shell, type Req } from './ui.ts';
|
|
17
17
|
import { endSessions, grantAccess, roleHints, roleHintsHtml, splitRoles } from './users.ts';
|
|
18
18
|
import { appOr404, componentForm, lookups, saveComponent } from './forms.ts';
|
|
19
19
|
import { catalogPicker } from './rest-catalogs.ts';
|
|
@@ -89,7 +89,7 @@ export async function sharedRoutes(app: FastifyInstance) {
|
|
|
89
89
|
if (!kids.length) return html``;
|
|
90
90
|
return html`<ul class="tree">${kids.map((p) => {
|
|
91
91
|
const next = new Set(seen).add(p.page_no);
|
|
92
|
-
return html`<li><a href="${
|
|
92
|
+
return html`<li><a href="${designerUrl(a.id, p.page_no)}">${icon('file')}<span>${p.name}</span><span class="kind">p${p.page_no}</span></a>${tree(p.page_no, next)}</li>`;
|
|
93
93
|
})}</ul>`;
|
|
94
94
|
};
|
|
95
95
|
const main = html`${appHeader(a, 'shared')}
|
package/src/builder/ui.ts
CHANGED
|
@@ -12,6 +12,8 @@ import { ensureDevbar } from '../runtime/devbar.ts';
|
|
|
12
12
|
// Shared building blocks of the builder UI.
|
|
13
13
|
|
|
14
14
|
export const BASE = '/builder';
|
|
15
|
+
/** (sprint 86) The page designer's address: the application's id and the page's number (/builder/pages/:id redirects here). */
|
|
16
|
+
export const designerUrl = (appId: number, pageNo: number, query = '') => `${BASE}/apps/${appId}/pages/${pageNo}${query ? `?${query}` : ''}`;
|
|
15
17
|
|
|
16
18
|
/** pgkiln's version and the database name, for the builder's status bar. */
|
|
17
19
|
const VERSION = (JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8')) as { version: string }).version;
|
package/src/cli/replace.ts
CHANGED
|
@@ -51,6 +51,8 @@ export const KEPT = ['app_access', 'api_client', 'session', 'sso_pending', 'save
|
|
|
51
51
|
'subscription',
|
|
52
52
|
// (057) users' Data Reporter reports (user data, like saved reports)
|
|
53
53
|
'data_report',
|
|
54
|
+
// (106) Form Designer forms, built by the application's administrators (installation data, like reports)
|
|
55
|
+
'user_form',
|
|
54
56
|
// (060) which AI services an application may use (installation data), its AI usage log and AI requests from SQL
|
|
55
57
|
'app_ai_service', 'ai_usage', 'ai_request',
|
|
56
58
|
// (061) AI assistant conversations (per session, follow their region)
|
|
@@ -79,6 +81,8 @@ const REPOINTED = ['saved_report.region_id', 'task.definition_id', 'workflow.def
|
|
|
79
81
|
'rest_sync_log.source_id',
|
|
80
82
|
// (057) Data Reporter reports follow their region like saved reports
|
|
81
83
|
'data_report.region_id',
|
|
84
|
+
// (106) Form Designer forms follow their region
|
|
85
|
+
'user_form.region_id',
|
|
82
86
|
// (061) AI assistant conversations follow their region
|
|
83
87
|
'ai_conversation.region_id'];
|
|
84
88
|
|
|
@@ -201,6 +205,12 @@ export async function replaceApp(db: Db, doc: unknown, alias: string, opts: Repl
|
|
|
201
205
|
from unnest($1::int[], $2::int[]) as m(old, neu) where s.region_id = m.old`,
|
|
202
206
|
[pairs.map((p) => p[0]), pairs.map((p) => p[1])],
|
|
203
207
|
);
|
|
208
|
+
if (pairs.length)
|
|
209
|
+
await db.query(
|
|
210
|
+
`update meta.user_form s set region_id = m.neu
|
|
211
|
+
from unnest($1::int[], $2::int[]) as m(old, neu) where s.region_id = m.old`,
|
|
212
|
+
[pairs.map((p) => p[0]), pairs.map((p) => p[1])],
|
|
213
|
+
);
|
|
204
214
|
if (pairs.length)
|
|
205
215
|
await db.query(
|
|
206
216
|
`update meta.ai_conversation s set region_id = m.neu
|
package/src/i18n/ar.ts
CHANGED
|
@@ -63,6 +63,7 @@ export const ar: Record<MessageKey, string> = {
|
|
|
63
63
|
'common.back': "رجوع",
|
|
64
64
|
'common.home': "الانتقال إلى الصفحة الرئيسية",
|
|
65
65
|
'common.skip': "تخطَّ إلى المحتوى",
|
|
66
|
+
'common.back_to_top': "العودة إلى الأعلى",
|
|
66
67
|
'common.toggle_nav': "إظهار التنقل أو إخفاؤه",
|
|
67
68
|
'common.dismiss': "إغلاق",
|
|
68
69
|
// messages and errors
|
|
@@ -390,6 +391,9 @@ export const ar: Record<MessageKey, string> = {
|
|
|
390
391
|
'facets.stars_up': "{n} نجوم فأكثر",
|
|
391
392
|
'facets.star_up': "نجمة واحدة فأكثر",
|
|
392
393
|
'facets.clear_short': "مسح",
|
|
394
|
+
'facets.menu': "خيارات {facet}",
|
|
395
|
+
'facets.sort_count': "الترتيب حسب العدد",
|
|
396
|
+
'facets.sort_name': "الترتيب حسب الاسم",
|
|
393
397
|
'facets.show_all': "عرض الكل ({n})",
|
|
394
398
|
'facets.total': "إجمالي عدد الصفوف {n}",
|
|
395
399
|
'smart.placeholder': "بحث أو تصفية…",
|
|
@@ -685,6 +689,64 @@ export const ar: Record<MessageKey, string> = {
|
|
|
685
689
|
'reporter.changed': "عُدِّل في {date}",
|
|
686
690
|
'reporter.no_match': "لا توجد تقارير مطابقة للبحث.",
|
|
687
691
|
'reporter.chart_tag': "مخطط",
|
|
692
|
+
// Form Designer (form-designer.ts)
|
|
693
|
+
'formdesigner.no_sources': "لم تُعدّ أي جداول لهذه المنطقة بعد.",
|
|
694
|
+
'formdesigner.no_forms': "لا توجد نماذج بعد.",
|
|
695
|
+
'formdesigner.new': "نموذج جديد",
|
|
696
|
+
'formdesigner.create': "تصميم",
|
|
697
|
+
'formdesigner.draft': "مسودة",
|
|
698
|
+
'formdesigner.design': "تصميم النموذج",
|
|
699
|
+
'formdesigner.design_of': "تصميم {name}",
|
|
700
|
+
'formdesigner.back': "كل النماذج",
|
|
701
|
+
'formdesigner.add': "إضافة",
|
|
702
|
+
'formdesigner.edit_row': "تعديل",
|
|
703
|
+
'formdesigner.rows': "صفوف {form}",
|
|
704
|
+
'formdesigner.save': "حفظ",
|
|
705
|
+
'formdesigner.delete': "حذف",
|
|
706
|
+
'formdesigner.delete_confirm': "حذف هذا الصف؟",
|
|
707
|
+
'formdesigner.cancel': "إلغاء",
|
|
708
|
+
'formdesigner.saved_row': "تم الحفظ.",
|
|
709
|
+
'formdesigner.deleted_row': "تم الحذف.",
|
|
710
|
+
'formdesigner.required': "الحقل {field} مطلوب.",
|
|
711
|
+
'formdesigner.not_in_list': "{field}: اختر قيمة من القائمة.",
|
|
712
|
+
'formdesigner.changed': "غيّر شخص آخر هذا الصف في هذه الأثناء. راجعه واحفظ مرة أخرى.",
|
|
713
|
+
'formdesigner.row_not_found': "هذا الصف غير موجود، أو لا يُسمح لك برؤيته.",
|
|
714
|
+
'formdesigner.not_allowed': "لا يُسمح لك بفعل ذلك بهذا الصف.",
|
|
715
|
+
'formdesigner.form_not_found': "هذا النموذج غير موجود (بعد الآن).",
|
|
716
|
+
'formdesigner.unknown_source': "لم يعد جدول هذا النموذج معروضًا.",
|
|
717
|
+
'formdesigner.nothing': "لا يحتوي هذا النموذج على حقول بعد.",
|
|
718
|
+
'formdesigner.choose': "- اختر -",
|
|
719
|
+
'formdesigner.name': "الاسم",
|
|
720
|
+
'formdesigner.description': "الوصف",
|
|
721
|
+
'formdesigner.published': "منشور: يمكن لكل من يرى هذه الصفحة استخدام النموذج",
|
|
722
|
+
'formdesigner.fields': "الحقول",
|
|
723
|
+
'formdesigner.field_on': "في النموذج",
|
|
724
|
+
'formdesigner.column': "العمود",
|
|
725
|
+
'formdesigner.key': "مفتاح",
|
|
726
|
+
'formdesigner.order': "الترتيب",
|
|
727
|
+
'formdesigner.label': "التسمية",
|
|
728
|
+
'formdesigner.help': "نص المساعدة",
|
|
729
|
+
'formdesigner.required_short': "مطلوب",
|
|
730
|
+
'formdesigner.display': "العرض",
|
|
731
|
+
'formdesigner.default': "الافتراضي",
|
|
732
|
+
'formdesigner.in_list': "في القائمة",
|
|
733
|
+
'formdesigner.display_auto': "حقل",
|
|
734
|
+
'formdesigner.display_textarea': "منطقة نص",
|
|
735
|
+
'formdesigner.display_select': "قائمة اختيار",
|
|
736
|
+
'formdesigner.display_radio': "مجموعة خيارات",
|
|
737
|
+
'formdesigner.actions': "يمكن للمستخدمين",
|
|
738
|
+
'formdesigner.allow_insert': "إضافة صفوف",
|
|
739
|
+
'formdesigner.allow_update': "تغيير صفوف",
|
|
740
|
+
'formdesigner.allow_delete': "حذف صفوف",
|
|
741
|
+
'formdesigner.list_help': "تعرض قائمة الصفوف الأعمدة المحددة «في القائمة»؛ وبدونها يضيف النموذج صفوفًا فقط.",
|
|
742
|
+
'formdesigner.save_form': "حفظ النموذج",
|
|
743
|
+
'formdesigner.delete_form': "حذف النموذج",
|
|
744
|
+
'formdesigner.delete_form_confirm': "حذف النموذج «{name}»؟ تبقى الصفوف التي حفظها.",
|
|
745
|
+
'formdesigner.form_saved': "تم حفظ النموذج «{name}».",
|
|
746
|
+
'formdesigner.form_deleted': "تم حذف النموذج.",
|
|
747
|
+
'formdesigner.name_required': "أعطِ النموذج اسمًا.",
|
|
748
|
+
'formdesigner.no_fields': "ضع عمودًا واحدًا على الأقل في النموذج.",
|
|
749
|
+
'formdesigner.changed_by': "غيّره {user} في {date}",
|
|
688
750
|
// AI assistant region and natural-language report filters (assistant.ts, ai-filter.ts)
|
|
689
751
|
'assistant.conversation': "المحادثة",
|
|
690
752
|
'assistant.you': "أنت",
|
package/src/i18n/cs.ts
CHANGED
|
@@ -63,6 +63,7 @@ export const cs: Record<MessageKey, string> = {
|
|
|
63
63
|
'common.back': "Zpět",
|
|
64
64
|
'common.home': "Na úvodní stránku",
|
|
65
65
|
'common.skip': "Přejít na obsah",
|
|
66
|
+
'common.back_to_top': "Zpět nahoru",
|
|
66
67
|
'common.toggle_nav': "Zobrazit nebo skrýt navigaci",
|
|
67
68
|
'common.dismiss': "Zavřít",
|
|
68
69
|
// messages and errors
|
|
@@ -390,6 +391,9 @@ export const cs: Record<MessageKey, string> = {
|
|
|
390
391
|
'facets.stars_up': "{n} hvězdičky a více",
|
|
391
392
|
'facets.star_up': "1 hvězdička a více",
|
|
392
393
|
'facets.clear_short': "Vymazat",
|
|
394
|
+
'facets.menu': "Možnosti: {facet}",
|
|
395
|
+
'facets.sort_count': "Seřadit podle počtu",
|
|
396
|
+
'facets.sort_name': "Seřadit podle názvu",
|
|
393
397
|
'facets.show_all': "Zobrazit vše ({n})",
|
|
394
398
|
'facets.total': "Celkový počet řádků {n}",
|
|
395
399
|
'smart.placeholder': "Hledat nebo filtrovat…",
|
|
@@ -685,6 +689,64 @@ export const cs: Record<MessageKey, string> = {
|
|
|
685
689
|
'reporter.changed': "změněno {date}",
|
|
686
690
|
'reporter.no_match': "Hledání neodpovídá žádná sestava.",
|
|
687
691
|
'reporter.chart_tag': "graf",
|
|
692
|
+
// Form Designer (form-designer.ts)
|
|
693
|
+
'formdesigner.no_sources': "Pro tuto oblast zatím nejsou nastaveny žádné tabulky.",
|
|
694
|
+
'formdesigner.no_forms': "Zatím nejsou žádné formuláře.",
|
|
695
|
+
'formdesigner.new': "Nový formulář",
|
|
696
|
+
'formdesigner.create': "Navrhnout",
|
|
697
|
+
'formdesigner.draft': "Koncept",
|
|
698
|
+
'formdesigner.design': "Navrhnout formulář",
|
|
699
|
+
'formdesigner.design_of': "Návrh: {name}",
|
|
700
|
+
'formdesigner.back': "Všechny formuláře",
|
|
701
|
+
'formdesigner.add': "Přidat",
|
|
702
|
+
'formdesigner.edit_row': "Upravit",
|
|
703
|
+
'formdesigner.rows': "Řádky: {form}",
|
|
704
|
+
'formdesigner.save': "Uložit",
|
|
705
|
+
'formdesigner.delete': "Smazat",
|
|
706
|
+
'formdesigner.delete_confirm': "Smazat tento řádek?",
|
|
707
|
+
'formdesigner.cancel': "Zrušit",
|
|
708
|
+
'formdesigner.saved_row': "Uloženo.",
|
|
709
|
+
'formdesigner.deleted_row': "Smazáno.",
|
|
710
|
+
'formdesigner.required': "{field} je povinné.",
|
|
711
|
+
'formdesigner.not_in_list': "{field}: vyberte hodnotu ze seznamu.",
|
|
712
|
+
'formdesigner.changed': "Někdo jiný mezitím tento řádek změnil. Zkontrolujte ho a uložte znovu.",
|
|
713
|
+
'formdesigner.row_not_found': "Tento řádek neexistuje, nebo ho nesmíte vidět.",
|
|
714
|
+
'formdesigner.not_allowed': "To s tímto řádkem dělat nesmíte.",
|
|
715
|
+
'formdesigner.form_not_found': "Tento formulář neexistuje (už).",
|
|
716
|
+
'formdesigner.unknown_source': "Tabulka tohoto formuláře se už nenabízí.",
|
|
717
|
+
'formdesigner.nothing': "Tento formulář zatím nemá žádná pole.",
|
|
718
|
+
'formdesigner.choose': "- Vyberte -",
|
|
719
|
+
'formdesigner.name': "Název",
|
|
720
|
+
'formdesigner.description': "Popis",
|
|
721
|
+
'formdesigner.published': "Zveřejněno: formulář může používat každý, kdo vidí tuto stránku",
|
|
722
|
+
'formdesigner.fields': "Pole",
|
|
723
|
+
'formdesigner.field_on': "Ve formuláři",
|
|
724
|
+
'formdesigner.column': "Sloupec",
|
|
725
|
+
'formdesigner.key': "klíč",
|
|
726
|
+
'formdesigner.order': "Pořadí",
|
|
727
|
+
'formdesigner.label': "Popisek",
|
|
728
|
+
'formdesigner.help': "Nápověda",
|
|
729
|
+
'formdesigner.required_short': "Povinné",
|
|
730
|
+
'formdesigner.display': "Zobrazení",
|
|
731
|
+
'formdesigner.default': "Výchozí",
|
|
732
|
+
'formdesigner.in_list': "V seznamu",
|
|
733
|
+
'formdesigner.display_auto': "Pole",
|
|
734
|
+
'formdesigner.display_textarea': "Textová oblast",
|
|
735
|
+
'formdesigner.display_select': "Výběrový seznam",
|
|
736
|
+
'formdesigner.display_radio': "Přepínače",
|
|
737
|
+
'formdesigner.actions': "Uživatelé smějí",
|
|
738
|
+
'formdesigner.allow_insert': "přidávat řádky",
|
|
739
|
+
'formdesigner.allow_update': "měnit řádky",
|
|
740
|
+
'formdesigner.allow_delete': "mazat řádky",
|
|
741
|
+
'formdesigner.list_help': "Seznam řádků ukazuje sloupce zaškrtnuté „V seznamu“; bez nich formulář jen přidává řádky.",
|
|
742
|
+
'formdesigner.save_form': "Uložit formulář",
|
|
743
|
+
'formdesigner.delete_form': "Smazat formulář",
|
|
744
|
+
'formdesigner.delete_form_confirm': "Smazat formulář „{name}“? Řádky jím uložené zůstanou.",
|
|
745
|
+
'formdesigner.form_saved': "Formulář „{name}“ uložen.",
|
|
746
|
+
'formdesigner.form_deleted': "Formulář smazán.",
|
|
747
|
+
'formdesigner.name_required': "Pojmenujte formulář.",
|
|
748
|
+
'formdesigner.no_fields': "Dejte do formuláře alespoň jeden sloupec.",
|
|
749
|
+
'formdesigner.changed_by': "změnil(a) {user} dne {date}",
|
|
688
750
|
// AI assistant region and natural-language report filters (assistant.ts, ai-filter.ts)
|
|
689
751
|
'assistant.conversation': "Konverzace",
|
|
690
752
|
'assistant.you': "Vy",
|
package/src/i18n/da.ts
CHANGED
|
@@ -63,6 +63,7 @@ export const da: Record<MessageKey, string> = {
|
|
|
63
63
|
'common.back': "Tilbage",
|
|
64
64
|
'common.home': "Gå til forsiden",
|
|
65
65
|
'common.skip': "Gå til indholdet",
|
|
66
|
+
'common.back_to_top': "Til toppen",
|
|
66
67
|
'common.toggle_nav': "Vis eller skjul navigationen",
|
|
67
68
|
'common.dismiss': "Luk",
|
|
68
69
|
// messages and errors
|
|
@@ -390,6 +391,9 @@ export const da: Record<MessageKey, string> = {
|
|
|
390
391
|
'facets.stars_up': "{n} stjerner og op",
|
|
391
392
|
'facets.star_up': "1 stjerne og op",
|
|
392
393
|
'facets.clear_short': "Ryd",
|
|
394
|
+
'facets.menu': "Indstillinger for {facet}",
|
|
395
|
+
'facets.sort_count': "Sortér efter antal",
|
|
396
|
+
'facets.sort_name': "Sortér efter navn",
|
|
393
397
|
'facets.show_all': "Vis alle ({n})",
|
|
394
398
|
'facets.total': "Antal rækker i alt {n}",
|
|
395
399
|
'smart.placeholder': "Søg eller filtrér…",
|
|
@@ -685,6 +689,64 @@ export const da: Record<MessageKey, string> = {
|
|
|
685
689
|
'reporter.changed': "ændret {date}",
|
|
686
690
|
'reporter.no_match': "Ingen rapporter matcher søgningen.",
|
|
687
691
|
'reporter.chart_tag': "diagram",
|
|
692
|
+
// Form Designer (form-designer.ts)
|
|
693
|
+
'formdesigner.no_sources': "Der er endnu ikke sat tabeller op for denne region.",
|
|
694
|
+
'formdesigner.no_forms': "Der er endnu ingen formularer.",
|
|
695
|
+
'formdesigner.new': "Ny formular",
|
|
696
|
+
'formdesigner.create': "Design",
|
|
697
|
+
'formdesigner.draft': "Kladde",
|
|
698
|
+
'formdesigner.design': "Design formular",
|
|
699
|
+
'formdesigner.design_of': "Design af {name}",
|
|
700
|
+
'formdesigner.back': "Alle formularer",
|
|
701
|
+
'formdesigner.add': "Tilføj",
|
|
702
|
+
'formdesigner.edit_row': "Rediger",
|
|
703
|
+
'formdesigner.rows': "Rækker i {form}",
|
|
704
|
+
'formdesigner.save': "Gem",
|
|
705
|
+
'formdesigner.delete': "Slet",
|
|
706
|
+
'formdesigner.delete_confirm': "Slet denne række?",
|
|
707
|
+
'formdesigner.cancel': "Annuller",
|
|
708
|
+
'formdesigner.saved_row': "Gemt.",
|
|
709
|
+
'formdesigner.deleted_row': "Slettet.",
|
|
710
|
+
'formdesigner.required': "{field} er påkrævet.",
|
|
711
|
+
'formdesigner.not_in_list': "{field}: vælg en værdi fra listen.",
|
|
712
|
+
'formdesigner.changed': "En anden har ændret denne række i mellemtiden. Kontrollér den, og gem igen.",
|
|
713
|
+
'formdesigner.row_not_found': "Denne række findes ikke, eller du må ikke se den.",
|
|
714
|
+
'formdesigner.not_allowed': "Det må du ikke gøre med denne række.",
|
|
715
|
+
'formdesigner.form_not_found': "Denne formular findes ikke (længere).",
|
|
716
|
+
'formdesigner.unknown_source': "Denne formulars tabel tilbydes ikke længere.",
|
|
717
|
+
'formdesigner.nothing': "Denne formular har endnu ingen felter.",
|
|
718
|
+
'formdesigner.choose': "- Vælg -",
|
|
719
|
+
'formdesigner.name': "Navn",
|
|
720
|
+
'formdesigner.description': "Beskrivelse",
|
|
721
|
+
'formdesigner.published': "Udgivet: alle, der kan se denne side, må bruge formularen",
|
|
722
|
+
'formdesigner.fields': "Felter",
|
|
723
|
+
'formdesigner.field_on': "På formularen",
|
|
724
|
+
'formdesigner.column': "Kolonne",
|
|
725
|
+
'formdesigner.key': "nøgle",
|
|
726
|
+
'formdesigner.order': "Rækkefølge",
|
|
727
|
+
'formdesigner.label': "Etiket",
|
|
728
|
+
'formdesigner.help': "Hjælpetekst",
|
|
729
|
+
'formdesigner.required_short': "Påkrævet",
|
|
730
|
+
'formdesigner.display': "Visning",
|
|
731
|
+
'formdesigner.default': "Standard",
|
|
732
|
+
'formdesigner.in_list': "I listen",
|
|
733
|
+
'formdesigner.display_auto': "Felt",
|
|
734
|
+
'formdesigner.display_textarea': "Tekstområde",
|
|
735
|
+
'formdesigner.display_select': "Valgliste",
|
|
736
|
+
'formdesigner.display_radio': "Radioknapper",
|
|
737
|
+
'formdesigner.actions': "Brugere må",
|
|
738
|
+
'formdesigner.allow_insert': "tilføje rækker",
|
|
739
|
+
'formdesigner.allow_update': "ændre rækker",
|
|
740
|
+
'formdesigner.allow_delete': "slette rækker",
|
|
741
|
+
'formdesigner.list_help': "Listen over rækker viser kolonnerne med flueben i \"I listen\"; uden nogen tilføjer formularen kun rækker.",
|
|
742
|
+
'formdesigner.save_form': "Gem formular",
|
|
743
|
+
'formdesigner.delete_form': "Slet formular",
|
|
744
|
+
'formdesigner.delete_form_confirm': "Slet formularen \"{name}\"? Rækkerne, den har gemt, bliver.",
|
|
745
|
+
'formdesigner.form_saved': "Formularen \"{name}\" er gemt.",
|
|
746
|
+
'formdesigner.form_deleted': "Formularen er slettet.",
|
|
747
|
+
'formdesigner.name_required': "Giv formularen et navn.",
|
|
748
|
+
'formdesigner.no_fields': "Sæt mindst én kolonne på formularen.",
|
|
749
|
+
'formdesigner.changed_by': "ændret af {user} den {date}",
|
|
688
750
|
// AI assistant region and natural-language report filters (assistant.ts, ai-filter.ts)
|
|
689
751
|
'assistant.conversation': "Samtale",
|
|
690
752
|
'assistant.you': "Dig",
|