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
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// (sprint 121, WS-7) The builder in more languages, like APEX's builder (Deutsch, English, Español, Français, …):
|
|
2
|
+
// the builder's own texts are keyed messages in src/builder/i18n/<lang>.ts, the same mechanism as the runtime's
|
|
3
|
+
// texts in src/i18n.ts. English (en.ts) defines the keys; every other language file is a Record of the same keys
|
|
4
|
+
// (TypeScript refuses a missing or an unknown key, test/builder-i18n.test.ts checks the files and the source).
|
|
5
|
+
//
|
|
6
|
+
// A page gets its translator from the session: `const tx = tr(s)`, then `tx('rail.app_builder')`, or with
|
|
7
|
+
// {placeholders}: `tx('ab.count', { n: 3 })`. The result is plain text: the html`` tag escapes it like any value.
|
|
8
|
+
// `tx.rich()` puts markup (links) into a sentence: the message is escaped, Raw parameters are kept.
|
|
9
|
+
// `tx.dyn()` takes a key made at run time (a region type's gallery label, a SQL Workshop tool) with a fallback.
|
|
10
|
+
// A text that is not keyed yet simply stays English. Moving a screen over: docs/guide/12-development.md.
|
|
11
|
+
//
|
|
12
|
+
// The developer's choice: meta.developer.language (migration 127), copied into the session (__BLANG) at sign-in;
|
|
13
|
+
// before signing in, the sign-in page's language links set a cookie (LANG_COOKIE). See language.ts.
|
|
14
|
+
|
|
15
|
+
import type { FastifyRequest } from 'fastify';
|
|
16
|
+
import { esc, raw, Raw } from '../html.ts';
|
|
17
|
+
import type { Session } from '../session.ts';
|
|
18
|
+
import { de } from './i18n/de.ts';
|
|
19
|
+
import { en } from './i18n/en.ts';
|
|
20
|
+
import { es } from './i18n/es.ts';
|
|
21
|
+
import { fr } from './i18n/fr.ts';
|
|
22
|
+
import { nl } from './i18n/nl.ts';
|
|
23
|
+
import { pt } from './i18n/pt.ts';
|
|
24
|
+
|
|
25
|
+
export type BuilderKey = keyof typeof en;
|
|
26
|
+
export type BuilderMessages = Record<BuilderKey, string>;
|
|
27
|
+
export type Params = Record<string, string | number | null | undefined>;
|
|
28
|
+
|
|
29
|
+
/** The builder's languages, in the order of the language menu and the sign-in page's links (names in their own language). */
|
|
30
|
+
export const BUILDER_LANGUAGES = [
|
|
31
|
+
['de', 'Deutsch'], ['en', 'English'], ['es', 'Español'], ['fr', 'Français'], ['nl', 'Nederlands'], ['pt', 'Português'],
|
|
32
|
+
] as const;
|
|
33
|
+
export type BuilderLang = (typeof BUILDER_LANGUAGES)[number][0];
|
|
34
|
+
|
|
35
|
+
const MESSAGES: Record<BuilderLang, Readonly<Record<string, string>>> = { en, de, es, fr, nl, pt };
|
|
36
|
+
|
|
37
|
+
/** The cookie with the sign-in page's language on this device (the builder's path only). */
|
|
38
|
+
export const LANG_COOKIE = 'pgkiln_blang';
|
|
39
|
+
|
|
40
|
+
/** A known builder language code, or null: only the codes in BUILDER_LANGUAGES, exactly. */
|
|
41
|
+
export const validBuilderLanguage = (v: unknown): BuilderLang | null =>
|
|
42
|
+
typeof v === 'string' && Object.hasOwn(MESSAGES, v) ? (v as BuilderLang) : null;
|
|
43
|
+
|
|
44
|
+
/** The language of a developer's session (English when none was chosen). */
|
|
45
|
+
export const builderLanguage = (s: Pick<Session, 'state'> | null | undefined): BuilderLang => validBuilderLanguage(s?.state?.__BLANG) ?? 'en';
|
|
46
|
+
|
|
47
|
+
/** The language of a visitor's sign-in page (or a builder error page): the cookie, else English. */
|
|
48
|
+
export const cookieLanguage = (req: FastifyRequest): BuilderLang =>
|
|
49
|
+
validBuilderLanguage((req.cookies as Record<string, string> | undefined)?.[LANG_COOKIE]) ?? 'en';
|
|
50
|
+
|
|
51
|
+
export const languageName = (lang: BuilderLang) => BUILDER_LANGUAGES.find(([c]) => c === lang)![1];
|
|
52
|
+
|
|
53
|
+
const fill = (text: string, params?: Params) =>
|
|
54
|
+
params ? text.replace(/\{(\w+)\}/g, (m, k: string) => (params[k] === undefined || params[k] === null ? m : String(params[k]))) : text;
|
|
55
|
+
|
|
56
|
+
export interface BuilderTranslate {
|
|
57
|
+
(key: BuilderKey, params?: Params): string;
|
|
58
|
+
readonly lang: BuilderLang;
|
|
59
|
+
/** A key made at run time: its text in this language, else in English, else the fallback (never the raw key). */
|
|
60
|
+
dyn(key: string, fallback: string, params?: Params): string;
|
|
61
|
+
/** A sentence with markup: the message is escaped, string parameters too; Raw parameters (links) are kept. */
|
|
62
|
+
rich(key: BuilderKey, params: Record<string, string | number | Raw>): Raw;
|
|
63
|
+
/** An English label passed around as text (a breadcrumb, a page title) in this language, when it is a known one. */
|
|
64
|
+
label(text: string): string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* English labels that many builder pages pass to shell() as text (breadcrumbs, titles): translated there, so a
|
|
69
|
+
* page that isn't keyed yet still gets a translated breadcrumb trail for these.
|
|
70
|
+
*/
|
|
71
|
+
const LABELS: Record<string, BuilderKey> = {
|
|
72
|
+
'App Builder': 'rail.app_builder',
|
|
73
|
+
'SQL Workshop': 'rail.sql_workshop',
|
|
74
|
+
Home: 'rail.home',
|
|
75
|
+
'App Gallery': 'rail.gallery',
|
|
76
|
+
'Data Reporter': 'rail.reporter',
|
|
77
|
+
'Team Development': 'rail.team',
|
|
78
|
+
Search: 'common.search',
|
|
79
|
+
'Workspace home': 'wh.title',
|
|
80
|
+
'Workspace utilities': 'admin.utilities',
|
|
81
|
+
Dashboard: 'ab.tile.dashboard',
|
|
82
|
+
Import: 'ab.tile.import',
|
|
83
|
+
Developers: 'admin.developers',
|
|
84
|
+
Users: 'nav.users',
|
|
85
|
+
'Shared Components': 'app.tab.shared',
|
|
86
|
+
Settings: 'app.tab.settings',
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/** A translator: the language's messages (`own`, for tests: a partial file), then English, then the key itself. */
|
|
90
|
+
export function builderTranslator(lang: BuilderLang, own: Readonly<Record<string, string>> = MESSAGES[lang]): BuilderTranslate {
|
|
91
|
+
const text = (key: string): string | undefined => own[key] ?? (en as Record<string, string>)[key];
|
|
92
|
+
const tx = ((key: BuilderKey, params?: Params) => fill(text(key) ?? key, params)) as BuilderTranslate;
|
|
93
|
+
Object.defineProperty(tx, 'lang', { value: lang });
|
|
94
|
+
tx.dyn = (key, fallback, params) => fill(text(key) ?? fallback, params);
|
|
95
|
+
tx.rich = (key, params) =>
|
|
96
|
+
raw(esc(text(key) ?? key).replace(/\{(\w+)\}/g, (m, k: string) => {
|
|
97
|
+
const v = params[k];
|
|
98
|
+
return v === undefined ? m : v instanceof Raw ? v.value : esc(v);
|
|
99
|
+
}));
|
|
100
|
+
tx.label = (label) => (Object.hasOwn(LABELS, label) ? tx(LABELS[label]) : label);
|
|
101
|
+
return tx;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const CACHE = new Map<BuilderLang, BuilderTranslate>();
|
|
105
|
+
/** The translator of a language (one per language, kept). */
|
|
106
|
+
export const translatorFor = (lang: BuilderLang) => {
|
|
107
|
+
let t = CACHE.get(lang);
|
|
108
|
+
if (!t) CACHE.set(lang, (t = builderTranslator(lang)));
|
|
109
|
+
return t;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/** The translator of a developer's session. */
|
|
113
|
+
export const tr = (s: Pick<Session, 'state'> | null | undefined) => translatorFor(builderLanguage(s));
|
|
114
|
+
|
|
115
|
+
/** For tests and the coverage check: the message files. */
|
|
116
|
+
export const builderMessages = (lang: BuilderLang): Readonly<Record<string, string>> => MESSAGES[lang];
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
|
|
2
|
+
import { owner } from '../db.ts';
|
|
3
|
+
import { html, raw } from '../html.ts';
|
|
4
|
+
import { icon } from '../icons.ts';
|
|
5
|
+
import type { Session } from '../session.ts';
|
|
6
|
+
import { BUILDER_LANGUAGES, LANG_COOKIE, tr, validBuilderLanguage, type BuilderLang } from './i18n.ts';
|
|
7
|
+
import { back, BASE, csrf, developer, type Req } from './ui.ts';
|
|
8
|
+
|
|
9
|
+
// (sprint 121, WS-7) Choosing the builder's language (APEX: the language links of the builder).
|
|
10
|
+
// - Signed in: the language menu in the status bar (footer) of every builder page posts to /builder/language
|
|
11
|
+
// (CSRF-checked); the choice is stored for the developer (meta.developer.language, migration 127) and in the
|
|
12
|
+
// session, so it holds in every later session and on every device.
|
|
13
|
+
// - Signed out: the sign-in page's language links (GET /builder/login?lang=de) set a cookie only (LANG_COOKIE),
|
|
14
|
+
// for the sign-in page; signing in after such a link stores it for the developer (routes.ts).
|
|
15
|
+
// Only the codes of BUILDER_LANGUAGES are accepted; anything else is ignored (a link) or refused (400, the menu).
|
|
16
|
+
// Nothing here may run at import time: ui.ts imports this module and this module imports ui.ts.
|
|
17
|
+
|
|
18
|
+
export const langCookieOptions = () => ({ path: BASE, httpOnly: true, sameSite: 'lax' as const, maxAge: 365 * 24 * 3600, secure: process.env.COOKIE_SECURE === 'true' });
|
|
19
|
+
|
|
20
|
+
/** Put the developer's stored language into a session that has none yet (a session from before this sprint). */
|
|
21
|
+
export async function loadLanguage(s: Session) {
|
|
22
|
+
if (!s.username) return;
|
|
23
|
+
const r = await owner.one<{ language: string | null }>('select language from meta.developer where username = $1', [s.username]);
|
|
24
|
+
s.state.__BLANG = validBuilderLanguage(r?.language) ?? 'en';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Store a developer's choice (and set it for this session and the sign-in page of this device). */
|
|
28
|
+
export async function chooseLanguage(reply: FastifyReply, s: Session, lang: BuilderLang) {
|
|
29
|
+
await owner.query('update meta.developer set language = $2 where username = $1', [s.username, lang]);
|
|
30
|
+
s.state.__BLANG = lang;
|
|
31
|
+
reply.setCookie(LANG_COOKIE, lang, langCookieOptions());
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The language menu of the builder's status bar: a <details> menu of buttons in a form (no script needed). */
|
|
35
|
+
export function languageMenu(s: Session) {
|
|
36
|
+
const tx = tr(s);
|
|
37
|
+
return html`<details class="menu ide-lang" data-lang-menu>
|
|
38
|
+
<summary title="${tx('status.language')}">${icon('globe')}<span class="sr-only">${tx('status.language')}: </span><span lang="${tx.lang}">${BUILDER_LANGUAGES.find(([c]) => c === tx.lang)![1]}</span></summary>
|
|
39
|
+
<div class="menu-panel">
|
|
40
|
+
<form method="post" action="${BASE}/language" class="menu-section menu-links ide-lang-form" aria-label="${tx('status.language')}">${csrf(s)}
|
|
41
|
+
<span class="small muted">${tx('status.language')}</span>
|
|
42
|
+
${BUILDER_LANGUAGES.map(([code, name]) => html`<button name="language" value="${code}" lang="${code}"${code === tx.lang ? raw(' aria-current="true"') : ''}>${code === tx.lang ? icon('check') : html`<span class="ide-lang-pad" aria-hidden="true"></span>`}${name}</button>`)}
|
|
43
|
+
</form>
|
|
44
|
+
</div></details>`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The sign-in page's language links (APEX: Deutsch, English, Español, …), keeping where to go after signing in. */
|
|
48
|
+
export function signInLanguageLinks(current: BuilderLang, next: string, label: string) {
|
|
49
|
+
return html`<nav class="signin-langs" aria-label="${label}"><ul>${BUILDER_LANGUAGES.map(([code, name]) => {
|
|
50
|
+
const q = new URLSearchParams({ lang: code, ...(next !== `${BASE}/home` ? { next } : {}) });
|
|
51
|
+
return html`<li><a href="${BASE}/login?${q}" lang="${code}" hreflang="${code}"${code === current ? raw(' aria-current="true"') : ''}>${name}</a></li>`;
|
|
52
|
+
})}</ul></nav>`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** A builder address on this host to go back to (the referring page), else the App Builder. */
|
|
56
|
+
const backTo = (req: FastifyRequest) => {
|
|
57
|
+
try {
|
|
58
|
+
const u = new URL(String(req.headers.referer ?? ''));
|
|
59
|
+
return u.host === req.headers.host && u.pathname.startsWith(BASE) ? u.pathname + u.search : BASE;
|
|
60
|
+
} catch {
|
|
61
|
+
return BASE;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export async function languageRoutes(app: FastifyInstance) {
|
|
66
|
+
app.post(`${BASE}/language`, async (req: Req, reply) => {
|
|
67
|
+
const s = await developer(req, reply);
|
|
68
|
+
if (!s) return;
|
|
69
|
+
const lang = validBuilderLanguage(req.body?.language);
|
|
70
|
+
if (!lang) return reply.code(400).send('Unknown language');
|
|
71
|
+
await chooseLanguage(reply, s, lang);
|
|
72
|
+
return back(reply, s, backTo(req));
|
|
73
|
+
});
|
|
74
|
+
}
|
package/src/builder/launchpad.ts
CHANGED
|
@@ -4,7 +4,8 @@ import { html, type Raw } from '../html.ts';
|
|
|
4
4
|
import { icon } from '../icons.ts';
|
|
5
5
|
import type { Session } from '../session.ts';
|
|
6
6
|
import { back, BASE, csrf, designerUrl, developer, flash, send, shell, type Req } from './ui.ts';
|
|
7
|
-
import { SQL_TOOLS, TOOL_GROUPS } from './sql-tools.ts';
|
|
7
|
+
import { SQL_TOOLS, TOOL_GROUPS, toolDescription, toolLabel } from './sql-tools.ts';
|
|
8
|
+
import { tr } from './i18n.ts';
|
|
8
9
|
import { SYSTEM_SCHEMAS } from './object-browser.ts';
|
|
9
10
|
import { currentWorkspace, inWorkspaceSql, workshopDb } from './workspaces.ts';
|
|
10
11
|
import { isAdmin } from './locks.ts';
|
|
@@ -28,10 +29,11 @@ export async function pinsOf(s: Session): Promise<Pin[]> {
|
|
|
28
29
|
|
|
29
30
|
/** A pin toggle (a small form): pinned → unpin, else pin. */
|
|
30
31
|
export function pinButton(s: Session, p: Pin, pinned: boolean, backTo: string): Raw {
|
|
32
|
+
const tx = tr(s);
|
|
31
33
|
return html`<form method="post" action="${BASE}/pins" class="pin-form">${csrf(s)}
|
|
32
34
|
<input type="hidden" name="href" value="${p.href}"><input type="hidden" name="label" value="${p.label}"><input type="hidden" name="icon" value="${p.icon}">
|
|
33
35
|
<input type="hidden" name="pin" value="${pinned ? '0' : '1'}"><input type="hidden" name="back" value="${backTo}">
|
|
34
|
-
<button class="pin-btn${pinned ? ' is-pinned' : ''}" title="${pinned ? '
|
|
36
|
+
<button class="pin-btn${pinned ? ' is-pinned' : ''}" title="${tx(pinned ? 'sw.unpin' : 'sw.pin_title')}" aria-pressed="${pinned ? 'true' : 'false'}">${icon('pin')}<span class="sr-only">${tx(pinned ? 'sw.unpin_what' : 'sw.pin_what', { what: p.label })}</span></button></form>`;
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
/** The Launchpad part of the SQL Workshop home. */
|
|
@@ -39,18 +41,21 @@ export async function launchpad(s: Session, admin: boolean): Promise<Raw> {
|
|
|
39
41
|
const pins = await pinsOf(s);
|
|
40
42
|
const pinned = new Set(pins.map((p) => p.href));
|
|
41
43
|
const tools = SQL_TOOLS.filter((t) => admin || !t.admin);
|
|
44
|
+
const tx = tr(s);
|
|
45
|
+
// a pin keeps the tool's English name (pins are stored with their label); the card shows it in the builder's language
|
|
42
46
|
const card = (t: (typeof SQL_TOOLS)[number]) => html`<li class="lp-tool">
|
|
43
|
-
<a href="${t.href}">${icon(t.icon)}<span><strong>${t
|
|
47
|
+
<a href="${t.href}">${icon(t.icon)}<span><strong>${toolLabel(tx, t)}</strong><small class="muted">${toolDescription(tx, t)}</small></span></a>
|
|
44
48
|
${pinButton(s, { href: t.href, label: t.label, icon: t.icon }, pinned.has(t.href), `${BASE}/sql`)}</li>`;
|
|
49
|
+
const group = (g: string, label: string) => tx.dyn(`sql.group.${g}`, label);
|
|
45
50
|
return html`<section class="lp-pins" aria-labelledby="lp-pins-title">
|
|
46
|
-
<h2 id="lp-pins-title"
|
|
51
|
+
<h2 id="lp-pins-title">${tx('sw.pinned')}</h2>
|
|
47
52
|
${pins.length ? html`<ul class="lp-chips">${pins.map((p) => html`<li><a href="${p.href}">${icon(p.icon)}<span>${p.label}</span></a>${pinButton(s, p, true, `${BASE}/sql`)}</li>`)}</ul>`
|
|
48
|
-
: html`<p class="muted small"
|
|
49
|
-
<ul class="lp-chips lp-recent" data-lp-recent aria-label="
|
|
53
|
+
: html`<p class="muted small">${tx.rich('sw.pin_hint', { pin: icon('pin') })}</p>`}
|
|
54
|
+
<ul class="lp-chips lp-recent" data-lp-recent aria-label="${tx('sw.recent')}" hidden></ul>
|
|
50
55
|
</section>
|
|
51
56
|
<div class="lp-groups">
|
|
52
|
-
<div class="lp-tabs" role="tablist" aria-label="
|
|
53
|
-
${TOOL_GROUPS.map(([g, label]) => html`<section class="lp-group" id="lp-${g}" data-lp-group="${g}" aria-label="${label}"><h2>${label}</h2><ul class="lp-list">${tools.filter((t) => t.group === g).map(card)}</ul></section>`)}
|
|
57
|
+
<div class="lp-tabs" role="tablist" aria-label="${tx('wh.tools')}" hidden>${TOOL_GROUPS.map(([g, label], i) => html`<button type="button" role="tab" aria-selected="${i === 0 ? 'true' : 'false'}" aria-controls="lp-${g}" data-lp-tab="${g}">${group(g, label)}</button>`)}</div>
|
|
58
|
+
${TOOL_GROUPS.map(([g, label]) => html`<section class="lp-group" id="lp-${g}" data-lp-group="${g}" aria-label="${group(g, label)}"><h2>${group(g, label)}</h2><ul class="lp-list">${tools.filter((t) => t.group === g).map(card)}</ul></section>`)}
|
|
54
59
|
</div>`;
|
|
55
60
|
}
|
|
56
61
|
|
|
@@ -158,7 +158,7 @@ export async function objectBrowserRoutes(app: FastifyInstance) {
|
|
|
158
158
|
<a class="create-source" href="${BASE}/sql/load">${icon('upload')}<span><b>Load data</b><small class="muted">A table from a file</small></span></a></div>
|
|
159
159
|
<p class="muted">Select an object in the tree, or filter it by name.</p>`);
|
|
160
160
|
|
|
161
|
-
const main = html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('objects')}
|
|
161
|
+
const main = html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('objects', s)}
|
|
162
162
|
<div class="ob-head">
|
|
163
163
|
<form method="get" class="ob-schema">${filter ? html`<input type="hidden" name="q" value="${filter}">` : ''}
|
|
164
164
|
<label class="label" for="ob-schema">Schema</label>
|
package/src/builder/plugins.ts
CHANGED
|
@@ -84,7 +84,7 @@ async function page(s: Session, a: any, selected: string | undefined, result?: {
|
|
|
84
84
|
</div>`)
|
|
85
85
|
: '';
|
|
86
86
|
|
|
87
|
-
return html`${appHeader(a, 'shared')}
|
|
87
|
+
return html`${appHeader(a, 'shared', s)}
|
|
88
88
|
${result ? region(`Result: install SQL of ${result.name}`, resultsHtml('install', result.run)) : ''}
|
|
89
89
|
${detail}
|
|
90
90
|
${region('Plug-ins', html`<p class="muted u-mt0">Region, item, dynamic action and process types that bring their own code: JavaScript and CSS as
|
|
@@ -365,6 +365,6 @@ export async function queryBuilderRoutes(app: FastifyInstance) {
|
|
|
365
365
|
<form method="post" action="${BASE}/sql">${csrf(s)}<input type="hidden" name="sql" value="${built.sql}">
|
|
366
366
|
<div class="buttons"><button class="btn btn-hot">${icon('play')} Run in the SQL Worksheet</button></div></form>`)}`
|
|
367
367
|
: '';
|
|
368
|
-
return send(reply, s, shell(s, 'Query Builder', [['SQL Workshop', `${BASE}/sql`], ['Query Builder']], html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('query')}${form}${output}`, 'sql'));
|
|
368
|
+
return send(reply, s, shell(s, 'Query Builder', [['SQL Workshop', `${BASE}/sql`], ['Query Builder']], html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('query', s)}${form}${output}`, 'sql'));
|
|
369
369
|
});
|
|
370
370
|
}
|
package/src/builder/quicksql.ts
CHANGED
|
@@ -52,7 +52,7 @@ export async function quickSqlRoutes(app: FastifyInstance) {
|
|
|
52
52
|
<div class="buttons"><button class="btn" name="action" value="save">${icon('file')} Save as script</button>
|
|
53
53
|
<button class="btn btn-hot" name="action" value="run">${icon('play')} Run</button></div>`)}
|
|
54
54
|
</form>`;
|
|
55
|
-
return send(reply, s, shell(s, 'Quick SQL', [['SQL Workshop', `${BASE}/sql`], ['Quick SQL']], html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('quicksql')}${main}`, 'sql'));
|
|
55
|
+
return send(reply, s, shell(s, 'Quick SQL', [['SQL Workshop', `${BASE}/sql`], ['Quick SQL']], html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('quicksql', s)}${main}`, 'sql'));
|
|
56
56
|
};
|
|
57
57
|
|
|
58
58
|
app.get(`${BASE}/sql/quick`, async (req: Req, reply) => {
|
|
@@ -101,7 +101,7 @@ function tree(m: ModRow, sel: { t?: string; m?: string } = {}) {
|
|
|
101
101
|
|
|
102
102
|
/** The page frame: tabs, the module's tree beside the content. */
|
|
103
103
|
function frame(s: Session, m: ModRow, title: string, crumbs: [string, string?][], sel: { t?: string; m?: string }, body: Raw) {
|
|
104
|
-
const main = html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('rest')}${restNav('modules')}
|
|
104
|
+
const main = html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('rest', s)}${restNav('modules')}
|
|
105
105
|
<h2 class="u-mb1 wrap-anywhere">${m.title} <span class="muted small">${m.app_name} · /a/${m.app_alias}/rest/${m.name}/${m.enabled ? '' : ' · disabled'}</span></h2>
|
|
106
106
|
<div class="rm-layout">${region('Module', tree(m, sel))}<div class="rm-main">${body}</div></div>`;
|
|
107
107
|
return shell(s, title, [['SQL Workshop', `${BASE}/sql`], ['RESTful Services', MODULES], ...crumbs], main, 'sql');
|
|
@@ -136,7 +136,7 @@ async function listPage(s: Session, b: Record<string, unknown> = {}, problems: s
|
|
|
136
136
|
</div>
|
|
137
137
|
<div class="field"><label class="label" for="f_description">Description</label><input id="f_description" name="description" value="${v('description')}"></div>
|
|
138
138
|
<div class="buttons"><button class="btn btn-hot"${apps.length ? '' : raw(' disabled')}>${icon('plus')} Create module</button></div></form>`;
|
|
139
|
-
const main = html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('rest')}${restNav('modules')}
|
|
139
|
+
const main = html`<h1 class="u-mb1">SQL Workshop</h1>${workshopTabs('rest', s)}${restNav('modules')}
|
|
140
140
|
<p class="muted">RESTful Services modules: a module is a base path of an application, its templates are URIs with <code>:parameters</code>, and each template has handlers per method with their source: a collection query, a single row, SQL, a PL/pgSQL block or a media resource. Handlers run as the application's database role with the caller as <code>meta.app_user()</code>, so its grants and row level security apply; parameters are bound, never pasted into the SQL. Every module has an OpenAPI document. Modules belong to their application: they travel with its export (also in Shared Components › RESTful Services).</p>
|
|
141
141
|
${region('Modules', table)}${region('Create a module', form)}`;
|
|
142
142
|
return shell(s, 'RESTful Services: Modules', [['SQL Workshop', `${BASE}/sql`], ['RESTful Services', `${BASE}/sql/rest`], ['Modules']], main, 'sql');
|