pgkiln 0.88.0 → 0.89.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/docs/guide/03-builder.md +53 -25
- package/package.json +1 -1
- package/public/builder.css +57 -3
- package/public/builder.js +66 -0
- package/public/palette.js +8 -1
- package/public/worksheet.css +39 -4
- package/public/worksheet.js +15 -1
- package/src/builder/designer.ts +2 -2
- package/src/builder/gallery.ts +5 -5
- package/src/builder/home.ts +71 -60
- package/src/builder/i18n/de.ts +22 -0
- package/src/builder/i18n/en.ts +22 -0
- package/src/builder/i18n/es.ts +22 -0
- package/src/builder/i18n/fr.ts +22 -0
- package/src/builder/i18n/it.ts +22 -0
- package/src/builder/i18n/ja.ts +22 -0
- package/src/builder/i18n/ko.ts +22 -0
- package/src/builder/i18n/nl.ts +22 -0
- package/src/builder/i18n/pt.ts +22 -0
- package/src/builder/i18n/zh-cn.ts +22 -0
- package/src/builder/i18n/zh-tw.ts +22 -0
- package/src/builder/routes.ts +21 -10
- package/src/builder/sql.ts +2 -2
- package/src/builder/ui.ts +47 -33
- package/src/builder/worksheet.ts +33 -24
- package/src/builder/workspace-home.ts +17 -0
- package/src/session.ts +2 -0
package/docs/guide/03-builder.md
CHANGED
|
@@ -225,9 +225,16 @@ department. Every installation has the **Default** workspace, which holds the ap
|
|
|
225
225
|
- **New applications** (Create, from a file, pasted data, existing tables, a blueprint) and **imports** go into the
|
|
226
226
|
current workspace; a working copy stays in its main application's workspace. Imports made with the command line
|
|
227
227
|
(`pgkiln import`) go into Default. Exports don't carry the workspace.
|
|
228
|
-
- **Workspace utilities
|
|
229
|
-
|
|
230
|
-
|
|
228
|
+
- **Workspace utilities** (the rail's **Administration**, `/builder/utilities`; UX-4): the utilities in sections with a
|
|
229
|
+
**filter** field at the top: *People and sign-in* (users, developers, identity providers, LDAP directories, password
|
|
230
|
+
policy), *Connections* ([remote servers, web credentials](#static-workspace-files-remote-servers-and-web-credentials),
|
|
231
|
+
remote databases, REST source catalogs, mail services; for workspace administrators the mail queue and network
|
|
232
|
+
access), *Data and AI* (AI services, vector providers, UI defaults, static workspace files, icons), *Operations*
|
|
233
|
+
(application groups, backups, export and import, the dashboard, OpenTelemetry), the current **workspace** (for its
|
|
234
|
+
administrators: its page, [workspace message and environment banner](#workspace-message-and-environment-banner),
|
|
235
|
+
preferences, extension links, [workspace utilization](#workspace-utilization)) and the **instance** (for
|
|
236
|
+
administrators: instance settings, workspaces, [Database users](#database-users), the installation), then
|
|
237
|
+
**Cross Application Reports** (APEX: Cross Application Reports), each over
|
|
231
238
|
every application of the current workspace: *Application attributes* (type, sign-in, availability, build status,
|
|
232
239
|
debug level, language, database role), *Build options*, *Developer comments*, *Authentication* (sign-in methods,
|
|
233
240
|
identity providers, LDAP, access, the login page, Forgot password), *Authorization schemes*, *Password items* and
|
|
@@ -247,7 +254,7 @@ its developers must not reach other teams' applications or data.
|
|
|
247
254
|
### Workspace message and environment banner
|
|
248
255
|
|
|
249
256
|
Administrators and the workspace's administrators set them on the workspace's page (*Manage service*, or the
|
|
250
|
-
Administration
|
|
257
|
+
Administration → *Workspace message and environment banner*; APEX: Define Workspace Message, Define Environment
|
|
251
258
|
Banner): a **message** (plain text, up to 2000 characters) shown to the workspace's developers on the
|
|
252
259
|
[Workspace home](#workspace-home) (where they can also edit it in place) and the App Builder home,
|
|
253
260
|
and an **environment banner**: a label of up to 40 characters in one of eight colours, shown at the top of every
|
|
@@ -255,7 +262,7 @@ builder page while the workspace is current.
|
|
|
255
262
|
|
|
256
263
|
### Workspace preferences
|
|
257
264
|
|
|
258
|
-
Administrators and the workspace's administrators set them on the workspace's page (Administration
|
|
265
|
+
Administrators and the workspace's administrators set them on the workspace's page (Administration →
|
|
259
266
|
*Workspace preferences*; APEX: Set Workspace Preferences):
|
|
260
267
|
|
|
261
268
|
- **Enable the SQL Workshop**: off, every SQL Workshop page (the SQL Worksheet, the Object Browser, SQL scripts, Load and
|
|
@@ -349,7 +356,7 @@ says what was added, kept and replaced.
|
|
|
349
356
|
|
|
350
357
|
### Workspace utilization
|
|
351
358
|
|
|
352
|
-
Administration
|
|
359
|
+
Administration → **Workspace utilization** (`/builder/workspaces/<id>/utilization`; APEX: Workspace
|
|
353
360
|
Utilization), for administrators and the workspace's administrators: the number of applications, pages, developers,
|
|
354
361
|
page views of the last 30 days and SQL scripts; per application its pages, regions, items, page views, users and the
|
|
355
362
|
size of its static files; and the schemas the workspace's database roles use with their tables and storage.
|
|
@@ -462,11 +469,14 @@ longer sign in. Delete a workspace only when it is shared and empty; its logins
|
|
|
462
469
|
Every builder page has the same frame, modelled on an IDE:
|
|
463
470
|
|
|
464
471
|
- **The icon rail** on the left: Home (the [Workspace home](#workspace-home); the logo leads there too), App Builder,
|
|
465
|
-
SQL Workshop, [App Gallery](#app-gallery), [Data Reporter](#data-reporter-datasets-and-reporting-applications),
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
472
|
+
SQL Workshop, [App Gallery](#app-gallery), [Data Reporter](#data-reporter-datasets-and-reporting-applications),
|
|
473
|
+
Team Development and **Administration** (the workspace utilities, below); at the bottom Help, **Show labels** and your
|
|
474
|
+
account menu (the circle with your initials). (UX-3) The entry of the page you are on is marked, by the page's
|
|
475
|
+
address (an application's pages under App Builder; users, developers, workspaces, instance settings and the other
|
|
476
|
+
utilities under Administration). *Show labels* puts each entry's name next to its icon (*Hide labels* takes them
|
|
477
|
+
away; kept for your session); without them the name shows when you point at an icon or move to it with the
|
|
478
|
+
keyboard. The search is the field at the top of every page (**Ctrl+K**); an application's own search is its
|
|
479
|
+
**Search** tab. Your account menu has the
|
|
470
480
|
**builder theme** (*Dark*, the default; *Light*; or *System*, which follows the operating system),
|
|
471
481
|
Change password and Sign out. The theme is remembered on this device, also after signing out;
|
|
472
482
|
it only changes the builder, not your applications.
|
|
@@ -548,6 +558,8 @@ names and help of a shared component's form), the links to this guide, and any S
|
|
|
548
558
|
(sprint 109; APEX: the workspace home page) The page the builder opens after signing in, `/builder/home`, above the
|
|
549
559
|
App Builder and the SQL Workshop. Everything on it is of the current [workspace](#workspaces), and it works without
|
|
550
560
|
JavaScript. The [default-password banner](#signing-in-and-developer-accounts) stays on top until the password is changed.
|
|
561
|
+
(UX-13) For administrators, **Needs attention** under the heading lists what must not wait: failing
|
|
562
|
+
[lockout checks](#lockout-checks) and, with the release check on, a newer pgkiln (details in the System message).
|
|
551
563
|
|
|
552
564
|
- **Tool cards**: App Builder (`/builder/apps`), SQL Workshop (not when the workspace's preferences turn it off),
|
|
553
565
|
**Data Reporter** (when the server has the Data Reporter tool at `/builder/reporter`) and the [App Gallery](#app-gallery).
|
|
@@ -582,12 +594,13 @@ that may not reach the internet. Under the test runner the server never asks Git
|
|
|
582
594
|
The App Builder's home page, `/builder/apps` (`/builder` shows it too, unless the workspace's
|
|
583
595
|
[preferences](#workspace-preferences) send `/builder` to the Workspace home), is laid out like APEX's App Builder:
|
|
584
596
|
|
|
585
|
-
- **
|
|
586
|
-
(applications, pages, active accounts; page views, users, failed
|
|
587
|
-
24 hours, and per application the last 7 days with a link to its Activity page)
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
597
|
+
- **The header** (UX-5): the heading *App Builder*, **Create** (the page's one primary action), **Import** and a
|
|
598
|
+
**⋯** menu with the workspace **Dashboard** (applications, pages, active accounts; page views, users, failed
|
|
599
|
+
sign-ins and errors of the last 24 hours, and per application the last 7 days with a link to its Activity page)
|
|
600
|
+
and **Administration** (the [workspace utilities](#workspaces)). The list, the dashboard and Recent show the
|
|
601
|
+
current workspace. Every builder page follows the same pattern: one heading, one primary (accent) button,
|
|
602
|
+
the other actions beside it or in a ⋯ menu; an application's Pages tab has **Create page** as its primary action
|
|
603
|
+
in the application's header (other tabs: **Run**), and its Tasks list only what the page doesn't offer already.
|
|
591
604
|
- **The applications**, as a report (application id, name, alias, pages, page views of the last 24
|
|
592
605
|
hours, sign-in, last updated, **updated by**, **working copies**, and Edit / Run) or as cards. **Search** filters the list as you type
|
|
593
606
|
(by name, alias or id; without JavaScript, press Enter). Click a column heading, or use
|
|
@@ -1072,6 +1085,13 @@ The page designer has three panes, as in APEX's Page Designer. From 1024 pixels
|
|
|
1072
1085
|
side by side; on phones and portrait tablets they are tabs (**Tree**, **Layout**, **Properties**):
|
|
1073
1086
|
choosing a component opens the Properties tab.
|
|
1074
1087
|
|
|
1088
|
+
(UX-2) From 1024 pixels wide the designer is fixed to the window: only the panes scroll, so the toolbar
|
|
1089
|
+
with **Save**, Undo / Redo and the page navigator stays in view, and the property editor's own save button
|
|
1090
|
+
stays at the bottom of its pane. **Ctrl+S** (Cmd+S) saves. A change not saved yet puts a dot on Save, and
|
|
1091
|
+
leaving the page with one asks first. On a window under 800 pixels high the gallery under the layout is only
|
|
1092
|
+
its tabs (*Regions*, *Items*, *Buttons*): choose one to open it, choose the open one to fold it again; the
|
|
1093
|
+
designer remembers which you prefer.
|
|
1094
|
+
|
|
1075
1095
|
- **Left: the component tree**, with four tabs:
|
|
1076
1096
|
- **Rendering**: the page, *Pre-Rendering* (branches, computations and processes that run
|
|
1077
1097
|
before the page is shown), the regions by
|
|
@@ -1087,7 +1107,8 @@ choosing a component opens the Properties tab.
|
|
|
1087
1107
|
application items), linking to Shared Components.
|
|
1088
1108
|
|
|
1089
1109
|
Icons show which components have an authorization scheme, a condition or a build option; type icons are coloured
|
|
1090
|
-
by kind (regions, items, buttons, dynamic actions, processes).
|
|
1110
|
+
by kind (regions, items, buttons, dynamic actions, processes). (UX-12) A component's name comes first: when the
|
|
1111
|
+
pane is narrow its type text is left out (the icon shows the type; hover a component for its name and type). Folders open and close
|
|
1091
1112
|
with a click; with the keyboard, the arrow keys walk the tree (Left / Right close and open a
|
|
1092
1113
|
folder), Enter opens a component.
|
|
1093
1114
|
|
|
@@ -1790,7 +1811,7 @@ sending mailboxes (an application access policy or RBAC for Applications).
|
|
|
1790
1811
|
|
|
1791
1812
|
### Mail queue
|
|
1792
1813
|
|
|
1793
|
-
Administration
|
|
1814
|
+
Administration → **Mail queue** (`/builder/mail-queue`; APEX 26.2: Administration → Mail queue): the delivery
|
|
1794
1815
|
health of the mail queue, for administrators (every workspace, or one) and a workspace's administrators (the current
|
|
1795
1816
|
workspace's applications only; other developers get 403). Filter by period (24 hours, 48 hours, 7 days), workspace and
|
|
1796
1817
|
application:
|
|
@@ -1835,7 +1856,7 @@ applications off or send them to a collector of their own as well (through the a
|
|
|
1835
1856
|
|
|
1836
1857
|
### Network access
|
|
1837
1858
|
|
|
1838
|
-
Administration
|
|
1859
|
+
Administration → **Network access** (`/builder/network`; APEX 26.2: workspace network ACLs), for administrators
|
|
1839
1860
|
(any workspace, `?ws=`) and a workspace's administrators (their workspaces): which web services a workspace's
|
|
1840
1861
|
applications may call. The installation's list (`PGKILN_REST_ALLOWED_HOSTS`, set in the server's environment, shown
|
|
1841
1862
|
read-only; the private hosts to administrators only) applies to every workspace. A workspace can choose **Only the
|
|
@@ -2121,7 +2142,8 @@ others (Quick SQL, Query Builder, Load and Unload Data, Sample Data, DDL, Compar
|
|
|
2121
2142
|
|
|
2122
2143
|
(sprint 96; Database Actions: the Launchpad) The home page starts with **Pinned & recently visited**: the pages you
|
|
2123
2144
|
pinned (a tool, a database object from the Object Browser, a saved worksheet from its file menu; at most 24, per
|
|
2124
|
-
workspace) and the SQL Workshop pages you opened last in this browser
|
|
2145
|
+
workspace) and the SQL Workshop pages you opened last in this browser (UX-14: each page once, the latest visit
|
|
2146
|
+
first, without the Launchpad itself or a page that is pinned already). Below, the tools are in three tabs,
|
|
2125
2147
|
**Development**, **Administration** and **Monitoring**, each with a line about what it does and a pin; the **More**
|
|
2126
2148
|
menu uses the same headings.
|
|
2127
2149
|
|
|
@@ -2133,6 +2155,12 @@ the SQL Workshop's connection may use. Without JavaScript the header's field ope
|
|
|
2133
2155
|
- **SQL Worksheet** (`/builder/sql/worksheet`, sprint 91; Database Actions: SQL; was SQL Commands, whose address
|
|
2134
2156
|
`/builder/sql/commands` opens the same page): one screen with a **navigator** on the left, the **editor** with a
|
|
2135
2157
|
toolbar, **output tabs** below and a **status bar**.
|
|
2158
|
+
- (UX-1) The **toolbar is one row**: the worksheet's file menu, **Run Statement**, **Run Script**, **Explain Plan**
|
|
2159
|
+
and **Autotrace** (icons with their name as a tooltip when the window is narrow), **Suggest** and **Check**, the
|
|
2160
|
+
commit mode, fullscreen and a **⋯ menu** with *Continue on error*, Format, Convert case, Find, Load data, Clear
|
|
2161
|
+
and the keyboard shortcuts. **Run as** is in the status bar (its tooltip says who the statements run as).
|
|
2162
|
+
The editor takes about half of the window and the output the other half (drag the splitter; the size is
|
|
2163
|
+
remembered); before the first run the output is only its row of tabs, and choosing a tab opens it.
|
|
2136
2164
|
- The navigator has a schema (the same one as the Object Browser), an object type (*All Objects*, tables, views,
|
|
2137
2165
|
materialized views, indexes, sequences, types, functions, procedures, triggers, policies) and a search field.
|
|
2138
2166
|
A table or view opens to its columns with their types (a key marks the primary key); drag it into the editor
|
|
@@ -2217,11 +2245,11 @@ the SQL Workshop's connection may use. Without JavaScript the header's field ope
|
|
|
2217
2245
|
remembered for the session; the history and the log keep the statement as written.
|
|
2218
2246
|
- **Format** (**Ctrl + F7**) lays out the selection or the whole worksheet: a clause per line, the columns of a select
|
|
2219
2247
|
list and the conditions of a WHERE one per line, joins and subqueries indented, keywords in lower or UPPER case
|
|
2220
|
-
(chosen in the
|
|
2248
|
+
(chosen in the **⋯** menu). Strings, comments and `$$` bodies stay as they are. **Convert case** (**Ctrl + B**)
|
|
2221
2249
|
cycles the selection (or the word at the cursor) through UPPER, lower and Initial caps. **Find and replace**
|
|
2222
2250
|
(**Ctrl + F**, **Ctrl + H**) with a regular expression or exact case, **Go to line** (**Ctrl + G**), and
|
|
2223
|
-
**Ctrl + /** comments the selected lines out or in. The
|
|
2224
|
-
|
|
2251
|
+
**Ctrl + /** comments the selected lines out or in. The **⋯** menu lists every key; **F1** opens this help.
|
|
2252
|
+
*Load data* in the same menu opens Load Data for the navigator's schema.
|
|
2225
2253
|
- Each statement commits on its own (**Autocommit**, like Database Actions). Statements run as the **owner connection** (an isolated workspace's
|
|
2226
2254
|
developer login for its developers), on a connection of their own that is closed afterwards, so `SET ROLE` or
|
|
2227
2255
|
`SET search_path` doesn't leak. **Run as** picks an application of your workspace instead: the statements then
|
|
@@ -2414,7 +2442,7 @@ the SQL Workshop's connection may use. Without JavaScript the header's field ope
|
|
|
2414
2442
|
connections, memory, row security), the installed extensions and the schemas with their size.
|
|
2415
2443
|
- **SQL Monitor** (administrators only): see [SQL Monitor](#sql-monitor) below (it replaced the Database Monitor;
|
|
2416
2444
|
the old address `/builder/sql/utilities/monitor` redirects).
|
|
2417
|
-
- **Database Users** (administrators only; Administration
|
|
2445
|
+
- **Database Users** (administrators only; Administration → *Database users*, `/builder/db-users`; sprint 99,
|
|
2418
2446
|
Database Actions: Database Users): see [Database users](#database-users).
|
|
2419
2447
|
- **Schema Export / Import** (administrators only; `/builder/sql/data-pump`; sprint 103, Database Actions: Data
|
|
2420
2448
|
Pump): see [Schema export and import](#schema-export-and-import).
|
package/package.json
CHANGED
package/public/builder.css
CHANGED
|
@@ -444,7 +444,9 @@ body.ide {
|
|
|
444
444
|
.pd > .tab-panel > .pd-pane-title { display: none; }
|
|
445
445
|
/* with script the designer fills the window and each pane scrolls on its own */
|
|
446
446
|
.js .ide-full .ide-main { height: 100vh; height: 100dvh; display: flex; flex-direction: column; }
|
|
447
|
-
|
|
447
|
+
/* (UX-2) the work area is the containing block and clips: nothing inside (a screen-reader heading placed
|
|
448
|
+
absolutely, a pane's overflow) can make the page itself scroll */
|
|
449
|
+
.js .ide-full .ide-work { flex: 1 1 auto; min-height: 0; position: relative; overflow: hidden; }
|
|
448
450
|
.js .ide-full .pd { height: 100%; min-height: 0; }
|
|
449
451
|
.js .pd-pane { min-height: 0; overflow: auto; }
|
|
450
452
|
.js .pd-center { display: flex; flex-direction: column; overflow: hidden; }
|
|
@@ -484,8 +486,9 @@ body.ide {
|
|
|
484
486
|
}
|
|
485
487
|
.pd-node .icon { width: 16px; height: 16px; color: var(--muted); }
|
|
486
488
|
a.pd-node:hover { background: var(--surface-3); }
|
|
487
|
-
|
|
488
|
-
.pd-
|
|
489
|
+
/* (UX-12) the name keeps its room: the type gives way first (the icon tells it, the tooltip names it) */
|
|
490
|
+
.pd-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; flex: 0 1 auto; }
|
|
491
|
+
.pd-note { margin-left: auto; padding-left: 8px; max-width: 45%; min-width: 0; flex: 0 1000 auto; overflow: hidden; text-overflow: clip; white-space: nowrap; color: var(--muted); font-size: 11.5px; }
|
|
489
492
|
.pd-folder { color: var(--muted); font-weight: 600; cursor: default; }
|
|
490
493
|
.pd-tag { display: inline-flex; }
|
|
491
494
|
.pd-tag .icon { width: 13px; height: 13px; color: var(--warn); }
|
|
@@ -1671,3 +1674,54 @@ a.cp-type:hover, a.cp-type:focus-visible { border-color: var(--accent); }
|
|
|
1671
1674
|
.api-client-actions select { width: auto; min-height: 0; padding-block: 0.2rem; font-size: 0.8125rem; }
|
|
1672
1675
|
.api-client-actions .inline-form { display: flex; gap: 6px; align-items: center; }
|
|
1673
1676
|
.api-clients td.actions { text-align: end; }
|
|
1677
|
+
|
|
1678
|
+
/* (UX-2) Page Designer on a short window: the gallery folded to its tabs (builder.js), Save at hand */
|
|
1679
|
+
@media (min-width: 1024px) {
|
|
1680
|
+
.js .pd-gallery.is-folded { height: auto; min-height: 0; overflow: hidden; }
|
|
1681
|
+
.js .pd-gallery.is-folded .tab-panel { display: none; }
|
|
1682
|
+
}
|
|
1683
|
+
.tb-save.is-dirty { position: relative; }
|
|
1684
|
+
.tb-save.is-dirty::after { content: ""; position: absolute; top: 3px; right: 3px; width: 7px; height: 7px; border-radius: 50%; background: var(--hot); }
|
|
1685
|
+
#pd-p-props .component-form > .buttons { position: sticky; bottom: 0; z-index: 1; margin: 0; padding: 8px 0; background: var(--surface); border-top: 1px solid var(--border); }
|
|
1686
|
+
.pd-note.is-cut { display: none; }
|
|
1687
|
+
|
|
1688
|
+
/* (UX-3) the rail with labels next to its icons (the toggle at its bottom, kept in the session) */
|
|
1689
|
+
.rail-toggle { width: 100%; border: 0; background: none; cursor: pointer; font: inherit; }
|
|
1690
|
+
@media (min-width: 900px) {
|
|
1691
|
+
.ide.rail-labels { --rail-w: 196px; }
|
|
1692
|
+
.rail-labels .rail-list { padding: 0 10px; }
|
|
1693
|
+
.rail-labels .rail-link { display: flex; justify-content: flex-start; gap: 12px; padding: 0 12px; }
|
|
1694
|
+
.rail-labels .rail-link .rail-label { position: static; opacity: 1; transform: none; padding: 0; background: none; color: inherit;
|
|
1695
|
+
box-shadow: none; font-size: 13.5px; font-weight: 500; pointer-events: auto; }
|
|
1696
|
+
.rail-labels .rail-logo { margin-left: 18px; }
|
|
1697
|
+
.rail-labels .rail-user { justify-content: flex-start; padding-left: 14px; }
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
/* (UX-4) Administration: the workspace utilities in sections of compact tiles, with a filter (builder.js) */
|
|
1701
|
+
.adm { max-width: 1180px; }
|
|
1702
|
+
.adm .title-row { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 12px; }
|
|
1703
|
+
.adm-filter input { width: min(320px, 100%); }
|
|
1704
|
+
.adm-lede { max-width: 75ch; }
|
|
1705
|
+
.adm-section { margin-top: 22px; }
|
|
1706
|
+
.adm-section h2 { font-size: 15px; margin: 0 0 8px; color: var(--text-2, var(--muted)); text-transform: none; }
|
|
1707
|
+
.adm-tiles { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 250px), 1fr)); gap: 8px; }
|
|
1708
|
+
.adm-tile { display: flex; flex-direction: column; gap: 2px; height: 100%; padding: 10px 12px; border: 1px solid var(--border); border-radius: var(--radius);
|
|
1709
|
+
background: var(--surface); color: var(--text); text-decoration: none; transition: border-color var(--dur) var(--ease), background-color var(--dur) var(--ease); }
|
|
1710
|
+
.adm-tile:hover, .adm-tile:focus-visible { border-color: var(--hot); background: var(--surface-2); }
|
|
1711
|
+
.adm-tile-head { display: flex; align-items: center; gap: 8px; }
|
|
1712
|
+
.adm-tile-head .icon { width: 16px; height: 16px; color: var(--hot); flex: none; }
|
|
1713
|
+
.adm-tile-text { color: var(--muted); font-size: 12.5px; line-height: 1.4; }
|
|
1714
|
+
.adm-reports { columns: 2 260px; }
|
|
1715
|
+
|
|
1716
|
+
/* (UX-5) a builder page's header: one heading, one primary (accent) action, secondary actions and a ⋯ menu */
|
|
1717
|
+
.page-head { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 10px 16px; margin: 0 0 16px; }
|
|
1718
|
+
.page-head h1 { margin: 0; font-size: 22px; }
|
|
1719
|
+
.page-head-sub { margin: 2px 0 0; }
|
|
1720
|
+
.page-head-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
|
|
1721
|
+
.page-head-actions .btn { display: inline-flex; align-items: center; gap: 6px; }
|
|
1722
|
+
.page-head-actions .icon { width: 15px; height: 15px; }
|
|
1723
|
+
/* (UX-13) the Workspace home's "Needs attention" list for administrators, above the tools */
|
|
1724
|
+
.wh-attention { display: block; margin: 0 0 16px; }
|
|
1725
|
+
.wh-attention h2 { margin: 0 0 4px; font-size: 14px; }
|
|
1726
|
+
.wh-attention ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 4px; }
|
|
1727
|
+
.wh-attention .icon { width: 15px; height: 15px; vertical-align: -2px; }
|
package/public/builder.js
CHANGED
|
@@ -508,6 +508,38 @@
|
|
|
508
508
|
const dirty = () => save.classList.add('is-dirty');
|
|
509
509
|
form.addEventListener('input', dirty);
|
|
510
510
|
form.addEventListener('change', dirty);
|
|
511
|
+
// (UX-2) Ctrl+S saves; leaving with unsaved changes asks first (saving doesn't)
|
|
512
|
+
let submitting = false;
|
|
513
|
+
form.addEventListener('submit', () => (submitting = true));
|
|
514
|
+
document.addEventListener('keydown', (e) => {
|
|
515
|
+
if ((e.ctrlKey || e.metaKey) && !e.altKey && e.key.toLowerCase() === 's' && !e.target.closest?.('.ws, dialog')) {
|
|
516
|
+
e.preventDefault();
|
|
517
|
+
form.requestSubmit();
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
window.addEventListener('beforeunload', (e) => {
|
|
521
|
+
if (!submitting && save.classList.contains('is-dirty')) {
|
|
522
|
+
e.preventDefault();
|
|
523
|
+
e.returnValue = '';
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
// (UX-2) on a short window the gallery is only its tabs: choosing a tab opens it, choosing the open one folds it
|
|
528
|
+
const gallery = document.querySelector('.pd-gallery');
|
|
529
|
+
if (gallery) {
|
|
530
|
+
const short = window.matchMedia('(max-height: 800px)');
|
|
531
|
+
const apply = () => gallery.classList.toggle('is-folded', short.matches && store.get('gallery') !== 'open');
|
|
532
|
+
apply();
|
|
533
|
+
short.addEventListener('change', apply);
|
|
534
|
+
gallery.addEventListener('click', (e) => {
|
|
535
|
+
const t = e.target.closest('[role=tab]');
|
|
536
|
+
if (!t || !short.matches) return;
|
|
537
|
+
const folded = gallery.classList.contains('is-folded');
|
|
538
|
+
const wasActive = t.getAttribute('aria-selected') === 'true';
|
|
539
|
+
const open = folded || !wasActive;
|
|
540
|
+
store.set('gallery', open ? 'open' : null);
|
|
541
|
+
gallery.classList.toggle('is-folded', !open);
|
|
542
|
+
}, true); // before the tabs switch, so the tab that was open is known
|
|
511
543
|
}
|
|
512
544
|
}
|
|
513
545
|
|
|
@@ -1011,7 +1043,41 @@
|
|
|
1011
1043
|
setupQueryCanvas();
|
|
1012
1044
|
setupThemePreview();
|
|
1013
1045
|
setupTabs();
|
|
1046
|
+
// (UX-4) Administration: filter the utilities by their text; sections without a match hide
|
|
1047
|
+
const admQ = document.querySelector('[data-adm-filter]');
|
|
1048
|
+
if (admQ) {
|
|
1049
|
+
admQ.closest('.adm-filter').hidden = false;
|
|
1050
|
+
const page = admQ.closest('.adm');
|
|
1051
|
+
const none = page.querySelector('.adm-none');
|
|
1052
|
+
admQ.addEventListener('input', () => {
|
|
1053
|
+
const q = admQ.value.trim().toLowerCase();
|
|
1054
|
+
let any = false;
|
|
1055
|
+
for (const sec of page.querySelectorAll('.adm-section')) {
|
|
1056
|
+
let shown = 0;
|
|
1057
|
+
for (const li of sec.querySelectorAll('li')) {
|
|
1058
|
+
const hit = !q || li.textContent.toLowerCase().includes(q);
|
|
1059
|
+
li.hidden = !hit;
|
|
1060
|
+
if (hit) shown++;
|
|
1061
|
+
}
|
|
1062
|
+
sec.hidden = !shown;
|
|
1063
|
+
if (shown) any = true;
|
|
1064
|
+
}
|
|
1065
|
+
none.hidden = any;
|
|
1066
|
+
});
|
|
1067
|
+
}
|
|
1014
1068
|
for (const tree of document.querySelectorAll('.pd-tree')) setupTree(tree);
|
|
1069
|
+
// (UX-12) a node's type text only when it fits whole: the name comes first (the icon and tooltip tell the type)
|
|
1070
|
+
const fitNotes = () => {
|
|
1071
|
+
for (const n of document.querySelectorAll('.pd-tree .pd-note')) {
|
|
1072
|
+
n.classList.remove('is-cut');
|
|
1073
|
+
if (n.scrollWidth > n.clientWidth + 1) n.classList.add('is-cut');
|
|
1074
|
+
}
|
|
1075
|
+
};
|
|
1076
|
+
fitNotes();
|
|
1077
|
+
if ('ResizeObserver' in window) {
|
|
1078
|
+
const pane = document.querySelector('.pd-tree')?.closest('.pd-pane');
|
|
1079
|
+
if (pane) new ResizeObserver(() => requestAnimationFrame(fitNotes)).observe(pane);
|
|
1080
|
+
}
|
|
1015
1081
|
for (const tree of document.querySelectorAll('.pd-tree[data-tree-menu]')) setupTreeMenu(tree);
|
|
1016
1082
|
setupProperties();
|
|
1017
1083
|
setupCanvas();
|
package/public/palette.js
CHANGED
|
@@ -18,15 +18,22 @@
|
|
|
18
18
|
if (/^\/builder\/sql\/./.test(path) && !/\/(panel|navigator|action)$/.test(path)) {
|
|
19
19
|
const title = document.title.replace(/ · Builder$/, '');
|
|
20
20
|
const href = path + location.search;
|
|
21
|
-
|
|
21
|
+
// (UX-14) one entry per page (its path): opened again with other parameters, the newest wins
|
|
22
|
+
const list = (store.get('sw.recent') || []).filter((r) => r && typeof r.href === 'string' && r.href.split('?')[0] !== path);
|
|
22
23
|
list.unshift({ href, title });
|
|
23
24
|
store.set('sw.recent', list.slice(0, 8));
|
|
24
25
|
}
|
|
25
26
|
const recent = document.querySelector('[data-lp-recent]');
|
|
26
27
|
if (recent) {
|
|
27
28
|
const list = store.get('sw.recent') || [];
|
|
29
|
+
// (UX-14) not the page you are on, not what is pinned already, each path once
|
|
30
|
+
const pinned = new Set([...document.querySelectorAll('.lp-pins .lp-chips:not([data-lp-recent]) a[href]')].map((a) => a.getAttribute('href').split('?')[0]));
|
|
31
|
+
const seen = new Set([path]);
|
|
28
32
|
for (const r of list) {
|
|
29
33
|
if (typeof r?.href !== 'string' || !r.href.startsWith('/builder/')) continue;
|
|
34
|
+
const p = r.href.split('?')[0];
|
|
35
|
+
if (seen.has(p) || pinned.has(p)) continue;
|
|
36
|
+
seen.add(p);
|
|
30
37
|
const li = document.createElement('li');
|
|
31
38
|
const a = document.createElement('a');
|
|
32
39
|
a.href = r.href;
|
package/public/worksheet.css
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
screens: navigator | editor over output tabs, filling the window, with a status bar at the bottom. */
|
|
4
4
|
.ws-page { display: flex; flex-direction: column; padding: 8px 10px 0; background: var(--bg); }
|
|
5
5
|
.ws-page > .sw-tabs { margin-bottom: 6px; }
|
|
6
|
-
.ws { --ws-nav: 280px; --ws-out:
|
|
6
|
+
.ws { --ws-nav: 280px; --ws-out: 45%; display: grid; grid-template-columns: minmax(0, 1fr); gap: 0; background: var(--surface);
|
|
7
7
|
border: 1px solid var(--border); border-radius: var(--radius); min-width: 0; }
|
|
8
8
|
.ws-default { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); border: 0; padding: 0; }
|
|
9
9
|
.ws-nav { display: flex; flex-direction: column; min-width: 0; border-bottom: 1px solid var(--border); background: var(--surface-2); }
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
.ws-obj-actions { display: flex; flex-wrap: wrap; gap: 4px; padding: 4px 10px 8px 34px; }
|
|
33
33
|
.ws-obj-actions .btn { font-size: 11px; padding: 1px 6px; }
|
|
34
34
|
|
|
35
|
-
.ws-main { display: flex; flex-direction: column; min-width: 0; min-height: 0; }
|
|
35
|
+
.ws-main { display: flex; flex-direction: column; min-width: 0; min-height: 0; container-type: inline-size; }
|
|
36
36
|
.ws-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; padding: 6px 8px; border-bottom: 1px solid var(--border); }
|
|
37
37
|
.ws-toolbar .btn { display: inline-flex; align-items: center; gap: 4px; }
|
|
38
38
|
.ws-toolbar .icon { width: 15px; height: 15px; }
|
|
@@ -99,12 +99,16 @@
|
|
|
99
99
|
.js .ide-full .ws-editor .ce { flex: 1 1 auto; min-height: 0; grid-template-rows: minmax(0, 1fr) auto; border: 0; border-radius: 0; }
|
|
100
100
|
.js .ide-full .ws-editor .ce-main { min-height: 0; }
|
|
101
101
|
.js .ide-full .ws-editor .ce textarea.ce-input { height: 100%; min-height: 0; resize: none; }
|
|
102
|
-
.js .ide-full .ws-output { flex: 0
|
|
102
|
+
.js .ide-full .ws-output { flex: 0 1 var(--ws-out); }
|
|
103
|
+
/* (UX-1) the editor keeps at least ten lines; an empty output is only its tab strip until a run */
|
|
104
|
+
.js .ide-full .ws-editor { min-height: 13.5rem; }
|
|
105
|
+
.js .ide-full .ws-output[data-empty] { flex: 0 0 auto; }
|
|
106
|
+
.js .ide-full .ws-output[data-empty] .ws-pane { display: none; }
|
|
103
107
|
.js .ide-full .ws-pane { flex: 1 1 auto; }
|
|
104
108
|
.js .ide-full .ws-nav { overflow: hidden; }
|
|
105
109
|
}
|
|
106
110
|
@media (max-width: 899px) {
|
|
107
|
-
.ws-toolbar .btn span:not(.sr-only) { display: none; }
|
|
111
|
+
.ws-toolbar .btn span:not(.sr-only):not(.ws-dots) { display: none; }
|
|
108
112
|
.ws-toolbar .btn-hot span:not(.sr-only) { display: inline; }
|
|
109
113
|
}
|
|
110
114
|
/* fullscreen: only the editor and its toolbar */
|
|
@@ -299,3 +303,34 @@ details.menu.obj-menu > summary::before, details.menu.obj-menu > summary::after
|
|
|
299
303
|
.ws-txn-badge[data-state="open"] { color: var(--warn); border-color: color-mix(in srgb, var(--warn) 55%, var(--border)); background: color-mix(in srgb, var(--warn) 10%, transparent); font-weight: 600; }
|
|
300
304
|
.ws-txn-badge[data-state="aborted"] { color: var(--danger); border-color: var(--danger); background: var(--danger-soft); font-weight: 600; }
|
|
301
305
|
.ws-txn-alert { margin: 6px 8px; }
|
|
306
|
+
|
|
307
|
+
/* (UX-1) one toolbar row: the run buttons, Suggest / Check, Commit mode, fullscreen and a ⋯ menu with the rest */
|
|
308
|
+
/* it fits on one row; an open transaction (badge, Commit, Rollback) may take a second one */
|
|
309
|
+
.ws-toolbar > * { flex: none; }
|
|
310
|
+
/* the commit mode's options name it (Autocommit / Transaction): no label beside the select */
|
|
311
|
+
.ws-toolbar .ws-txn .ws-runas > span { display: none; }
|
|
312
|
+
@container (max-width: 950px) {
|
|
313
|
+
.ws-toolbar .btn:not(.btn-hot) > span:not(.sr-only):not(.ws-dots) { display: none; }
|
|
314
|
+
}
|
|
315
|
+
@container (max-width: 640px) {
|
|
316
|
+
.ws-toolbar > * { flex: 0 1 auto; min-width: 0; }
|
|
317
|
+
.ws-toolbar .ws-txn { flex-wrap: wrap; }
|
|
318
|
+
}
|
|
319
|
+
.ws-more .menu-panel { min-width: 18rem; max-height: calc(100dvh - 190px); overflow: auto; }
|
|
320
|
+
/* with script the worksheet tabs name the file: the file menu is its icon */
|
|
321
|
+
.js .ws-toolbar .ws-file-name { display: none; }
|
|
322
|
+
.ws-more-tools { display: flex; flex-direction: column; gap: 2px; }
|
|
323
|
+
.ws-menu-item { display: flex; align-items: center; gap: 8px; width: 100%; padding: 6px 8px; border: 0; border-radius: 6px; background: none;
|
|
324
|
+
color: var(--text); font: inherit; text-align: left; text-decoration: none; cursor: pointer; }
|
|
325
|
+
.ws-menu-item:hover, .ws-menu-item:focus-visible { background: var(--surface-2); }
|
|
326
|
+
.ws-menu-item .icon { width: 15px; height: 15px; }
|
|
327
|
+
.ws-menu-item kbd { margin-left: auto; font-size: 11.5px; color: var(--muted); }
|
|
328
|
+
.ws-more-tools .ws-check { padding: 6px 8px; }
|
|
329
|
+
.ws-more-head { display: block; margin-bottom: 4px; }
|
|
330
|
+
/* the code editor's hint bar under the editor: only while it has something to say (Check's result) */
|
|
331
|
+
.ws-editor .ce-bar:not(:has(.ce-msg:not(:empty))) { display: none; }
|
|
332
|
+
.ws-editor .ce-bar :is(.ce-hint, .ce-suggest, .ce-check) { display: none; }
|
|
333
|
+
/* (UX-1) who the statements run as: a chip at the right of the status bar */
|
|
334
|
+
.ws-status-run { display: flex; align-items: center; gap: 12px; min-width: 0; flex: 1 1 auto; }
|
|
335
|
+
.ws-runas-chip { flex: none; display: inline-flex; align-items: center; gap: 6px; }
|
|
336
|
+
.ws-runas-chip select { padding: 1px 22px 1px 6px; min-height: 0; font-size: 12.5px; }
|
package/public/worksheet.js
CHANGED
|
@@ -104,11 +104,15 @@
|
|
|
104
104
|
out.dataset.active = key;
|
|
105
105
|
};
|
|
106
106
|
show(out.dataset.active || 'result');
|
|
107
|
+
// (UX-1) before a run the output is only its tab strip; choosing a tab opens it
|
|
107
108
|
out.querySelector('[role=tablist]').addEventListener('click', (e) => {
|
|
108
109
|
const t = e.target.closest('[role=tab]');
|
|
109
|
-
if (t)
|
|
110
|
+
if (!t) return;
|
|
111
|
+
delete out.dataset.empty;
|
|
112
|
+
show(t.dataset.wsTab);
|
|
110
113
|
});
|
|
111
114
|
out.querySelector('[role=tablist]').addEventListener('keydown', (e) => {
|
|
115
|
+
delete out.dataset.empty;
|
|
112
116
|
const k = tabs.findIndex((t) => t.getAttribute('aria-selected') === 'true');
|
|
113
117
|
const next = { ArrowRight: k + 1, ArrowLeft: k - 1, Home: 0, End: tabs.length - 1 }[e.key];
|
|
114
118
|
if (next === undefined) return;
|
|
@@ -552,6 +556,16 @@
|
|
|
552
556
|
(k === 'h' ? fr : k === 'g' ? bar.querySelector('[data-ws-goto]') : fq).focus();
|
|
553
557
|
};
|
|
554
558
|
form.querySelector('[data-ws-find]').addEventListener('click', () => openFind('f'));
|
|
559
|
+
// (UX-1) Suggest and Check in the toolbar: the code editor's own buttons (its bar below the editor stays hidden
|
|
560
|
+
// until it has a message, such as Check's result)
|
|
561
|
+
const ceButton = (cls) => form.querySelector(`.ws-editor .${cls}`);
|
|
562
|
+
for (const [attr, cls] of [['data-ws-suggest', 'ce-suggest'], ['data-ws-check', 'ce-check']]) {
|
|
563
|
+
const btn = form.querySelector(`[${attr}]`);
|
|
564
|
+
btn?.addEventListener('click', () => ceButton(cls)?.click());
|
|
565
|
+
const sync = () => { if (btn) btn.hidden = !ceButton(cls) || ceButton(cls).hidden; };
|
|
566
|
+
sync();
|
|
567
|
+
window.addEventListener('load', sync);
|
|
568
|
+
}
|
|
555
569
|
form.querySelector('[data-ws-format]').addEventListener('click', format);
|
|
556
570
|
form.querySelector('[data-ws-case]').addEventListener('click', convertCase);
|
|
557
571
|
bar.addEventListener('keydown', (e) => {
|
package/src/builder/designer.ts
CHANGED
|
@@ -123,7 +123,7 @@ export async function designerRoutes(app: FastifyInstance) {
|
|
|
123
123
|
// (sprint 90) a commented-out component: struck through in the tree, Uncomment in its menu
|
|
124
124
|
const commented = (r: any) => (r.build_option === 'COMMENTED_OUT' ? raw(' data-commented') : '');
|
|
125
125
|
const leaf = (k: string, r: any, ic: Raw | '', label: string, note = '') =>
|
|
126
|
-
html`<li><a class="pd-node" href="${url(`c=${k}-${r.id}`)}"${cur(`${k}-${r.id}`)} data-kind="${k}" data-id="${r.id}"${commented(r)}>${ic}<span class="pd-label">${label}</span>${badges(r)}${note ? html`<span class="pd-note">${note}</span>` : ''}</a></li>`;
|
|
126
|
+
html`<li><a class="pd-node" href="${url(`c=${k}-${r.id}`)}"${cur(`${k}-${r.id}`)} data-kind="${k}" data-id="${r.id}" title="${note ? `${label} (${note})` : label}"${commented(r)}>${ic}<span class="pd-label">${label}</span>${badges(r)}${note ? html`<span class="pd-note">${note}</span>` : ''}</a></li>`;
|
|
127
127
|
const folder = (label: string, children: Raw[] | Raw, open = true) =>
|
|
128
128
|
html`<li${open ? '' : raw(' data-collapsed')}><span class="pd-node pd-folder"><span class="pd-label">${label}</span></span><ul>${children}</ul></li>`;
|
|
129
129
|
const none = (text: string) => html`<li class="pd-empty">${text}</li>`;
|
|
@@ -136,7 +136,7 @@ export async function designerRoutes(app: FastifyInstance) {
|
|
|
136
136
|
const bySlot = ['region', ...TOP_SLOTS, ...BOTTOM_SLOTS].map((sl) => [sl, buttons.filter((b) => (b.slot ?? 'region') === sl)] as const).filter(([, list]) => list.length);
|
|
137
137
|
const kids = [...(items.length ? [folder(tx('pd.tree.items'), items.map(itemLeaf))] : []),
|
|
138
138
|
...bySlot.map(([sl, list]) => folder(sl === 'region' ? tx('pd.tree.buttons') : tx('pd.tree.buttons_at', { slot: slotLabel(sl) }), list.map(buttonLeaf)))];
|
|
139
|
-
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>`;
|
|
139
|
+
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 ?? ''}" title="${regionTitle(r)} (${r.type})"${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>`;
|
|
140
140
|
};
|
|
141
141
|
// (sprint 77, APEX: the rendering tree by display position) the page template's positions, and others in use
|
|
142
142
|
const treeSide = p.page_template === 'left_side_column' ? 'left' : p.page_template === 'right_side_column' ? 'right' : null;
|
package/src/builder/gallery.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { migrate } from '../migrate.ts';
|
|
|
9
9
|
import { clientIp, logActivity } from '../session.ts';
|
|
10
10
|
import { openBlueprint } from './blueprints.ts';
|
|
11
11
|
import { isAdmin } from './locks.ts';
|
|
12
|
-
import { back, BASE, csrf, developer, flash, send, shell, type Req } from './ui.ts';
|
|
12
|
+
import { back, BASE, csrf, developer, flash, pageHeader, send, shell, type Req } from './ui.ts';
|
|
13
13
|
import { currentWorkspace, inWorkspaceSql, placeApp } from './workspaces.ts';
|
|
14
14
|
import { workspacesChanged } from '../workspace-db.ts';
|
|
15
15
|
import { scopedTx as tx, type BuilderKey } from './i18n.ts';
|
|
@@ -104,7 +104,7 @@ export async function galleryRoutes(app: FastifyInstance) {
|
|
|
104
104
|
const tabs = html`<nav class="gal-tabs segmented" aria-label="${tx('gal.kinds')}">${TABS.map(([t, l]) =>
|
|
105
105
|
html`<a href="${BASE}/gallery?tab=${t}${q ? `&q=${encodeURIComponent(q)}` : ''}"${tab === t ? raw(' aria-current="page"') : ''}>${tx(l)} <span class="gal-count">${counts[t]}</span></a>`)}</nav>`;
|
|
106
106
|
const blueprintCard = (kind: 'starters' | 'utilities') => (x: Starter) => card(x.icon, x.title, 'pgkiln', x.description,
|
|
107
|
-
html`<form method="post" action="${BASE}/gallery/${kind}/${x.key}">${csrf(s)}<button class="btn btn-
|
|
107
|
+
html`<form method="post" action="${BASE}/gallery/${kind}/${x.key}">${csrf(s)}<button class="btn btn-sm">${tx('gal.install')}</button></form>`);
|
|
108
108
|
const cards =
|
|
109
109
|
tab === 'samples'
|
|
110
110
|
? samples.filter(matches).map((x) => {
|
|
@@ -115,13 +115,13 @@ export async function galleryRoutes(app: FastifyInstance) {
|
|
|
115
115
|
? html`<a class="btn btn-sm" href="${BASE}/apps/${it.id}">${tx('gal.edit')}</a>`
|
|
116
116
|
: html`<span class="muted small">${tx('gal.other_workspace')}</span>`}`
|
|
117
117
|
: admin
|
|
118
|
-
? html`<form method="post" action="${BASE}/gallery/samples/${x.key}">${csrf(s)}<button class="btn btn-
|
|
118
|
+
? html`<form method="post" action="${BASE}/gallery/samples/${x.key}">${csrf(s)}<button class="btn btn-sm" data-confirm="${tx('gal.confirm_install', { title: x.title })}">${tx('gal.install')}</button></form>`
|
|
119
119
|
: html`<span class="muted small">${tx('gal.admin_installs')}</span>`);
|
|
120
120
|
})
|
|
121
121
|
: tab === 'starters' || tab === 'utilities'
|
|
122
122
|
? (tab === 'starters' ? starters : utilities).filter(matches).map(blueprintCard(tab))
|
|
123
123
|
: custom.filter(matches).map((x) => card('copy', x.name, tx('gal.this_workspace'), x.description ?? tx('gal.boilerplate_desc'),
|
|
124
|
-
html`<a class="btn btn-
|
|
124
|
+
html`<a class="btn btn-sm" href="${BASE}/create?boilerplate=${x.id}">${tx('gal.create_from')}</a><a class="btn btn-sm" href="${BASE}/apps/${x.id}">${tx('gal.edit')}</a>`));
|
|
125
125
|
const empty = q && counts[tab]
|
|
126
126
|
? html`<p class="muted">${tx.rich('gal.no_match', { q: html`<strong>${q}</strong>` })} <a href="${BASE}/gallery?tab=${tab}">${tx('gal.show_all')}</a></p>`
|
|
127
127
|
: tab === 'custom'
|
|
@@ -129,13 +129,13 @@ export async function galleryRoutes(app: FastifyInstance) {
|
|
|
129
129
|
: html`<p class="muted">${tx('gal.empty')}</p>`;
|
|
130
130
|
const intro = tx(({ samples: 'gal.intro.samples', starters: 'gal.intro.starters', utilities: 'gal.intro.utilities', custom: 'gal.intro.custom' } as const)[tab]);
|
|
131
131
|
const main = html`<div class="gal">
|
|
132
|
+
${pageHeader(tx('rail.gallery'))}
|
|
132
133
|
<div class="gal-bar">
|
|
133
134
|
<form class="ab-search gal-search" role="search" method="get" action="${BASE}/gallery"><label class="sr-only" for="gal-q">${tx('gal.search')}</label>${icon('search')}
|
|
134
135
|
<input type="hidden" name="tab" value="${tab}">
|
|
135
136
|
<input id="gal-q" name="q" type="search" placeholder="${tx('gal.search')}" autocomplete="off" value="${q}" data-filter-list=".gal [data-filter-row]"></form>
|
|
136
137
|
${tabs}
|
|
137
138
|
</div>
|
|
138
|
-
<h1 class="sr-only">${tx('rail.gallery')}</h1>
|
|
139
139
|
<p class="muted">${intro}</p>
|
|
140
140
|
${cards.length ? html`<div class="gal-grid">${cards}</div>` : empty}
|
|
141
141
|
</div>`;
|