pgkiln 0.45.0 → 0.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/db/migrations/106_form_designer.sql +102 -0
- package/db/migrations/107_app_version.sql +9 -0
- package/db/migrations/108_sql_history.sql +25 -0
- package/docs/apex-feature-parity.md +33 -4
- package/docs/guide/03-builder.md +123 -28
- package/docs/guide/04-pages-and-regions.md +74 -2
- package/docs/guide/05-items.md +14 -2
- package/docs/guide/08-security.md +1 -1
- package/docs/guide/09-reference.md +1 -1
- package/docs/guide/10-tutorial.md +1 -1
- package/docs/guide/11-from-apex.md +2 -1
- package/docs/guide/12-development.md +26 -3
- package/docs/guide/16-files.md +2 -2
- package/examples/hr/hr_61_form_designer.sql +80 -0
- package/package.json +1 -1
- package/public/app.css +62 -2
- package/public/app.js +244 -35
- package/public/builder.css +21 -1
- package/public/builder.js +67 -0
- package/public/sw.js +2 -1
- package/public/worksheet.css +244 -0
- package/public/worksheet.js +665 -0
- package/src/app.ts +9 -1
- package/src/builder/ai-builder.ts +3 -3
- package/src/builder/appwizard.ts +22 -6
- package/src/builder/arrange.ts +33 -12
- package/src/builder/code-editor.ts +1 -1
- package/src/builder/components.ts +1 -1
- package/src/builder/designer.ts +44 -17
- package/src/builder/errors.ts +65 -0
- package/src/builder/form-designer.ts +229 -0
- package/src/builder/locks.ts +6 -0
- package/src/builder/newapp.ts +2 -1
- package/src/builder/object-actions.ts +166 -0
- package/src/builder/object-browser.ts +111 -33
- package/src/builder/querybuilder.ts +1 -1
- package/src/builder/region-help.ts +2 -0
- package/src/builder/region-settings.ts +2 -0
- package/src/builder/reporter.ts +1 -1
- package/src/builder/routes.ts +7 -5
- package/src/builder/sampledata.ts +2 -2
- package/src/builder/scripts.ts +3 -3
- package/src/builder/search.ts +3 -3
- package/src/builder/shared.ts +2 -2
- package/src/builder/sql.ts +15 -73
- package/src/builder/table-editor.ts +219 -0
- package/src/builder/ui.ts +5 -3
- package/src/builder/unload.ts +8 -4
- package/src/builder/worksheet.ts +569 -0
- package/src/builder/workspaces.ts +1 -1
- package/src/cli/replace.ts +10 -0
- package/src/i18n/ar.ts +62 -0
- package/src/i18n/cs.ts +62 -0
- package/src/i18n/da.ts +62 -0
- package/src/i18n/de.ts +62 -0
- package/src/i18n/el.ts +62 -0
- package/src/i18n/es.ts +62 -0
- package/src/i18n/fi.ts +62 -0
- package/src/i18n/fr.ts +62 -0
- package/src/i18n/he.ts +62 -0
- package/src/i18n/it.ts +62 -0
- package/src/i18n/ja.ts +62 -0
- package/src/i18n/ko.ts +62 -0
- package/src/i18n/nb.ts +62 -0
- package/src/i18n/pl.ts +62 -0
- package/src/i18n/pt.ts +62 -0
- package/src/i18n/ru.ts +62 -0
- package/src/i18n/sv.ts +62 -0
- package/src/i18n/tr.ts +62 -0
- package/src/i18n/uk.ts +62 -0
- package/src/i18n/zh.ts +62 -0
- package/src/i18n.ts +124 -0
- package/src/metadata.ts +4 -2
- package/src/runtime/devbar.ts +1 -1
- package/src/runtime/facet-state.ts +10 -5
- package/src/runtime/facets.ts +29 -5
- package/src/runtime/form-designer.ts +676 -0
- package/src/runtime/grid.ts +15 -8
- package/src/runtime/items.ts +33 -21
- package/src/runtime/lists.ts +1 -1
- package/src/runtime/pdf.ts +2 -1
- package/src/runtime/regions.ts +4 -0
- package/src/runtime/render.ts +7 -3
- package/src/runtime/report.ts +2 -2
- package/src/runtime/routes.ts +3 -1
- package/src/runtime/smart-filters.ts +2 -1
- package/src/runtime/tasks.ts +6 -3
- package/src/sqlformat.ts +208 -0
- package/src/sqlplan.ts +133 -0
- package/src/sqlscript.ts +6 -2
- package/src/sqlworksheet.ts +278 -0
- package/src/tabledef.ts +226 -0
- package/src/unload.ts +14 -3
package/src/i18n.ts
CHANGED
|
@@ -90,6 +90,7 @@ const en = {
|
|
|
90
90
|
'common.back': 'Go back',
|
|
91
91
|
'common.home': 'Go to the home page',
|
|
92
92
|
'common.skip': 'Skip to content',
|
|
93
|
+
'common.back_to_top': "Back to top",
|
|
93
94
|
'common.toggle_nav': 'Toggle navigation',
|
|
94
95
|
'common.dismiss': 'Dismiss',
|
|
95
96
|
// messages and errors
|
|
@@ -417,6 +418,9 @@ const en = {
|
|
|
417
418
|
'facets.stars_up': '{n} stars and up',
|
|
418
419
|
'facets.star_up': '1 star and up',
|
|
419
420
|
'facets.clear_short': 'Clear',
|
|
421
|
+
'facets.menu': 'Options of {facet}',
|
|
422
|
+
'facets.sort_count': 'Sort by count',
|
|
423
|
+
'facets.sort_name': 'Sort by name',
|
|
420
424
|
'facets.show_all': 'Show all ({n})',
|
|
421
425
|
'facets.total': 'Total row count {n}',
|
|
422
426
|
'smart.placeholder': 'Search or filter…',
|
|
@@ -712,6 +716,64 @@ const en = {
|
|
|
712
716
|
'reporter.changed': "changed {date}",
|
|
713
717
|
'reporter.no_match': "No reports match the search.",
|
|
714
718
|
'reporter.chart_tag': "chart",
|
|
719
|
+
// Form Designer (form-designer.ts)
|
|
720
|
+
'formdesigner.no_sources': "No tables are set up for this region yet.",
|
|
721
|
+
'formdesigner.no_forms': "There are no forms yet.",
|
|
722
|
+
'formdesigner.new': "New form",
|
|
723
|
+
'formdesigner.create': "Design",
|
|
724
|
+
'formdesigner.draft': "Draft",
|
|
725
|
+
'formdesigner.design': "Design form",
|
|
726
|
+
'formdesigner.design_of': "Design of {name}",
|
|
727
|
+
'formdesigner.back': "All forms",
|
|
728
|
+
'formdesigner.add': "Add",
|
|
729
|
+
'formdesigner.edit_row': "Edit",
|
|
730
|
+
'formdesigner.rows': "Rows of {form}",
|
|
731
|
+
'formdesigner.save': "Save",
|
|
732
|
+
'formdesigner.delete': "Delete",
|
|
733
|
+
'formdesigner.delete_confirm': "Delete this row?",
|
|
734
|
+
'formdesigner.cancel': "Cancel",
|
|
735
|
+
'formdesigner.saved_row': "Saved.",
|
|
736
|
+
'formdesigner.deleted_row': "Deleted.",
|
|
737
|
+
'formdesigner.required': "{field} is required.",
|
|
738
|
+
'formdesigner.not_in_list': "{field}: choose a value from the list.",
|
|
739
|
+
'formdesigner.changed': "Someone else changed this row in the meantime. Check it and save again.",
|
|
740
|
+
'formdesigner.row_not_found': "This row does not exist, or you may not see it.",
|
|
741
|
+
'formdesigner.not_allowed': "You may not do that with this row.",
|
|
742
|
+
'formdesigner.form_not_found': "This form does not exist (any more).",
|
|
743
|
+
'formdesigner.unknown_source': "This form's table is no longer offered.",
|
|
744
|
+
'formdesigner.nothing': "This form has no fields yet.",
|
|
745
|
+
'formdesigner.choose': "- Choose -",
|
|
746
|
+
'formdesigner.name': "Name",
|
|
747
|
+
'formdesigner.description': "Description",
|
|
748
|
+
'formdesigner.published': "Published: everyone who can see this page may use the form",
|
|
749
|
+
'formdesigner.fields': "Fields",
|
|
750
|
+
'formdesigner.field_on': "On the form",
|
|
751
|
+
'formdesigner.column': "Column",
|
|
752
|
+
'formdesigner.key': "key",
|
|
753
|
+
'formdesigner.order': "Order",
|
|
754
|
+
'formdesigner.label': "Label",
|
|
755
|
+
'formdesigner.help': "Help text",
|
|
756
|
+
'formdesigner.required_short': "Required",
|
|
757
|
+
'formdesigner.display': "Display",
|
|
758
|
+
'formdesigner.default': "Default",
|
|
759
|
+
'formdesigner.in_list': "In the list",
|
|
760
|
+
'formdesigner.display_auto': "Field",
|
|
761
|
+
'formdesigner.display_textarea': "Text area",
|
|
762
|
+
'formdesigner.display_select': "Select list",
|
|
763
|
+
'formdesigner.display_radio': "Radio group",
|
|
764
|
+
'formdesigner.actions': "Users may",
|
|
765
|
+
'formdesigner.allow_insert': "add rows",
|
|
766
|
+
'formdesigner.allow_update': "change rows",
|
|
767
|
+
'formdesigner.allow_delete': "delete rows",
|
|
768
|
+
'formdesigner.list_help': "The list of rows shows the columns ticked \"In the list\"; without any, the form only adds rows.",
|
|
769
|
+
'formdesigner.save_form': "Save form",
|
|
770
|
+
'formdesigner.delete_form': "Delete form",
|
|
771
|
+
'formdesigner.delete_form_confirm': "Delete the form \"{name}\"? The rows it saved stay.",
|
|
772
|
+
'formdesigner.form_saved': "Form \"{name}\" saved.",
|
|
773
|
+
'formdesigner.form_deleted': "Form deleted.",
|
|
774
|
+
'formdesigner.name_required': "Give the form a name.",
|
|
775
|
+
'formdesigner.no_fields': "Put at least one column on the form.",
|
|
776
|
+
'formdesigner.changed_by': "changed by {user} on {date}",
|
|
715
777
|
// AI assistant region and natural-language report filters (assistant.ts, ai-filter.ts)
|
|
716
778
|
'assistant.conversation': "Conversation",
|
|
717
779
|
'assistant.you': "You",
|
|
@@ -799,6 +861,7 @@ const nl: Record<MessageKey, string> = {
|
|
|
799
861
|
'common.back': 'Terug',
|
|
800
862
|
'common.home': 'Naar de startpagina',
|
|
801
863
|
'common.skip': 'Naar de inhoud',
|
|
864
|
+
'common.back_to_top': "Naar boven",
|
|
802
865
|
'common.toggle_nav': 'Navigatie tonen of verbergen',
|
|
803
866
|
'common.dismiss': 'Sluiten',
|
|
804
867
|
'error.not_found': 'Niet gevonden',
|
|
@@ -1123,6 +1186,9 @@ const nl: Record<MessageKey, string> = {
|
|
|
1123
1186
|
'facets.stars_up': '{n} sterren of meer',
|
|
1124
1187
|
'facets.star_up': '1 ster of meer',
|
|
1125
1188
|
'facets.clear_short': 'Wissen',
|
|
1189
|
+
'facets.menu': 'Opties van {facet}',
|
|
1190
|
+
'facets.sort_count': 'Sorteren op aantal',
|
|
1191
|
+
'facets.sort_name': 'Sorteren op naam',
|
|
1126
1192
|
'facets.show_all': 'Alle tonen ({n})',
|
|
1127
1193
|
'facets.total': 'Totaal aantal rijen {n}',
|
|
1128
1194
|
'smart.placeholder': 'Zoeken of filteren…',
|
|
@@ -1417,6 +1483,64 @@ const nl: Record<MessageKey, string> = {
|
|
|
1417
1483
|
'reporter.changed': "gewijzigd {date}",
|
|
1418
1484
|
'reporter.no_match': "Geen rapporten gevonden.",
|
|
1419
1485
|
'reporter.chart_tag': "grafiek",
|
|
1486
|
+
// Form Designer (form-designer.ts)
|
|
1487
|
+
'formdesigner.no_sources': "Voor deze regio zijn nog geen tabellen ingesteld.",
|
|
1488
|
+
'formdesigner.no_forms': "Er zijn nog geen formulieren.",
|
|
1489
|
+
'formdesigner.new': "Nieuw formulier",
|
|
1490
|
+
'formdesigner.create': "Ontwerpen",
|
|
1491
|
+
'formdesigner.draft': "Concept",
|
|
1492
|
+
'formdesigner.design': "Formulier ontwerpen",
|
|
1493
|
+
'formdesigner.design_of': "Ontwerp van {name}",
|
|
1494
|
+
'formdesigner.back': "Alle formulieren",
|
|
1495
|
+
'formdesigner.add': "Toevoegen",
|
|
1496
|
+
'formdesigner.edit_row': "Bewerken",
|
|
1497
|
+
'formdesigner.rows': "Rijen van {form}",
|
|
1498
|
+
'formdesigner.save': "Opslaan",
|
|
1499
|
+
'formdesigner.delete': "Verwijderen",
|
|
1500
|
+
'formdesigner.delete_confirm': "Deze rij verwijderen?",
|
|
1501
|
+
'formdesigner.cancel': "Annuleren",
|
|
1502
|
+
'formdesigner.saved_row': "Opgeslagen.",
|
|
1503
|
+
'formdesigner.deleted_row': "Verwijderd.",
|
|
1504
|
+
'formdesigner.required': "{field} is verplicht.",
|
|
1505
|
+
'formdesigner.not_in_list': "{field}: kies een waarde uit de lijst.",
|
|
1506
|
+
'formdesigner.changed': "Iemand anders heeft deze rij intussen gewijzigd. Controleer haar en sla opnieuw op.",
|
|
1507
|
+
'formdesigner.row_not_found': "Deze rij bestaat niet, of je mag haar niet zien.",
|
|
1508
|
+
'formdesigner.not_allowed': "Dat mag je met deze rij niet doen.",
|
|
1509
|
+
'formdesigner.form_not_found': "Dit formulier bestaat niet (meer).",
|
|
1510
|
+
'formdesigner.unknown_source': "De tabel van dit formulier wordt niet meer aangeboden.",
|
|
1511
|
+
'formdesigner.nothing': "Dit formulier heeft nog geen velden.",
|
|
1512
|
+
'formdesigner.choose': "- Kies -",
|
|
1513
|
+
'formdesigner.name': "Naam",
|
|
1514
|
+
'formdesigner.description': "Omschrijving",
|
|
1515
|
+
'formdesigner.published': "Gepubliceerd: iedereen die deze pagina kan zien mag het formulier gebruiken",
|
|
1516
|
+
'formdesigner.fields': "Velden",
|
|
1517
|
+
'formdesigner.field_on': "Op het formulier",
|
|
1518
|
+
'formdesigner.column': "Kolom",
|
|
1519
|
+
'formdesigner.key': "sleutel",
|
|
1520
|
+
'formdesigner.order': "Volgorde",
|
|
1521
|
+
'formdesigner.label': "Label",
|
|
1522
|
+
'formdesigner.help': "Helptekst",
|
|
1523
|
+
'formdesigner.required_short': "Verplicht",
|
|
1524
|
+
'formdesigner.display': "Weergave",
|
|
1525
|
+
'formdesigner.default': "Standaard",
|
|
1526
|
+
'formdesigner.in_list': "In de lijst",
|
|
1527
|
+
'formdesigner.display_auto': "Veld",
|
|
1528
|
+
'formdesigner.display_textarea': "Tekstvak",
|
|
1529
|
+
'formdesigner.display_select': "Keuzelijst",
|
|
1530
|
+
'formdesigner.display_radio': "Keuzerondjes",
|
|
1531
|
+
'formdesigner.actions': "Gebruikers mogen",
|
|
1532
|
+
'formdesigner.allow_insert': "rijen toevoegen",
|
|
1533
|
+
'formdesigner.allow_update': "rijen wijzigen",
|
|
1534
|
+
'formdesigner.allow_delete': "rijen verwijderen",
|
|
1535
|
+
'formdesigner.list_help': "De lijst met rijen toont de kolommen met een vinkje bij \"In de lijst\"; zonder vinkjes voegt het formulier alleen rijen toe.",
|
|
1536
|
+
'formdesigner.save_form': "Formulier opslaan",
|
|
1537
|
+
'formdesigner.delete_form': "Formulier verwijderen",
|
|
1538
|
+
'formdesigner.delete_form_confirm': "Het formulier \"{name}\" verwijderen? De rijen die ermee zijn opgeslagen blijven bestaan.",
|
|
1539
|
+
'formdesigner.form_saved': "Formulier \"{name}\" opgeslagen.",
|
|
1540
|
+
'formdesigner.form_deleted': "Formulier verwijderd.",
|
|
1541
|
+
'formdesigner.name_required': "Geef het formulier een naam.",
|
|
1542
|
+
'formdesigner.no_fields': "Zet minstens één kolom op het formulier.",
|
|
1543
|
+
'formdesigner.changed_by': "gewijzigd door {user} op {date}",
|
|
1420
1544
|
// AI assistant region and natural-language report filters (assistant.ts, ai-filter.ts)
|
|
1421
1545
|
'assistant.conversation': "Gesprek",
|
|
1422
1546
|
'assistant.you': "Jij",
|
package/src/metadata.ts
CHANGED
|
@@ -98,6 +98,8 @@ export interface App {
|
|
|
98
98
|
availability_users: string[];
|
|
99
99
|
/** (084) users can send feedback from the navigation bar */
|
|
100
100
|
feedback: boolean;
|
|
101
|
+
/** (107) the application's version (Release 1.0), shown in its pages' footer */
|
|
102
|
+
version: string | null;
|
|
101
103
|
/** (085) schema.function(jsonb) returns jsonb that rewrites error messages; seconds of the session timeout warning */
|
|
102
104
|
error_function: string | null;
|
|
103
105
|
session_warning_seconds: number;
|
|
@@ -146,7 +148,7 @@ export interface Region {
|
|
|
146
148
|
title: string | null;
|
|
147
149
|
/** (0.31, 073) optional, unique on the page */
|
|
148
150
|
static_id?: string | null;
|
|
149
|
-
type: 'login' | 'report' | 'form' | 'chart' | 'cards' | 'static' | 'grid' | 'calendar' | 'dynamic' | 'facets' | 'tasks' | 'workflows' | 'map' | 'tree' | 'template_component' | 'smart_filters' | 'display_selector' | 'list' | 'data_reporter' | 'ai_assistant' | 'plugin' | 'url' | 'search';
|
|
151
|
+
type: 'login' | 'report' | 'form' | 'chart' | 'cards' | 'static' | 'grid' | 'calendar' | 'dynamic' | 'facets' | 'tasks' | 'workflows' | 'map' | 'tree' | 'template_component' | 'smart_filters' | 'display_selector' | 'list' | 'data_reporter' | 'ai_assistant' | 'plugin' | 'url' | 'search' | 'form_designer';
|
|
150
152
|
source: string | null;
|
|
151
153
|
table_name: string | null;
|
|
152
154
|
pk_column: string | null;
|
|
@@ -359,7 +361,7 @@ const agg = (table: string, fk: string, parent: string, appId: string) =>
|
|
|
359
361
|
// No caching on purpose: edits made in the builder show up on the next request.
|
|
360
362
|
export async function loadApp(alias: string) {
|
|
361
363
|
return runtime.one<App>(
|
|
362
|
-
`select a.id, a.alias, a.name, a.home_page, a.authentication, a.access_control, a.sso_providers, a.local_login, a.remember_me_days, a.login_page, a.password_reset, a.session_group, a.ldap_directories, a.header_name, a.header_auto_create, a.logout_url, a.db_auth_roles, a.db_auth_member_of, a.custom_auth_function, a.custom_auth_code, a.custom_auth_post_code, a.nav_list, a.navbar_list, a.pwa, a.pwa_short_name, a.pwa_icon is not null as pwa_has_icon, a.pwa_offline_pages, a.pwa_offline_submit, a.pwa_push, a.db_role, a.debug, a.debug_level, a.theme,
|
|
364
|
+
`select a.id, a.alias, a.name, a.version, a.home_page, a.authentication, a.access_control, a.sso_providers, a.local_login, a.remember_me_days, a.login_page, a.password_reset, a.session_group, a.ldap_directories, a.header_name, a.header_auto_create, a.logout_url, a.db_auth_roles, a.db_auth_member_of, a.custom_auth_function, a.custom_auth_code, a.custom_auth_post_code, a.nav_list, a.navbar_list, a.pwa, a.pwa_short_name, a.pwa_icon is not null as pwa_has_icon, a.pwa_offline_pages, a.pwa_offline_submit, a.pwa_push, a.db_role, a.debug, a.debug_level, a.theme,
|
|
363
365
|
a.availability, a.availability_message, a.availability_users, a.feedback, a.error_function, a.session_warning_seconds, a.component_settings,
|
|
364
366
|
a.language, a.languages, a.language_from, a.date_format, a.timestamp_format, a.time_zone, a.time_zone_auto, a.currency,
|
|
365
367
|
coalesce((select jsonb_agg(jsonb_build_object('name', l.name, 'query', l.query, 'rest_source', l.rest_source)) from meta.lov l where l.app_id = a.id), '[]') as lovs,
|
package/src/runtime/devbar.ts
CHANGED
|
@@ -97,7 +97,7 @@ export function devbarHtml(ctx: PageContext): Raw | '' {
|
|
|
97
97
|
<nav aria-label="Developer toolbar"><ul class="devbar-tools">
|
|
98
98
|
${tool(BUILDER, 'App Builder', 'home', 'Home')}
|
|
99
99
|
${tool(`${BUILDER}/apps/${ctx.app.id}`, `Application ${ctx.app.id}`, 'grid', `App ${ctx.app.id}`)}
|
|
100
|
-
${tool(`${BUILDER}/
|
|
100
|
+
${tool(`${BUILDER}/apps/${ctx.app.id}/pages/${page}`, `Edit page ${page}`, 'edit', `Page ${page}`)}
|
|
101
101
|
${tool(`${ctx.base}/${page}/devbar/session`, 'Session', 'database', 'Session')}
|
|
102
102
|
${menu(`Debug: ${level || appLevel ? 'on' : 'off'}`, 'bolt', `devbar-debug-menu${level || appLevel ? ' is-on' : ''}`, html`<form method="post" action="${ctx.base}/${page}/devbar/debug" class="devbar-debug">
|
|
103
103
|
<input type="hidden" name="__csrf" value="${ctx.session.csrf_token}">
|
|
@@ -174,7 +174,10 @@ export const isFacetParam = (reportId: number, name: string) => Object.values(fa
|
|
|
174
174
|
* The facet filters on a report that its facets allow (except those on one
|
|
175
175
|
* column: a facet's own counts ignore its own selection).
|
|
176
176
|
*/
|
|
177
|
-
|
|
177
|
+
/** (sprint 84) Reads a custom range's bound typed in the application's date format as YYYY-MM-DD (null: not a date in it). */
|
|
178
|
+
export type DateReader = (text: string) => string | null;
|
|
179
|
+
|
|
180
|
+
export function facetFilters(params: URLSearchParams, reportId: number, defs: Map<string, FacetDef>, except?: string, readDate?: DateReader): FacetFilter[] {
|
|
178
181
|
const k = facetKeys(reportId);
|
|
179
182
|
const out: FacetFilter[] = [];
|
|
180
183
|
for (const def of defs.values()) {
|
|
@@ -194,8 +197,10 @@ export function facetFilters(params: URLSearchParams, reportId: number, defs: Ma
|
|
|
194
197
|
const r = parseRange(chosen);
|
|
195
198
|
if (r && def.ranges.some((x) => x.from === r.from && x.to === r.to)) out.push({ column, kind: 'range', from: r.from, to: r.to, custom: false });
|
|
196
199
|
} else if (def.custom) {
|
|
197
|
-
|
|
198
|
-
const
|
|
200
|
+
// (sprint 84) a date bound in the date format is read as YYYY-MM-DD; a number stays as it is
|
|
201
|
+
const bound = (v: string) => (v && readDate && !/^-?\d+(\.\d+)?$/.test(v) ? (readDate(v) ?? v) : v);
|
|
202
|
+
const from = bound((params.get(k.from + column) ?? '').trim());
|
|
203
|
+
const to = bound((params.get(k.to + column) ?? '').trim());
|
|
199
204
|
if ((from || to) && anyBound(from) && anyBound(to)) out.push({ column, kind: 'range', from, to, custom: true });
|
|
200
205
|
}
|
|
201
206
|
}
|
|
@@ -235,8 +240,8 @@ export function rangeSql(c: string, oid: number | undefined, r: { from: string;
|
|
|
235
240
|
}
|
|
236
241
|
|
|
237
242
|
/** The conditions of all applicable filters. */
|
|
238
|
-
export function facetWhere(params: URLSearchParams, reportId: number, defs: Map<string, FacetDef>, cols: Map<string, number>, p: SqlParams, except?: string): string[] {
|
|
239
|
-
return facetFilters(params, reportId, defs, except)
|
|
243
|
+
export function facetWhere(params: URLSearchParams, reportId: number, defs: Map<string, FacetDef>, cols: Map<string, number>, p: SqlParams, except?: string, readDate?: DateReader): string[] {
|
|
244
|
+
return facetFilters(params, reportId, defs, except, readDate)
|
|
240
245
|
.map((f) => facetFilterSql(f, cols, p))
|
|
241
246
|
.filter((s): s is string => s !== null);
|
|
242
247
|
}
|
package/src/runtime/facets.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { savepoint } from '../db.ts';
|
|
|
4
4
|
import { html, raw, type Raw } from '../html.ts';
|
|
5
5
|
import type { Region } from '../metadata.ts';
|
|
6
6
|
import { bindValues, publicError, stripSemicolon, toState, type PageContext } from './context.ts';
|
|
7
|
-
import { heading } from './items.ts';
|
|
7
|
+
import { appDateMask, appDateReader, dateField, heading } from './items.ts';
|
|
8
8
|
import { fieldsOf, regionUrl } from './report.ts';
|
|
9
9
|
import { renderChartBody } from './charts.ts';
|
|
10
10
|
import {
|
|
@@ -54,7 +54,7 @@ const col = (name: string) => `"__q".${pg.escapeIdentifier(name)}`;
|
|
|
54
54
|
|
|
55
55
|
/** The search and every other facet's filter, as conditions (their values in p). */
|
|
56
56
|
function othersWhere(ctx: PageContext, fs: FacetSource, except: string, p: SqlParams, withSearch = true) {
|
|
57
|
-
const where = facetWhere(ctx.params, fs.report.id, fs.all, fs.cols, p, except);
|
|
57
|
+
const where = facetWhere(ctx.params, fs.report.id, fs.all, fs.cols, p, except, appDateReader(ctx));
|
|
58
58
|
if (withSearch && fs.search) where.unshift(searchSql(fs.search, p, hidesColumns(fs.report) ? userColumnNames(fs.report, [...fs.cols.keys()]) : undefined));
|
|
59
59
|
return where;
|
|
60
60
|
}
|
|
@@ -99,6 +99,9 @@ export async function rangeCounts(ctx: PageContext, fs: FacetSource, f: FacetDef
|
|
|
99
99
|
return res.rows[0] as number[];
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
/** (sprint 84) The URL parameter of a checkbox facet's order: "name", else by count. Not a filter: Apply and Reset keep it. */
|
|
103
|
+
export const facetSortKey = (reportId: number, column: string) => `r${reportId}_fo_${column}`;
|
|
104
|
+
|
|
102
105
|
/** (sprint 78) Checkbox facet values shown before "Show all" (APEX shows seven). */
|
|
103
106
|
export const FACET_SHOWN = 7;
|
|
104
107
|
|
|
@@ -141,13 +144,28 @@ export async function renderFacets(ctx: PageContext, r: Region): Promise<Raw> {
|
|
|
141
144
|
const k = facetKeys(report.id);
|
|
142
145
|
const formId = `ff${r.id}`;
|
|
143
146
|
const defs = facetDefs(r.config).filter((f) => fs.cols.has(f.column));
|
|
144
|
-
const active = new Set(facetFilters(ctx.params, report.id, fs.all).map((f) => f.column));
|
|
147
|
+
const active = new Set(facetFilters(ctx.params, report.id, fs.all, undefined, appDateReader(ctx)).map((f) => f.column));
|
|
145
148
|
const groups: Raw[] = [];
|
|
146
149
|
const clearLink = (f: FacetDef) =>
|
|
147
150
|
active.has(f.column)
|
|
148
151
|
? html` <a class="facet-clear" href="${regionUrl(ctx, report, (p) => { for (const n of facetParamNames(report.id, f.column)) p.delete(n); p.delete(`r${report.id}_p`); })}" aria-label="${t('facets.clear_one', { facet: facetLabel(ctx, f) })}">${t('facets.clear_short')}</a>`
|
|
149
152
|
: '';
|
|
150
153
|
|
|
154
|
+
// (sprint 84; APEX: a facet's ⋮ menu) Clear, and the values sorted by count or by name; links, so it works without JavaScript
|
|
155
|
+
const facetMenu = (f: FacetDef, label: string, byName: boolean) => {
|
|
156
|
+
const sortHref = (by: 'count' | 'name') => regionUrl(ctx, report, (p) => {
|
|
157
|
+
if (by === 'name') p.set(facetSortKey(report.id, f.column), 'name');
|
|
158
|
+
else p.delete(facetSortKey(report.id, f.column));
|
|
159
|
+
});
|
|
160
|
+
const current = (on: boolean) => (on ? raw(' aria-current="true"') : '');
|
|
161
|
+
return html`<details class="menu facet-menu"><summary aria-label="${t('facets.menu', { facet: label })}"><span aria-hidden="true">⋮</span></summary>
|
|
162
|
+
<div class="menu-panel"><div class="menu-section menu-links">
|
|
163
|
+
${active.has(f.column) ? html`<a href="${regionUrl(ctx, report, (p) => { for (const n of facetParamNames(report.id, f.column)) p.delete(n); p.delete(`r${report.id}_p`); })}">${t('facets.clear_short')}</a>` : ''}
|
|
164
|
+
<a href="${sortHref('count')}"${current(!byName)}>${t('facets.sort_count')}</a>
|
|
165
|
+
<a href="${sortHref('name')}"${current(byName)}>${t('facets.sort_name')}</a>
|
|
166
|
+
</div></div></details>`;
|
|
167
|
+
};
|
|
168
|
+
|
|
151
169
|
// the search facet: the report's own search, in the facet panel
|
|
152
170
|
if (r.config.search === true) {
|
|
153
171
|
groups.push(html`<div class="facet facet-search">
|
|
@@ -169,10 +187,13 @@ export async function renderFacets(ctx: PageContext, r: Region): Promise<Raw> {
|
|
|
169
187
|
const selected = new Set(ctx.params.getAll(k.values + f.column));
|
|
170
188
|
// keep selected values visible even when their count dropped to 0
|
|
171
189
|
for (const s of selected) if (s && !values.some((x) => x.v === s)) values.push({ v: s, n: 0 });
|
|
190
|
+
// (sprint 84; APEX: the facet's menu) the values by count (the query's order) or by name
|
|
191
|
+
const byName = ctx.params.get(facetSortKey(report.id, f.column)) === 'name';
|
|
172
192
|
const excluded = f.exclude && ctx.params.get(k.exclude + f.column) === '1';
|
|
173
193
|
// (sprint 78) a boolean's values as Yes and No, not true and false
|
|
174
194
|
const bool = fs.cols.get(f.column) === 16;
|
|
175
195
|
const shown = (v: string) => (bool && (v === 'true' || v === 'false') ? t(v === 'true' ? 'item.yes' : 'item.no') : (toState(v) ?? ''));
|
|
196
|
+
if (byName) values.sort((a, b) => shown(a.v).localeCompare(shown(b.v), ctx.locale.lang, { numeric: true, sensitivity: 'base' }));
|
|
176
197
|
const chart = facetChart(ctx, report, f, label, values.map((x) => ({
|
|
177
198
|
label: shown(x.v), n: x.n,
|
|
178
199
|
apply: (p) => { p.delete(k.values + f.column); p.delete(k.exclude + f.column); p.append(k.values + f.column, x.v); },
|
|
@@ -181,7 +202,7 @@ export async function renderFacets(ctx: PageContext, r: Region): Promise<Raw> {
|
|
|
181
202
|
html`<label class="check facet-value"><input type="checkbox" form="${formId}" name="${k.values}${f.column}" value="${x.v}"${selected.has(x.v) ? raw(' checked') : ''} data-facet>
|
|
182
203
|
<span class="facet-label">${shown(x.v)}</span><span class="facet-count">${x.n}</span></label>`;
|
|
183
204
|
groups.push(html`<fieldset class="facet${excluded ? ' facet-excluded' : ''}">
|
|
184
|
-
<legend>${label}${clearLink(f)}</legend
|
|
205
|
+
<legend>${label}${clearLink(f)}</legend>${facetMenu(f, label, byName)}
|
|
185
206
|
${chart}
|
|
186
207
|
${values.slice(0, FACET_SHOWN).map(value)}
|
|
187
208
|
${values.length > FACET_SHOWN
|
|
@@ -219,7 +240,10 @@ export async function renderFacets(ctx: PageContext, r: Region): Promise<Raw> {
|
|
|
219
240
|
const fid = `${formId}_${f.column.replace(/[^A-Za-z0-9_]/g, '_')}`;
|
|
220
241
|
const input = (which: 'from' | 'to', value: string) =>
|
|
221
242
|
html`<label class="sr-only" for="${fid}_${which}">${label}: ${t(`facets.${which}`)}</label>
|
|
222
|
-
|
|
243
|
+
${kind === 'date'
|
|
244
|
+
// (sprint 84) a date in the application's date format, with pgkiln's date picker
|
|
245
|
+
? dateField(ctx, { id: `${fid}_${which}`, iso: isCustom ? value : '', mask: appDateMask(ctx), attrs: html` form="${formId}" name="${which === 'from' ? k.from : k.to}${f.column}"` })
|
|
246
|
+
: html`<input id="${fid}_${which}" type="number" step="any" form="${formId}" name="${which === 'from' ? k.from : k.to}${f.column}" value="${isCustom ? value : ''}" placeholder="${t(`facets.${which}`)}">`}`;
|
|
223
247
|
const rangeChart = facetChart(ctx, report, f, label, f.ranges.map((x, i) => ({
|
|
224
248
|
label: f.type === 'star' ? ctx.locale.t(Number(x.from) === 1 ? 'facets.star_up' : 'facets.stars_up', { n: String(x.from) }) : rangeLabel(x, t),
|
|
225
249
|
n: counts![i],
|