pgkiln 0.86.0 → 0.87.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/apex-feature-parity.md +3 -3
- package/docs/guide/04-pages-and-regions.md +23 -4
- package/docs/guide/05-items.md +20 -1
- package/docs/guide/07-dynamic-actions.md +1 -1
- package/docs/guide/08-security.md +6 -2
- package/docs/guide/11-from-apex.md +3 -0
- package/examples/gallery/samples/sample-dynamic-actions.sql +7 -4
- package/examples/gallery/samples/sample-grids.sql +9 -29
- package/examples/gallery/samples/ux-patterns.sql +3 -3
- package/package.json +1 -1
- package/public/app.css +2 -0
- package/public/app.js +2 -0
- package/src/builder/components.ts +1 -1
- package/src/builder/region-help.ts +2 -2
- package/src/runtime/authz.ts +7 -0
- package/src/runtime/grid.ts +69 -9
- package/src/runtime/items.ts +9 -1
- package/src/runtime/regions.ts +16 -4
- package/src/runtime/signin.ts +5 -5
|
@@ -7,7 +7,7 @@ open item and open an issue or pull request; see [CONTRIBUTING.md](../CONTRIBUTI
|
|
|
7
7
|
Legend: ✅ available · 🟡 partial (see notes) · ❌ not yet · ➖ not planned (a deliberate choice,
|
|
8
8
|
or better served by the PostgreSQL ecosystem; see the notes and [extensions](guide/15-extensions.md))
|
|
9
9
|
|
|
10
|
-
Last reviewed: 2026-11-06 against **APEX 26.2** (sprint 164: every App Builder screen in the eleven builder languages (WS-7 leftover); sprint 165: navigation menu badges from SQL, the notification bell and live badges; sprint 162: the SQL Workshop tools and the region and report settings in the eleven builder languages (CD-8); sprint 161: component events of grids, calendars, maps and trees (CD-3); sprint 160: the Workflow Diagram region, an instance's steps with the reached, current and failed ones marked (CD-7); sprint 159: map layers Lines, Polygons and Extruded Polygons with fill and line styles per layer or per row (CD-6); sprint 158: the item types Image Upload, Text Field with autocomplete, Percent Graph and Display Map (CD-5); sprint 157: the process types Close Dialog, Clear Session State, Reset Pagination, User Preferences, Server Side Geocoding, Print Report, Human Task – Create / – Manage and Form – Initialization (CD-4); sprint 156: dynamic action events double click, key down / press, mouse enter / leave, select, resize, scroll, page unload, before refresh and facets change, greater / less and not-in-list conditions, and the actions Cancel Event, Set Style, Collapse / Expand Tree, Show AI Assistant, Trigger Geocoding and Open report dialog (CD-3); sprint 155: APEX's validation types, with an authorization scheme, Always execute and the error's display location (CD-1); sprint 154: one catalogue of server-side condition types for every component, shown in words in the Page Designer (CD-2); sprint 153: Database users in the eleven builder languages (WS-7 leftover); sprint 152: Workspace utilities, the dashboard, Mail queue, Network access, Instance settings and LDAP directories in the eleven builder languages (WS-7 leftover); sprint 151: vector providers with a model in the database, PostgresML (APEX's ONNX models, AP-1 leftover); sprint 150: remote databases, another PostgreSQL database's tables as foreign tables the applications query (REST Enabled SQL, AP-7 leftover); sprint 149: running workflow instances inside parallel branches migrate to a newer version (WS-6 leftover); sprint 148: the AI assistant streams its answers as the model writes them (WS-4 leftover); sprint 147: `pgkiln advise --offline` checks an export's files without a database (AP-10 leftover); sprint 146: the Advisor checks JSON sources' views against their data profile (AP-8 leftover); sprint 145: the texts that offered AI with "Claude or OpenAI" name all four providers: Claude, OpenAI, Gemini and OpenAI-compatible services (AP-11); sprint 144: `pgkiln advise` for CI with text, JSON and SARIF output (APEXlang static analysis / SQLcl CodeScan), security checks in the Advisor, and working copies and locks from the command line and MCP (the Working Copy APIs) (AP-10); sprint 143: the interactive report's 26.2 dialogs: *is one of* / *is not one of* filters with the column's values and counts, *is between* two values (dates with the date picker in the application's format), relative periods on date columns, and the column order by drag and drop or arrow buttons, followed by downloads and the PDF (AP-9); sprint 142: JSON sources with a data profile and updatable views whose arrays are rows, document views of a table with its children (APEX's duality views), JSON path columns in reports (AP-8); sprint 141: static workspace files (`#WORKSPACE_FILES#`), remote servers for REST data sources, catalogs and AI services (an environment variable per environment), and workspace-level web credentials (AP-7); sprint 140: the items Shuttle (ordered), Select One and Select Many, the regions Classic Report (report templates, pagination types), Breadcrumb and Help Text with a page help text, and a [mapping of every APEX item and region type](#item-and-region-types-one-by-one) (AP-4); sprint 139: OpenTelemetry, traces with a span per SQL statement, web call, AI request and tool call, logs, metrics and `/metrics` for Prometheus, browser spans relayed by pgkiln, per workspace off or its own collector (AP-6); sprint 138: an application's Utilities tab with a dashboard, database object dependencies, change history per component, Upgrade application, UI defaults (APEX's Attribute Dictionary), application and page checksums and an icon browser (AP-5); sprint 137: Create page as a grid of page types with previews, the page types interactive report, smart filters, search page, data loading, tree, timeline, media list, content row, comments, dashboard, wizard, unified task list and workflow console, a Feature tab with nine features that install once, and Create page as copy with shared components mapped or copied (AP-3); sprint 136: vector providers (an AI service with an embedding model or a SQL function), `meta.embed`, *Add vector column* in Table Properties with an HNSW index and a queue that keeps embeddings current, search types vector and hybrid (reciprocal rank fusion with full-text) and a Retrieve (RAG) tool for AI agents, all as the app's role through RLS, on pgvector (AP-1); sprint 135: full-text search configurations, APEX's Oracle Text search type on PostgreSQL full-text search with stemming, ranking, highlights, *Did you mean* and *Add full-text search* in Table Properties (AP-2); sprint 134: the builder also in Italiano, 中文 (简体), 中文 (繁體), 日本語 and 한국어, APEX's ten builder languages plus Nederlands (SEC-5); sprints 121, 123–125: the builder in Deutsch, English, Español, Français, Nederlands and Português, step by step: the frame, sign-in, Workspace home, App Builder, Page Designer menus, the SQL Workshop's home, SQL Worksheet, Object Browser and Table Properties, Shared Components, application settings, users, identity providers, developers and workspaces (WS-7); sprint 122: *Ask in your own words* sets faceted search and smart filter facets, smart filters read a typed phrase (WS-5), and Sample Maps, Sample Approvals, Sample Data Loading and Sample Files in the App Gallery (WS-8); sprint 120: the App Gallery's single-topic sample apps (charts, cards, calendar, interactive grids, dynamic actions, reporting), the UX Pattern Catalog and a Utility Apps tab with Lookup Lists, Release Notes and Announcements (WS-8); sprint 119: data security per table, a roles × columns matrix and masked values per application role in a masking view, the same identity for pages, AI tools, REST, AutoREST and MCP (WS-13); sprint 118: Administration → Mail queue with delivery per hour and per workspace, the slowest provider calls and failures with Retry and Purge, a workspace's own mail service, and workspace network access lists that narrow the installation's allow-list (WS-9); sprint 117: Pillar Light and Pillar Dark navigation appearances with a header strip, clearer navigation tree states, subtle colour utilities, RTL; sprint 116: the comparison moved from 26.1 to 26.2, with a section for the 26.2 workspace items WS-1 … WS-13 and OCI managed identities; the App Builder list with Updated by, working copies and the lock; `pgkiln import --only`, a file-level import; stale rows on facet menus and blueprints fixed; sprint 115: popup LOVs that load on scroll, Wait Popup dynamic actions and dynamic actions on menu entries; sprint 114: Workflow 26.2 with custom task outcomes, task events and notifications, Send E-Mail and draft steps, migrating running instances and the diagram as SVG / PNG; sprint 113: Team Development with issues, labels, milestones, templates, a board and a set-up wizard; sprint 112: AI interactive reports with chips, Undo, computed columns and a phrase parser that needs no AI service; sprint 111: AI Agents with tools as a shared component, a Navigate tool with checksummed URLs, Gemini and OpenAI-compatible services; sprint 110: Data Reporter as a workspace tool with datasets, reporting applications, editors and viewers, published reports with stable URLs and pins; sprint 109: the Workspace home above the App Builder and SQL Workshop, with a summary, top applications, users and developers, and an optional release check; sprint 108: the code editor's syntax colours in the builder's light and dark themes, read-only SQL highlighted, the Data Modeler in dark mode; sprint 107: a transaction mode in the SQL Worksheet with Commit and Rollback; DA-18: JavaScript (MLE) not planned, plv8 where installed; sprint 106: the change log of DDL change sets with Flyway, Liquibase and SQL downloads; sprint 104: RESTful Services modules, templates and handlers with PL/pgSQL and media handlers, privileges, OpenAPI and Try it; sprint 103: Schema Export / Import with pg_dump and pg_restore; sprint 102: the SQL Monitor with live sessions, Top SQL and a statement's plan; sprint 101: Scheduling of pg_cron jobs with a schedule builder, run now and run history; sprint 100: RESTful Services, tables and views published as REST endpoints with paging, filters, keys and OpenAPI; sprint 99: Administration → Database Users for PostgreSQL roles; sprint 98: the Data Modeler with relational diagrams, DDL and a data dictionary; sprint 97: charts of a query in the worksheet, saved charts and their dashboard; sprint 96: the SQL Workshop's Launchpad with pins and tool groups, the builder's search with Ctrl+K; sprint 95: Table Properties: create, edit or copy a table in a form with a DDL preview and Apply in one transaction; sprint 94: an object's menu with Open in a panel, Quick DDL, Add index / trigger / policy and Drop with dependents; sprint 93: worksheets saved as SQL Scripts with tabs and a Files list, Format, Convert case, find and replace, bind and substitution variables; sprint 92: the worksheet's Query Result with sort, value dialog, copy and downloads, plans as a tree with the slowest step and misestimates, SQL History with status, search and run again; sprint 91: the SQL Worksheet like Database Actions' SQL: navigator, Run Statement at the cursor and Run Script with a log per statement, Explain Plan, Autotrace, notices, history, Run as an application role; sprint 90: Comment Out in the page designer, gallery labels in title case; sprint 89: the Object Browser's schema, one row of tabs, blank NULLs, Modify column; sprint 88: home cards with colour bands and round icons, the hero on the page background, a page footer with the version and Back to top; sprint 87: the Create Application wizard reloads the page list for a schema, RLS badges, the alias from the name; sprint 86: the page designer by page number, HTML error pages in the builder; sprint 85: the Form Designer, beyond APEX; sprint 84: the date picker, facet menus; sprint 83: Component Settings and Component Groups; sprint 82: Report Queries; sprint 81: Application Computations and Shortcuts in Shared Components; sprint 80: the master grid's first row selected when the page opens and after a search; grid cells as text with their format mask until edited, with cell navigation by arrow keys, Enter, F2 and Escape; sprint 79: generated dashboards with sums, donuts and lines, leaving out charts that say nothing; applications open in their own light or dark mode until the user chooses, per application; a user menu with Appearance and Sign in when signed out; the developer toolbar's App and Page texts and debug state; the report search's column scope in the magnifier; sprint 78: generated faceted search reports with one column per foreign key, the faceted search's total row count, Show all and Yes/No values, one search field, friendly headings and number formats in generated grids and master-detail pages, friendly names in the Create Application wizard's page list; sprint 77: the Page Designer's layout and tree by display position, button positions, the tree's context menu with Duplicate and Copy to other page, Show Common / Show All, Go to group and the Template Options dialog; display positions, button positions and side-column page templates at runtime; sprint 76: the interactive report's menus and pagination, date items in the date format; sprint 75: the SQL Workshop's Object Browser and tabs; sprint 74: the builder's information architecture: Shared Components grid, Settings tabs, application home search and tasks; sprint 73: the Create Application wizard on one page with page types and features; sprint 72: the runtime chrome compared with APEX 26.2's: the developer toolbar, the Iris look, the navigation bar's Help menu and Install button; sprint 71: generated applications compared with APEX 26.2's: labels, foreign key names in reports, file columns, format masks, the Home page and the dashboard; sprint 70: treemap, sunburst and timeline charts; sprint 69: dragging tasks on a Gantt chart; sprint 68: the Data Reporter's welcome page, Find my workspace; sprint 67: REST source catalogs from OpenAPI documents, a workspace's export and import (two rows added); sprint 66: a blueprint from an existing application; sprint 65: facet charts; sprint 64: approval vacation rules and expiration policies; sprint 63: range, box plot and stock charts; sprint 62: workspace preferences (SQL Workshop and RESTful services per workspace), extension links, Manage backups across applications, SQL Workshop utilities with object reports, About Database and a database monitor; sprint 61: the builder's screens compared with APEX 26.1: the workspace sign-in page, the Administration menu with workspace message, environment banner and utilization, the SQL Workshop home, the App Gallery, application groups and cross application reports (four rows added); sprint 60: isolated workspaces with their own database logins, row-level security on pgkiln's tables and workspace database roles, workspace administrators; sprint 59: the Page Designer's help pane, Quick edit from the developer toolbar; sprint 58: page groups; sprint 57: region templates hero, title bar, buttons container, inline dialog and drawer, wizard with wizard progress, the carousel; sprint 56: Generate DDL and Schema Comparison in the SQL Workshop, `pgkiln ddl` and `pgkiln compare`; sprint 55: an editable login page with a Login region and the Minimal page template, Forgot password through the application's mail service; sprint 54: interactive grid views, the single row view, group by and chart; sprint 53: application settings, user preferences and items kept per user, dynamic translations, `meta.data_export` and data export downloads, display image and geocoded address items; sprint 52: every dynamic action event and action, Ajax callbacks, an error handling function, the session timeout warning; sprint 51: checked imports, application backups, availability and Run only, feedback, type-specific attribute help; sprint 50: lost update detection, warn on unsaved changes, the URL and search regions with search configurations, the developer toolbar with a session state viewer, and e-mail through SMTP with OAuth2, Microsoft Graph, Amazon SES and HTTP APIs; import checks, application availability, property help and e-mail compared again; 2026-10-06: pgkiln 0.31.0, in progress: static application files and the Execute JavaScript dynamic action; region, item, dynamic action and process plug-ins; conditional and dynamic theme styles; XML and Excel in `meta.parse_data`, zips in SQL, `meta.v_boolean`; file items in S3-compatible object storage; map layers by the visible area and as vector tiles; a graphical query builder; tenants for workflows and tasks; application files as YAML and region static IDs; Lucide icons and icon modifiers; PWA push notifications; 0.30.0: 136 icons and an icon picker; an axe-core accessibility audit; cropping pictures; session sharing between applications; twenty-two built-in languages; 0.29.0: workspaces; the Iris base style; drawers; built-in template components including the metric card; Theme Roller dark-mode colours, live preview and template options on items and report columns; report selection across pages; instance settings; fourteen built-in languages; 0.28.0: AI with Claude and OpenAI; 0.27.0: Data Reporter; sample data generator; create application from several sheets, pasted data or existing tables; 0.26.0: working copies with a three-way merge; theme, library and boilerplate applications with subscriptions; 0.25.0: Gantt, pyramid and polar charts; map layers, clustering and server-side spatial filters; REST data sources that write back from forms and grids and synchronise into tables, OAuth2 password and refresh-token grants; debug messages per request and an install/upgrade log; `meta.web_request()` and `meta.parse_data()`; Theme Roller style variants and template options; 0.24.0: automations with several actions, error handling per row and runs from SQL; invoke-API workflow steps; Unload Data in the Data Workshop; create page wizards for cards, calendar, chart, map, faceted search, form and master-detail; create an application from a file; 0.23.0: interactive grid aggregates, frozen/moved/resized columns, layouts and saved grid reports, master-detail, row actions and copy/paste; download processes, execution chains in the background, workflow processes, branches to a function's URL or another app, the Dialog Closed event; number format masks, automatic time zone, runtime messages in German, French and Spanish; SQL scripts, Quick SQL, a query builder, XML loading and data load definitions; custom authentication, lists, page and application locks with developer comments, supporting objects; 0.22.0: keyset paging, report PDFs and REST collections streamed from a cursor, database-account authentication; 0.21.0: HTTP-header authentication behind a reverse proxy; 0.20.0: a popup list of values with server-side search; 0.19.0: row-range pagination, maximum row counts and row limits, lazy loading and caching of regions, streamed CSV/Excel downloads; 0.18.0: calendar week/day/list views with drag and drop, bubble/gauge/funnel/radar charts and drill-down, smart filters, a region display selector and more facet types, computations, conditional branches, build options, menu buttons, REST data sources and web credentials, rich text/Markdown, rating, combobox, date range and QR code items; 0.17.0: an App Builder home, workspace dashboard and utilities like APEX's; 0.16.0: a Page Designer with drag-and-drop layout, a code editor with autocomplete, template components and plug-ins, parallel branches and versions of workflows, the `pgkiln` command line with one file per component; 0.15.0: drop and paste files, heat maps, filtering a report by the map area; 0.14.0: stacked, combo, scatter and pie charts, several files per upload item; 0.13.0: map and tree regions; 0.12.0: REST modules in the builder; 0.11.0: workflows and Progressive Web Apps; 0.10.0: LDAP, SAML, "Keep me signed in", document templates, JSON loading, approvals and the task list; pgkiln installs no application, HR is an example).
|
|
10
|
+
Last reviewed: 2026-11-06 against **APEX 26.2** (sprint 167: hidden items with Value Protected off, default values for an interactive grid's new rows, page items in any region type; sprint 164: every App Builder screen in the eleven builder languages (WS-7 leftover); sprint 165: navigation menu badges from SQL, the notification bell and live badges; sprint 162: the SQL Workshop tools and the region and report settings in the eleven builder languages (CD-8); sprint 161: component events of grids, calendars, maps and trees (CD-3); sprint 160: the Workflow Diagram region, an instance's steps with the reached, current and failed ones marked (CD-7); sprint 159: map layers Lines, Polygons and Extruded Polygons with fill and line styles per layer or per row (CD-6); sprint 158: the item types Image Upload, Text Field with autocomplete, Percent Graph and Display Map (CD-5); sprint 157: the process types Close Dialog, Clear Session State, Reset Pagination, User Preferences, Server Side Geocoding, Print Report, Human Task – Create / – Manage and Form – Initialization (CD-4); sprint 156: dynamic action events double click, key down / press, mouse enter / leave, select, resize, scroll, page unload, before refresh and facets change, greater / less and not-in-list conditions, and the actions Cancel Event, Set Style, Collapse / Expand Tree, Show AI Assistant, Trigger Geocoding and Open report dialog (CD-3); sprint 155: APEX's validation types, with an authorization scheme, Always execute and the error's display location (CD-1); sprint 154: one catalogue of server-side condition types for every component, shown in words in the Page Designer (CD-2); sprint 153: Database users in the eleven builder languages (WS-7 leftover); sprint 152: Workspace utilities, the dashboard, Mail queue, Network access, Instance settings and LDAP directories in the eleven builder languages (WS-7 leftover); sprint 151: vector providers with a model in the database, PostgresML (APEX's ONNX models, AP-1 leftover); sprint 150: remote databases, another PostgreSQL database's tables as foreign tables the applications query (REST Enabled SQL, AP-7 leftover); sprint 149: running workflow instances inside parallel branches migrate to a newer version (WS-6 leftover); sprint 148: the AI assistant streams its answers as the model writes them (WS-4 leftover); sprint 147: `pgkiln advise --offline` checks an export's files without a database (AP-10 leftover); sprint 146: the Advisor checks JSON sources' views against their data profile (AP-8 leftover); sprint 145: the texts that offered AI with "Claude or OpenAI" name all four providers: Claude, OpenAI, Gemini and OpenAI-compatible services (AP-11); sprint 144: `pgkiln advise` for CI with text, JSON and SARIF output (APEXlang static analysis / SQLcl CodeScan), security checks in the Advisor, and working copies and locks from the command line and MCP (the Working Copy APIs) (AP-10); sprint 143: the interactive report's 26.2 dialogs: *is one of* / *is not one of* filters with the column's values and counts, *is between* two values (dates with the date picker in the application's format), relative periods on date columns, and the column order by drag and drop or arrow buttons, followed by downloads and the PDF (AP-9); sprint 142: JSON sources with a data profile and updatable views whose arrays are rows, document views of a table with its children (APEX's duality views), JSON path columns in reports (AP-8); sprint 141: static workspace files (`#WORKSPACE_FILES#`), remote servers for REST data sources, catalogs and AI services (an environment variable per environment), and workspace-level web credentials (AP-7); sprint 140: the items Shuttle (ordered), Select One and Select Many, the regions Classic Report (report templates, pagination types), Breadcrumb and Help Text with a page help text, and a [mapping of every APEX item and region type](#item-and-region-types-one-by-one) (AP-4); sprint 139: OpenTelemetry, traces with a span per SQL statement, web call, AI request and tool call, logs, metrics and `/metrics` for Prometheus, browser spans relayed by pgkiln, per workspace off or its own collector (AP-6); sprint 138: an application's Utilities tab with a dashboard, database object dependencies, change history per component, Upgrade application, UI defaults (APEX's Attribute Dictionary), application and page checksums and an icon browser (AP-5); sprint 137: Create page as a grid of page types with previews, the page types interactive report, smart filters, search page, data loading, tree, timeline, media list, content row, comments, dashboard, wizard, unified task list and workflow console, a Feature tab with nine features that install once, and Create page as copy with shared components mapped or copied (AP-3); sprint 136: vector providers (an AI service with an embedding model or a SQL function), `meta.embed`, *Add vector column* in Table Properties with an HNSW index and a queue that keeps embeddings current, search types vector and hybrid (reciprocal rank fusion with full-text) and a Retrieve (RAG) tool for AI agents, all as the app's role through RLS, on pgvector (AP-1); sprint 135: full-text search configurations, APEX's Oracle Text search type on PostgreSQL full-text search with stemming, ranking, highlights, *Did you mean* and *Add full-text search* in Table Properties (AP-2); sprint 134: the builder also in Italiano, 中文 (简体), 中文 (繁體), 日本語 and 한국어, APEX's ten builder languages plus Nederlands (SEC-5); sprints 121, 123–125: the builder in Deutsch, English, Español, Français, Nederlands and Português, step by step: the frame, sign-in, Workspace home, App Builder, Page Designer menus, the SQL Workshop's home, SQL Worksheet, Object Browser and Table Properties, Shared Components, application settings, users, identity providers, developers and workspaces (WS-7); sprint 122: *Ask in your own words* sets faceted search and smart filter facets, smart filters read a typed phrase (WS-5), and Sample Maps, Sample Approvals, Sample Data Loading and Sample Files in the App Gallery (WS-8); sprint 120: the App Gallery's single-topic sample apps (charts, cards, calendar, interactive grids, dynamic actions, reporting), the UX Pattern Catalog and a Utility Apps tab with Lookup Lists, Release Notes and Announcements (WS-8); sprint 119: data security per table, a roles × columns matrix and masked values per application role in a masking view, the same identity for pages, AI tools, REST, AutoREST and MCP (WS-13); sprint 118: Administration → Mail queue with delivery per hour and per workspace, the slowest provider calls and failures with Retry and Purge, a workspace's own mail service, and workspace network access lists that narrow the installation's allow-list (WS-9); sprint 117: Pillar Light and Pillar Dark navigation appearances with a header strip, clearer navigation tree states, subtle colour utilities, RTL; sprint 116: the comparison moved from 26.1 to 26.2, with a section for the 26.2 workspace items WS-1 … WS-13 and OCI managed identities; the App Builder list with Updated by, working copies and the lock; `pgkiln import --only`, a file-level import; stale rows on facet menus and blueprints fixed; sprint 115: popup LOVs that load on scroll, Wait Popup dynamic actions and dynamic actions on menu entries; sprint 114: Workflow 26.2 with custom task outcomes, task events and notifications, Send E-Mail and draft steps, migrating running instances and the diagram as SVG / PNG; sprint 113: Team Development with issues, labels, milestones, templates, a board and a set-up wizard; sprint 112: AI interactive reports with chips, Undo, computed columns and a phrase parser that needs no AI service; sprint 111: AI Agents with tools as a shared component, a Navigate tool with checksummed URLs, Gemini and OpenAI-compatible services; sprint 110: Data Reporter as a workspace tool with datasets, reporting applications, editors and viewers, published reports with stable URLs and pins; sprint 109: the Workspace home above the App Builder and SQL Workshop, with a summary, top applications, users and developers, and an optional release check; sprint 108: the code editor's syntax colours in the builder's light and dark themes, read-only SQL highlighted, the Data Modeler in dark mode; sprint 107: a transaction mode in the SQL Worksheet with Commit and Rollback; DA-18: JavaScript (MLE) not planned, plv8 where installed; sprint 106: the change log of DDL change sets with Flyway, Liquibase and SQL downloads; sprint 104: RESTful Services modules, templates and handlers with PL/pgSQL and media handlers, privileges, OpenAPI and Try it; sprint 103: Schema Export / Import with pg_dump and pg_restore; sprint 102: the SQL Monitor with live sessions, Top SQL and a statement's plan; sprint 101: Scheduling of pg_cron jobs with a schedule builder, run now and run history; sprint 100: RESTful Services, tables and views published as REST endpoints with paging, filters, keys and OpenAPI; sprint 99: Administration → Database Users for PostgreSQL roles; sprint 98: the Data Modeler with relational diagrams, DDL and a data dictionary; sprint 97: charts of a query in the worksheet, saved charts and their dashboard; sprint 96: the SQL Workshop's Launchpad with pins and tool groups, the builder's search with Ctrl+K; sprint 95: Table Properties: create, edit or copy a table in a form with a DDL preview and Apply in one transaction; sprint 94: an object's menu with Open in a panel, Quick DDL, Add index / trigger / policy and Drop with dependents; sprint 93: worksheets saved as SQL Scripts with tabs and a Files list, Format, Convert case, find and replace, bind and substitution variables; sprint 92: the worksheet's Query Result with sort, value dialog, copy and downloads, plans as a tree with the slowest step and misestimates, SQL History with status, search and run again; sprint 91: the SQL Worksheet like Database Actions' SQL: navigator, Run Statement at the cursor and Run Script with a log per statement, Explain Plan, Autotrace, notices, history, Run as an application role; sprint 90: Comment Out in the page designer, gallery labels in title case; sprint 89: the Object Browser's schema, one row of tabs, blank NULLs, Modify column; sprint 88: home cards with colour bands and round icons, the hero on the page background, a page footer with the version and Back to top; sprint 87: the Create Application wizard reloads the page list for a schema, RLS badges, the alias from the name; sprint 86: the page designer by page number, HTML error pages in the builder; sprint 85: the Form Designer, beyond APEX; sprint 84: the date picker, facet menus; sprint 83: Component Settings and Component Groups; sprint 82: Report Queries; sprint 81: Application Computations and Shortcuts in Shared Components; sprint 80: the master grid's first row selected when the page opens and after a search; grid cells as text with their format mask until edited, with cell navigation by arrow keys, Enter, F2 and Escape; sprint 79: generated dashboards with sums, donuts and lines, leaving out charts that say nothing; applications open in their own light or dark mode until the user chooses, per application; a user menu with Appearance and Sign in when signed out; the developer toolbar's App and Page texts and debug state; the report search's column scope in the magnifier; sprint 78: generated faceted search reports with one column per foreign key, the faceted search's total row count, Show all and Yes/No values, one search field, friendly headings and number formats in generated grids and master-detail pages, friendly names in the Create Application wizard's page list; sprint 77: the Page Designer's layout and tree by display position, button positions, the tree's context menu with Duplicate and Copy to other page, Show Common / Show All, Go to group and the Template Options dialog; display positions, button positions and side-column page templates at runtime; sprint 76: the interactive report's menus and pagination, date items in the date format; sprint 75: the SQL Workshop's Object Browser and tabs; sprint 74: the builder's information architecture: Shared Components grid, Settings tabs, application home search and tasks; sprint 73: the Create Application wizard on one page with page types and features; sprint 72: the runtime chrome compared with APEX 26.2's: the developer toolbar, the Iris look, the navigation bar's Help menu and Install button; sprint 71: generated applications compared with APEX 26.2's: labels, foreign key names in reports, file columns, format masks, the Home page and the dashboard; sprint 70: treemap, sunburst and timeline charts; sprint 69: dragging tasks on a Gantt chart; sprint 68: the Data Reporter's welcome page, Find my workspace; sprint 67: REST source catalogs from OpenAPI documents, a workspace's export and import (two rows added); sprint 66: a blueprint from an existing application; sprint 65: facet charts; sprint 64: approval vacation rules and expiration policies; sprint 63: range, box plot and stock charts; sprint 62: workspace preferences (SQL Workshop and RESTful services per workspace), extension links, Manage backups across applications, SQL Workshop utilities with object reports, About Database and a database monitor; sprint 61: the builder's screens compared with APEX 26.1: the workspace sign-in page, the Administration menu with workspace message, environment banner and utilization, the SQL Workshop home, the App Gallery, application groups and cross application reports (four rows added); sprint 60: isolated workspaces with their own database logins, row-level security on pgkiln's tables and workspace database roles, workspace administrators; sprint 59: the Page Designer's help pane, Quick edit from the developer toolbar; sprint 58: page groups; sprint 57: region templates hero, title bar, buttons container, inline dialog and drawer, wizard with wizard progress, the carousel; sprint 56: Generate DDL and Schema Comparison in the SQL Workshop, `pgkiln ddl` and `pgkiln compare`; sprint 55: an editable login page with a Login region and the Minimal page template, Forgot password through the application's mail service; sprint 54: interactive grid views, the single row view, group by and chart; sprint 53: application settings, user preferences and items kept per user, dynamic translations, `meta.data_export` and data export downloads, display image and geocoded address items; sprint 52: every dynamic action event and action, Ajax callbacks, an error handling function, the session timeout warning; sprint 51: checked imports, application backups, availability and Run only, feedback, type-specific attribute help; sprint 50: lost update detection, warn on unsaved changes, the URL and search regions with search configurations, the developer toolbar with a session state viewer, and e-mail through SMTP with OAuth2, Microsoft Graph, Amazon SES and HTTP APIs; import checks, application availability, property help and e-mail compared again; 2026-10-06: pgkiln 0.31.0, in progress: static application files and the Execute JavaScript dynamic action; region, item, dynamic action and process plug-ins; conditional and dynamic theme styles; XML and Excel in `meta.parse_data`, zips in SQL, `meta.v_boolean`; file items in S3-compatible object storage; map layers by the visible area and as vector tiles; a graphical query builder; tenants for workflows and tasks; application files as YAML and region static IDs; Lucide icons and icon modifiers; PWA push notifications; 0.30.0: 136 icons and an icon picker; an axe-core accessibility audit; cropping pictures; session sharing between applications; twenty-two built-in languages; 0.29.0: workspaces; the Iris base style; drawers; built-in template components including the metric card; Theme Roller dark-mode colours, live preview and template options on items and report columns; report selection across pages; instance settings; fourteen built-in languages; 0.28.0: AI with Claude and OpenAI; 0.27.0: Data Reporter; sample data generator; create application from several sheets, pasted data or existing tables; 0.26.0: working copies with a three-way merge; theme, library and boilerplate applications with subscriptions; 0.25.0: Gantt, pyramid and polar charts; map layers, clustering and server-side spatial filters; REST data sources that write back from forms and grids and synchronise into tables, OAuth2 password and refresh-token grants; debug messages per request and an install/upgrade log; `meta.web_request()` and `meta.parse_data()`; Theme Roller style variants and template options; 0.24.0: automations with several actions, error handling per row and runs from SQL; invoke-API workflow steps; Unload Data in the Data Workshop; create page wizards for cards, calendar, chart, map, faceted search, form and master-detail; create an application from a file; 0.23.0: interactive grid aggregates, frozen/moved/resized columns, layouts and saved grid reports, master-detail, row actions and copy/paste; download processes, execution chains in the background, workflow processes, branches to a function's URL or another app, the Dialog Closed event; number format masks, automatic time zone, runtime messages in German, French and Spanish; SQL scripts, Quick SQL, a query builder, XML loading and data load definitions; custom authentication, lists, page and application locks with developer comments, supporting objects; 0.22.0: keyset paging, report PDFs and REST collections streamed from a cursor, database-account authentication; 0.21.0: HTTP-header authentication behind a reverse proxy; 0.20.0: a popup list of values with server-side search; 0.19.0: row-range pagination, maximum row counts and row limits, lazy loading and caching of regions, streamed CSV/Excel downloads; 0.18.0: calendar week/day/list views with drag and drop, bubble/gauge/funnel/radar charts and drill-down, smart filters, a region display selector and more facet types, computations, conditional branches, build options, menu buttons, REST data sources and web credentials, rich text/Markdown, rating, combobox, date range and QR code items; 0.17.0: an App Builder home, workspace dashboard and utilities like APEX's; 0.16.0: a Page Designer with drag-and-drop layout, a code editor with autocomplete, template components and plug-ins, parallel branches and versions of workflows, the `pgkiln` command line with one file per component; 0.15.0: drop and paste files, heat maps, filtering a report by the map area; 0.14.0: stacked, combo, scatter and pie charts, several files per upload item; 0.13.0: map and tree regions; 0.12.0: REST modules in the builder; 0.11.0: workflows and Progressive Web Apps; 0.10.0: LDAP, SAML, "Keep me signed in", document templates, JSON loading, approvals and the task list; pgkiln installs no application, HR is an example).
|
|
11
11
|
|
|
12
12
|
## At a glance
|
|
13
13
|
|
|
@@ -72,7 +72,7 @@ database links row and added static workspace files with workspace web credentia
|
|
|
72
72
|
| Classic report (report templates, pagination types) | ✅ | (sprint 140) Region type `classic_report`: the report renderer without the Actions menu, column menus, saved reports or downloads, and none of an interactive report's settings from the URL; sorting by headings, an optional search; `report_template` `standard` (a table), `value_pairs` (Value Attribute Pairs), `badges` (Badge List) or `media_list` (Media List); `pagination` `total` (X–Y of Z), `range` (X–Y) or `none` ([chapter 4](guide/04-pages-and-regions.md#classic_report-classic-report)) |
|
|
73
73
|
| Interactive report | ✅ | (sprint 76) An Actions menu of nine entries with dialogs, a column heading menu (sort, hide, control break, the column's values to filter by), a search in one column, hidden columns, pagination with the range, first/last and rows per page. Search, column filters, sort, rows per page, control break, aggregates (with subtotals), highlight, saved private and public reports, computed columns, group by, pivot, chart view, row selection into a page item, CSV, Excel (typed cells) and PDF download, print, reset, reflow on phones. a maximum row count (`max_rows`), natural-language control (*Ask in your own words*, 26.1) and row selection that stays across pages. Not possible: flashback queries (PostgreSQL has none; use an audit or temporal table) |
|
|
74
74
|
| Interactive report dialogs of 26.2: multi-select and date-range filters, drag-and-drop columns | ✅ | (sprint 143, AP-9) *Actions → Filter*: **is one of** / **is not one of** (several values at once: ticked in a list of the column's values with counts and a search, or typed with commas; *is not one of* keeps the rows without a value), **is between** two values (both included; either may stay empty; on a date column the date picker in the application's date format, and the whole last day of a timestamp), and **is in** a period on a date column (today, yesterday, the last 7 or 30 days, the next 7 days, this or last month, this or last year; rolling, so a saved report stays current). Each is a chip in words (*Job is one of CLERK, ANALYST*). *Actions → Columns*: the columns in their order, **dragged** into another or moved with **arrow buttons** (announced to screen readers; without JavaScript the arrows submit the form); the order is a chip that puts it back, is kept in saved reports and followed by the CSV, Excel and PDF downloads, which also leave out hidden columns ([chapter 4](guide/04-pages-and-regions.md#report-interactive-report)). *Ask in your own words* and AI answers use the same operators. Text templates for AI computed columns: computed columns stay expressions over the report's columns |
|
|
75
|
-
| Interactive grid | ✅ | Inline edit, add and delete rows, lists of values, required columns, per-row errors, all-or-nothing save, signed row keys, search and paging; **aggregates** over the whole result (developer and user defined), **frozen columns**, column **reorder/resize/hide** (Actions → Columns, or drag and drop), per-user layouts and **saved grid reports** (private and public), **master-detail** (signed row selection, the first row selected on load and after a search, details refreshed in place), **navigation and edit mode** (cells as text with their format mask until clicked, Enter or F2; arrow keys between cells), a **row actions menu** (edit, duplicate, delete, links) and **copy/paste** of cell ranges (tab-separated, spreadsheet compatible) ([chapter 4](guide/04-pages-and-regions.md)) |
|
|
75
|
+
| Interactive grid | ✅ | Inline edit, add and delete rows, lists of values, required columns, per-row errors, all-or-nothing save, signed row keys, search and paging; **aggregates** over the whole result (developer and user defined), **frozen columns**, column **reorder/resize/hide** (Actions → Columns, or drag and drop), per-user layouts and **saved grid reports** (private and public), **master-detail** (signed row selection, the first row selected on load and after a search, details refreshed in place), **navigation and edit mode** (cells as text with their format mask until clicked, Enter or F2; arrow keys between cells), a **row actions menu** (edit, duplicate, delete, links) and **copy/paste** of cell ranges (tab-separated, spreadsheet compatible) ([chapter 4](guide/04-pages-and-regions.md)); (167) a column's **default** for new rows (`default` with `&ITEM.`, or `default_sql`), as APEX's column Default |
|
|
76
76
|
| Interactive grid views: single row view, chart view, group by | ✅ | (sprint 54) **Single row view**: one row of the search as a form with first/previous/next/last and *Add row*, from the Actions or row actions menu, saved by the grid's own process with the same checks; **group by** and **chart** set up in the Actions menu over every row of the search, as for the interactive report; `views` limits them; Save comes back to the same search, page and view ([chapter 4](guide/04-pages-and-regions.md#grid-interactive-grid)). No pivot view on grids (the interactive report has one), no editing in the single row view of columns the grid doesn't show |
|
|
77
77
|
| Form (automatic row processing) | ✅ | Fetch, insert, update, delete, in a page or a modal dialog. Detects rows deleted meanwhile. (sprint 50) **Lost update detection** (APEX: *Prevent Lost Updates* and *Lock Row*): the page carries a checksum of the row's form columns (`meta.row_version`, independent of time zone and date format, as the app's role); *Save* and *Delete* lock the row and refuse when it changed meanwhile; off per form with `{"lost_update": false}` ([chapter 4](guide/04-pages-and-regions.md#form)) |
|
|
78
78
|
| Charts | ✅ | Bar, column, stacked, line, area, combination (columns + lines), scatter, bubble, donut, pie, gauge, funnel, radar, **Gantt** (start/end, progress, milestones, dependencies, a today line, a time axis from hours to years), **pyramid** (area-proportional segments, or two series back to back) and **polar** (polar area); (sprint 63) **range** (floating columns from low to high), **box plot** (from one value per row or five statistics) and **stock** (candlesticks from open, high, low and close); multi-series, tooltips, data table, (sprint 71) a value axis with gridlines on horizontal bars and top-N with an *Other* bar (`"top": 10`), palette checked for colour-vision deficiency; drill-down links on chart marks (checksummed, only to pages the user may open). Drawn on the server as SVG without inline styles. (sprint 69) **Gantt editing**: drag a task or its end, or the arrow keys, runs the region's SQL as the app's role. (sprint 70) **treemap** (squarified, two levels shown), **sunburst** (up to four rings) from `node_id` / `parent_id` columns, and **timeline** (lanes by a `series` column, points and periods, overlaps in rows). **Missing:** other Oracle JET types (e.g. picto, NBox, Sankey) |
|
|
@@ -135,7 +135,7 @@ review is a diff. Not counted in [At a glance](#at-a-glance): the rows above com
|
|
|
135
135
|
| Display Only | `display` | display values of a list of values |
|
|
136
136
|
| File Upload (File Browse) | `file` | several files per item |
|
|
137
137
|
| Geocoded Address | `address` | `GEOCODER_URL` |
|
|
138
|
-
| Hidden | `hidden` | |
|
|
138
|
+
| Hidden | `hidden` | *Value Protected* (the default) keeps the value on the server, never rendered; (167) `{"value_protected": false}` is a hidden input the browser may set, as APEX's *Value Protected = No* |
|
|
139
139
|
| Image Upload (24.2) | `file` | with `{"accept": "image/*", "crop": "1:1", "max_px": 1600}` |
|
|
140
140
|
| Markdown Editor | `markdown` | |
|
|
141
141
|
| Number Field | `number` | format masks |
|
|
@@ -70,8 +70,8 @@ Button Position):
|
|
|
70
70
|
|---|---|
|
|
71
71
|
| `create`, `edit`, `change`, `copy`, `help`, `up` | In the region's header, after the title |
|
|
72
72
|
| `previous`, `close`, `delete` | At the left end of the region's bottom bar |
|
|
73
|
-
| `next` | At the right end of the bottom bar |
|
|
74
|
-
| `region` (or empty) | With the region's other buttons, as before: the header of reports and charts, the bottom of forms and static content |
|
|
73
|
+
| `next` | At the right end of the bottom bar, after every other button there |
|
|
74
|
+
| `region` (or empty) | With the region's other buttons, as before: the header of reports and charts, the bottom of forms and static content (in the bottom bar, before `next`) |
|
|
75
75
|
|
|
76
76
|
Positions apply to the `standard`, `collapsible` and `plain` templates; the other templates place every
|
|
77
77
|
button where they always do. Page-level buttons have no position.
|
|
@@ -229,12 +229,16 @@ on an interval, use a dynamic action with the [`timer`](07-dynamic-actions.md) e
|
|
|
229
229
|
| [`workflows`](06-processing.md#workflows) | Workflow console: the workflows the user started or administers |
|
|
230
230
|
| [`search`](#search-search-region) | One search field over several sources (search configurations), results grouped per source |
|
|
231
231
|
| [`url`](#url-url-region) | Another site's page (or a page of this server) in a sandboxed frame |
|
|
232
|
-
| `login` | The sign-in form of the application's own login page
|
|
232
|
+
| `login` | The sign-in form of the application's own login page: identity providers, user name and password, *Keep me signed in*, *Forgot password?*; see [chapter 8](08-security.md#the-login-page). The cursor starts in the user name field; (167) `{"autofocus": false}` leaves it where the page puts it (a login region lower on a page that has other content) |
|
|
233
233
|
| [`breadcrumb`](#breadcrumb-and-help_text) | (140) The page's breadcrumb trail, for the Breadcrumb bar position |
|
|
234
234
|
| [`help_text`](#breadcrumb-and-help_text) | (140) The page's help text |
|
|
235
235
|
| [`static`](#static-and-dynamic-content) | Fixed HTML with `&ITEM.` substitutions |
|
|
236
236
|
| [`dynamic`](#static-and-dynamic-content) | HTML produced by a SELECT |
|
|
237
237
|
|
|
238
|
+
(167) Items can be placed in any region. Reports, forms and static content place them themselves; in the other
|
|
239
|
+
types (a tree, chart, map, cards, calendar …) they show under the region's content, as in APEX. A `login`
|
|
240
|
+
region's fields post to a form of their own, so items placed in it are not shown: put them in a region next to it.
|
|
241
|
+
|
|
238
242
|
Region-specific options go in the region's **attributes** (`config`, a JSON object).
|
|
239
243
|
|
|
240
244
|
**Static ID** (APEX: Static ID, optional): lower case letters, digits, `_` and `-`, starting with a
|
|
@@ -556,7 +560,7 @@ Attributes:
|
|
|
556
560
|
| `pagination`, `max_rows` | | As for reports |
|
|
557
561
|
| `allow` | all `true` | `{"insert": false, "update": true, "delete": false}` |
|
|
558
562
|
| `readonly` | `[]` | Columns that may not be edited |
|
|
559
|
-
| `columns` | `{}` | Per column: `{"deptno": {"lov": "LOV:DEPARTMENTS", "required": true}}`. `lov` makes it a select list (any [list of values](05-items.md#lists-of-values)) |
|
|
563
|
+
| `columns` | `{}` | Per column: `{"deptno": {"lov": "LOV:DEPARTMENTS", "required": true}}`. `lov` makes it a select list (any [list of values](05-items.md#lists-of-values)). (167) `default` and `default_sql` give a new row's starting value: see [New rows' default values](#new-rows-default-values) |
|
|
560
564
|
| `headings`, `hidden`, `formats` | | As for reports |
|
|
561
565
|
| `aggregates` | | Totals in the footer: `{"sal": ["sum", "avg"], "empno": "count"}` (`sum`, `avg`, `count`, `min`, `max`) |
|
|
562
566
|
| `layout` | | The default column layout: `{"order": ["ename", "sal"], "hidden": ["comm"], "widths": {"ename": 180}, "frozen": 1}` |
|
|
@@ -571,6 +575,21 @@ Attributes:
|
|
|
571
575
|
Cell editors follow the column type: number, date, date-time, checkbox (boolean), select list
|
|
572
576
|
(with `lov`) or text.
|
|
573
577
|
|
|
578
|
+
**New rows' default values.** (167; APEX: a column's *Default*) A column's `default` is text with `&ITEM.`
|
|
579
|
+
substitutions; `default_sql` is a SELECT whose first column of the first row is the value (with `:BINDS`). Both
|
|
580
|
+
are worked out when the grid is shown, and *Add row* (and the single row view's new row) starts with them:
|
|
581
|
+
|
|
582
|
+
```json
|
|
583
|
+
{"columns": {"deptno": {"lov": "LOV:DEPARTMENTS", "default": "&P27_DEPTNO."},
|
|
584
|
+
"status": {"default": "NEW"},
|
|
585
|
+
"hiredate": {"default_sql": "select current_date"}}}
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
A new row the user left at its default values counts as blank and isn't inserted, like an empty one; change
|
|
589
|
+
any value and the whole row, defaults included, is inserted. A duplicated row is always inserted. Only columns
|
|
590
|
+
a new row may set take a default (a detail grid's master column is filled from the master as before). For
|
|
591
|
+
anything that needs a decision per row, the `grid_row_init` dynamic action event still runs on each new row.
|
|
592
|
+
|
|
574
593
|
**Aggregates** are computed in the database over **every row of the search**, not only the rows
|
|
575
594
|
on the page, and shown in a footer row under their columns. Besides the developer's
|
|
576
595
|
(`aggregates`), users add their own with **Actions → Aggregate** (kept in the URL as
|
package/docs/guide/05-items.md
CHANGED
|
@@ -27,7 +27,7 @@ page, optionally inside a region. Name them `P<page>_<NAME>` (uppercase letters,
|
|
|
27
27
|
| `select_one` | (140) a search field over the list of values: typing filters a list under it ([Select One](#shuttle-select-one-and-select-many)); a select list without JavaScript | the chosen return value |
|
|
28
28
|
| `select_many` | (140) as `select_one`, for several values shown as chips with a remove button; a multi-select list without JavaScript | **colon-separated** return values |
|
|
29
29
|
| `display` | read-only text (shows the display value for list items) | whatever was set |
|
|
30
|
-
| `hidden` | not rendered | set by URL (with checksum), fetch or
|
|
30
|
+
| `hidden` | not rendered: the value stays on the server; (167) `{"value_protected": false}` renders a hidden input the browser may set ([Hidden items](#hidden-items)) | set by URL (with checksum), fetch, processes or server-side dynamic actions; unprotected also by browser-side dynamic actions, JavaScript and a submit |
|
|
31
31
|
| `file` | file upload ([chapter 16](16-files.md)); on phones the camera with `{"capture": "environment"}`, photos made smaller with `{"max_px": 1600}` ([chapter 17](17-mobile.md)) | the id of the uploaded temporary file (a uuid) |
|
|
32
32
|
| `location` | a text field with *Use my location* ([chapter 17](17-mobile.md)) | `latitude,longitude`, e.g. `52.01160,4.35710` |
|
|
33
33
|
| `richtext` | rich text editor (toolbar; a plain HTML textarea without JavaScript), full width | **sanitised HTML**, see [below](#rich-text-and-markdown) |
|
|
@@ -330,3 +330,22 @@ Items get their values from:
|
|
|
330
330
|
- **application processes** (application items).
|
|
331
331
|
|
|
332
332
|
Values persist in session state until changed. `?clear=1` in a URL clears the page's items.
|
|
333
|
+
|
|
334
|
+
### Hidden items
|
|
335
|
+
|
|
336
|
+
A `hidden` item is **value protected** by default: it isn't rendered, its value never leaves the server, and a
|
|
337
|
+
submit can't change it, so it is safe for keys and other values the user must not tamper with. The server sets
|
|
338
|
+
it: the URL (with checksum), a form's fetch, processes, computations and the server-side dynamic actions
|
|
339
|
+
(`set_value` and `execute_sql` set its session value, though the browser never sees it).
|
|
340
|
+
|
|
341
|
+
(167; APEX: *Value Protected* = No) `{"value_protected": false}` makes it a field of the page instead: a hidden
|
|
342
|
+
input that browser-side dynamic actions (`clear`, `set_value` from a component event's value, `current_position`),
|
|
343
|
+
`execute_javascript` functions and client-side conditions read and set, and whose value a submit (or a dynamic
|
|
344
|
+
action's *items to submit*) sends back. A change event on it runs its `change` dynamic actions. Its posted value
|
|
345
|
+
is **user input**, exactly like a text field's: check it in a validation before you trust it.
|
|
346
|
+
|
|
347
|
+
```yaml
|
|
348
|
+
name: P5_SELECTED_NODE
|
|
349
|
+
type: hidden
|
|
350
|
+
config: {value_protected: false}
|
|
351
|
+
```
|
|
@@ -91,7 +91,7 @@ items, in the browser. A tree of departments that filters an employee report, wi
|
|
|
91
91
|
|---|---|---|
|
|
92
92
|
| `show` / `hide` | browser | Show or hide the affected items/region. **With a condition, the opposite happens when the condition is false**, so one dynamic action gives "show commission only for salesmen". Also applied when the page loads (and already on the server, so there's no flicker) |
|
|
93
93
|
| `enable` / `disable` | browser | Enable or disable the controls, reversed when the condition is false |
|
|
94
|
-
| `set_value` | server | Runs `code` (a SELECT); its first row's columns set the affected items, in order.
|
|
94
|
+
| `set_value` | server | Runs `code` (a SELECT); its first row's columns set the affected items, in order. A protected `hidden` item (the default) gets the value in its session state only (the browser never sees it); (167) a hidden item with `{"value_protected": false}` is a hidden input that browser-side actions set too ([Hidden items](05-items.md#hidden-items)) |
|
|
95
95
|
| `execute_sql` | server | Runs `code`; returned columns named like items set those items |
|
|
96
96
|
| `refresh_region` | server | Re-renders `affected_region_id` with the current session state (e.g. a report filtered by the changed item) |
|
|
97
97
|
| `refresh_item` | server | Re-renders the affected items (e.g. a list whose LOV depends on another item) |
|
|
@@ -720,10 +720,14 @@ prefer policies on `meta.app_user()` / `meta.has_role()` on tables with a maskin
|
|
|
720
720
|
page and **user**, so a link can't be edited (`?P3_EMPNO=7839` → `7902`) and one user's links
|
|
721
721
|
don't work for another. Build links in SQL with `meta.page_url(3, '{"P3_EMPNO": 7839}')`.
|
|
722
722
|
- **Submitted values**: only editable, visible items are taken from a submit. Hidden, display-only,
|
|
723
|
-
read-only and unauthorized items keep their server-side values.
|
|
723
|
+
read-only and unauthorized items keep their server-side values. (167) A hidden item with
|
|
724
|
+
`{"value_protected": false}` is the exception the developer chooses: a hidden input whose posted value is
|
|
725
|
+
user input, like a text field's.
|
|
726
|
+
- **Grid defaults**: a new grid row posts the default values it started with (`_def`), only to tell an
|
|
727
|
+
untouched row from a filled-in one; it never sets a column.
|
|
724
728
|
- **Grid rows**: every row's primary key is signed the same way.
|
|
725
729
|
- **Master-detail selection**: a master grid's select links carry a signature bound to the
|
|
726
|
-
application, page, user, region and value; only then does the value go into the item (hidden
|
|
730
|
+
application, page, user, region and value; only then does the value go into the item (protected hidden
|
|
727
731
|
items are never taken from a submit, and URL items need the page checksum). The detail
|
|
728
732
|
endpoint `GET …/region/:id` serves only lazy regions and details of a visible master, after
|
|
729
733
|
the page's and region's authorization and conditions. A detail grid's master column is filled
|
|
@@ -184,6 +184,9 @@ Tips:
|
|
|
184
184
|
together.
|
|
185
185
|
- **Errors are hidden by default**: unexpected errors show a reference number unless the app is in
|
|
186
186
|
debug mode.
|
|
187
|
+
- **Hidden items aren't in the page**: a `hidden` item is value protected and its value stays on the server (APEX
|
|
188
|
+
renders it with a checksum). For APEX's *Value Protected* = No, give it `{"value_protected": false}`: a hidden
|
|
189
|
+
input that dynamic actions and JavaScript set ([Hidden items](05-items.md#hidden-items)).
|
|
187
190
|
- **Dates**: date items, date ranges, grid date cells and facet ranges are text in the application's date format
|
|
188
191
|
(else ISO, `2026-10-01`) with pgkiln's date picker; format masks
|
|
189
192
|
apply to report, grid and cards columns and to number and display items
|
|
@@ -15,7 +15,9 @@
|
|
|
15
15
|
-- (page 10) closed or cancelled by a dynamic action;
|
|
16
16
|
-- page 7 regions and the window: before / after refresh, resize, scroll,
|
|
17
17
|
-- Close Region, Print, Share, Download, Push Subscribe;
|
|
18
|
-
-- page 8 a tree: Selection Change, Expand Tree, Collapse Tree;
|
|
18
|
+
-- page 8 a tree: Selection Change, Expand Tree, Collapse Tree; an item in the
|
|
19
|
+
-- tree region and a hidden item a dynamic action sets in the browser
|
|
20
|
+
-- (value_protected false);
|
|
19
21
|
-- page 9 faceted search: Facets Change and the report's dialogs;
|
|
20
22
|
-- page 11 Show AI Assistant (asking needs an AI service set up by an
|
|
21
23
|
-- administrator; the page works without one).
|
|
@@ -189,7 +191,7 @@ select pg_temp.page_id('sample-dynamic-actions', r.page), r.seq, r.title, r.type
|
|
|
189
191
|
|
|
190
192
|
-- 8 A tree
|
|
191
193
|
(8, 5, 'About this page', 'static', 12, 'plain',
|
|
192
|
-
'<p class="lead">Clicking a node of the tree runs <strong>Selection change [Tree]</strong>: a <em>Set value</em> without SQL puts the node''s id into
|
|
194
|
+
'<p class="lead">Clicking a node of the tree runs <strong>Selection change [Tree]</strong>: a <em>Set value</em> without SQL puts the node''s id into a hidden item (<code>{"value_protected": false}</code>: a hidden input the browser may set) and into the display item under the tree (items show in any region type), and <em>Refresh region</em> draws the report with it. <strong>Expand tree</strong> and <strong>Collapse tree</strong> open or close every node.</p>',
|
|
193
195
|
'{}', null),
|
|
194
196
|
(8, 10, 'Categories and products', 'tree', 5, 'standard',
|
|
195
197
|
E'select ''c-'' || lower(category) as id, null::text as parent_id, category as label from sample_da.product group by category\nunion all\nselect id::text, ''c-'' || lower(category), name from sample_da.product',
|
|
@@ -261,7 +263,8 @@ select pg_temp.page_id('sample-dynamic-actions', i.page), pg_temp.region_id('sam
|
|
|
261
263
|
(6, 'delivery', 40, 'P6_CUSTOMS', 'Customs number', 'text', null, null, 'Only outside the EU (shown with in list).', '{}'),
|
|
262
264
|
(7, 'tools', 10, 'P7_REFRESHED', 'Last shuffled', 'display', null, 'not yet', null, '{}'),
|
|
263
265
|
(7, 'tools', 20, 'P7_PICKED', 'Picked', 'text', null, null, 'Selection change: the rows checked in Pick products, colon separated.', '{"warn_unsaved": false}'),
|
|
264
|
-
(8, '
|
|
266
|
+
(8, 'catalogue-tree', 10, 'P8_SHOWN', 'Chosen node', 'display', null, 'none yet', 'Set by the tree: a category (c-…) or a product id.', '{}'),
|
|
267
|
+
(8, 'chosen', 10, 'P8_NODE', null, 'hidden', null, null, null, '{"value_protected": false}')
|
|
265
268
|
) as i (page, region, seq, name, label, type, lov, def, help, config);
|
|
266
269
|
|
|
267
270
|
insert into meta.button (page_id, region_id, seq, name, label, action, hot)
|
|
@@ -387,7 +390,7 @@ select pg_temp.page_id('sample-dynamic-actions', d.page), d.seq, d.name, d.event
|
|
|
387
390
|
(7, 90, 'Turn on notifications', 'click', 'NOTIFY', null, null, 'push_subscribe', null, null, null, null,
|
|
388
391
|
'Notifications are on for this device.', null, null),
|
|
389
392
|
-- page 8 A tree
|
|
390
|
-
(8, 10, 'Remember the node', 'tree_selection_change', 'catalogue-tree', null, null, 'set_value', 'P8_NODE', null, null, null, null, null, null),
|
|
393
|
+
(8, 10, 'Remember the node', 'tree_selection_change', 'catalogue-tree', null, null, 'set_value', 'P8_NODE,P8_SHOWN', null, null, null, null, null, null),
|
|
391
394
|
(8, 20, 'Show its products', 'tree_selection_change', 'catalogue-tree', null, null, 'refresh_region', null, 'chosen', null, 'P8_NODE', null, null, null),
|
|
392
395
|
(8, 30, 'Open every node', 'click', 'EXPAND', null, null, 'expand_tree', null, 'catalogue-tree', null, null, null, null, null),
|
|
393
396
|
(8, 40, 'Close every node', 'click', 'COLLAPSE', null, null, 'collapse_tree', null, 'catalogue-tree', null, null, null, null, null),
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
--
|
|
4
4
|
-- The interactive grid: an editable grid made by the wizard; a grid with
|
|
5
5
|
-- select lists, required columns, totals, a frozen column, a row menu and a
|
|
6
|
-
-- default column layout, and dynamic actions on a
|
|
7
|
-
-- save; a master grid with its detail grid; and a read-only grid with its single row, group by and chart views.
|
|
6
|
+
-- default column layout, new rows' default values, and dynamic actions on a
|
|
7
|
+
-- new row and after a save; a master grid with its detail grid; and a read-only grid with its single row, group by and chart views.
|
|
8
8
|
--
|
|
9
9
|
-- Installed by an administrator from the App Gallery, or with
|
|
10
10
|
-- psql "$DATABASE_URL" -1 -f examples/gallery/samples/sample-grids.sql
|
|
@@ -109,7 +109,7 @@ select pg_temp.page_id('sample-grids', r.page), r.seq, r.title, r.type, 12, r.te
|
|
|
109
109
|
|
|
110
110
|
-- 3 Grid features
|
|
111
111
|
(3, 5, 'About this page', 'static', 'plain',
|
|
112
|
-
'<p class="lead">The grid''s attributes: <code>columns</code> gives the team column a list of values and makes the name required; <code>aggregates</code> adds totals over every row of the search; <code>frozen</code> keeps the name in view while the grid scrolls sideways; <code>row_actions</code> adds a menu per row (duplicate, delete); <code>layout</code> sets the default order and widths, and every user can rearrange it (Actions → Columns).</p><p>Two dynamic actions listen to the grid itself (the trigger is its static id <code>members</code>): <strong>Row initialization</strong> (<code>grid_row_init</code>) runs when <strong>Add row</strong> or Duplicate adds a row, and
|
|
112
|
+
'<p class="lead">The grid''s attributes: <code>columns</code> gives the team column a list of values and makes the name required; <code>aggregates</code> adds totals over every row of the search; <code>frozen</code> keeps the name in view while the grid scrolls sideways; <code>row_actions</code> adds a menu per row (duplicate, delete); <code>layout</code> sets the default order and widths, and every user can rearrange it (Actions → Columns).</p><p>Two dynamic actions listen to the grid itself (the trigger is its static id <code>members</code>): <strong>Row initialization</strong> (<code>grid_row_init</code>) runs when <strong>Add row</strong> or Duplicate adds a row, and shows a message; the row''s role, start date and hours already have values, from the columns'' <code>default</code> (text, may use <code>&ITEM.</code>) and <code>default_sql</code> (a SELECT) in <code>columns</code>. A new row left at its defaults isn''t saved; <strong>Save</strong> (<code>grid_save</code>) shows a message on the page that comes back after the grid was saved.</p>',
|
|
113
113
|
null, null, '{}', null),
|
|
114
114
|
(3, 10, 'Members', 'grid', 'standard',
|
|
115
115
|
E'select id, name, team_id, role, starts, hours, rate, remote\n from sample_grids.member order by team_id, name',
|
|
@@ -118,7 +118,9 @@ select pg_temp.page_id('sample-grids', r.page), r.seq, r.title, r.type, 12, r.te
|
|
|
118
118
|
"headings": {"team_id": "Team", "starts": "Started", "hours": "Hours a week", "rate": "Hourly rate"},
|
|
119
119
|
"columns": {"name": {"required": true},
|
|
120
120
|
"team_id": {"lov": "select name, id from sample_grids.team order by name", "required": true},
|
|
121
|
-
"role": {"lov": "STATIC:Lead,Developer,Designer,Tester,Analyst", "required": true}
|
|
121
|
+
"role": {"lov": "STATIC:Lead,Developer,Designer,Tester,Analyst", "required": true, "default": "Developer"},
|
|
122
|
+
"starts": {"default_sql": "select current_date"},
|
|
123
|
+
"hours": {"default": "40"}},
|
|
122
124
|
"formats": {"rate": "999G990D00"},
|
|
123
125
|
"aggregates": {"hours": ["sum", "avg"], "rate": "avg", "id": "count"},
|
|
124
126
|
"frozen": 1,
|
|
@@ -151,33 +153,11 @@ update meta.region set config = jsonb_set(config, '{columns,role}', '{"lov": "ST
|
|
|
151
153
|
where page_id = pg_temp.page_id('sample-grids', 4) and seq = 20;
|
|
152
154
|
|
|
153
155
|
-- page 3: dynamic actions on the grid's own events (trigger: the grid's static id).
|
|
154
|
-
-- grid_row_init: a new row (Add row, Duplicate)
|
|
156
|
+
-- grid_row_init: a new row (Add row, Duplicate); its defaults come from the grid's columns (default, default_sql);
|
|
155
157
|
-- grid_save: a message on the page that comes back after a save.
|
|
156
|
-
insert into meta.static_file (app_id, name, mime, content)
|
|
157
|
-
select a.id, 'sample-grids.js', 'text/javascript', convert_to($js$// Sample Interactive Grids: functions for "Execute JavaScript" dynamic actions.
|
|
158
|
-
// A new member row: its empty role, start date and hours get defaults (a duplicated row keeps its copied values).
|
|
159
|
-
pgkiln.actions.register('grids.memberDefaults', (da) => {
|
|
160
|
-
const row = da.element;
|
|
161
|
-
if (!row) return;
|
|
162
|
-
const now = new Date();
|
|
163
|
-
const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
|
|
164
|
-
const defaults = { role: 'Developer', starts: today, hours: '40' };
|
|
165
|
-
for (const th of row.closest('table').querySelectorAll('thead th[data-col-name]')) {
|
|
166
|
-
const value = defaults[th.dataset.colName];
|
|
167
|
-
const control = value && row.querySelector(`td[data-col="${th.dataset.col}"] :is(input:not([type=hidden]), select)`);
|
|
168
|
-
if (!control || control.value) continue;
|
|
169
|
-
control.value = value;
|
|
170
|
-
control.dispatchEvent(new Event('input', { bubbles: true }));
|
|
171
|
-
control.dispatchEvent(new Event('change', { bubbles: true }));
|
|
172
|
-
}
|
|
173
|
-
});
|
|
174
|
-
$js$, 'utf8')
|
|
175
|
-
from meta.app a where a.alias = 'sample-grids';
|
|
176
|
-
|
|
177
|
-
update meta.app set static_includes = '{sample-grids.js}' where alias = 'sample-grids';
|
|
178
|
-
|
|
179
158
|
insert into meta.dynamic_action (page_id, seq, name, event, trigger_element, action, code, message)
|
|
180
159
|
values
|
|
181
|
-
(pg_temp.page_id('sample-grids', 3), 10, '
|
|
160
|
+
(pg_temp.page_id('sample-grids', 3), 10, 'A new member', 'grid_row_init', 'members', 'show_success', null,
|
|
161
|
+
'A grid_row_init dynamic action: a new row. Its role, start date and hours start with the columns'' defaults; fill in the name and the team.'),
|
|
182
162
|
(pg_temp.page_id('sample-grids', 3), 20, 'Saved', 'grid_save', 'members', 'show_success', null,
|
|
183
163
|
'A grid_save dynamic action: the members are saved, and the totals include your changes.');
|
|
@@ -287,7 +287,7 @@ select pg_temp.page_id('ux-patterns', r.page), r.seq, r.title, r.type, r.cols, r
|
|
|
287
287
|
|
|
288
288
|
-- 15 Button positions
|
|
289
289
|
(15, 5, 'About this page', 'static', 12, 'plain', '{}',
|
|
290
|
-
'<p class="lead">A region''s button has a <strong>Button position</strong> (Page Designer → button → Layout; <code>slot</code> in the files), as APEX''s button positions. <em>Create, Edit, Change, Copy, Help</em> and <em>Up</em> go into the region''s header after the title; <em>Previous, Close</em> and <em>Delete</em> to the left end of the bottom bar and <em>Next</em> to the right end; <em>Region</em> (or none) with the region''s other buttons. Each button is labelled with its position. Previous and Next go to the neighbouring pages, Up and Close to the home page, Help opens an inline dialog; the others do nothing.</p>',
|
|
290
|
+
'<p class="lead">A region''s button has a <strong>Button position</strong> (Page Designer → button → Layout; <code>slot</code> in the files), as APEX''s button positions. <em>Create, Edit, Change, Copy, Help</em> and <em>Up</em> go into the region''s header after the title; <em>Previous, Close</em> and <em>Delete</em> to the left end of the bottom bar and <em>Next</em> to the right end; <em>Region</em> (or none) with the region''s other buttons (in the bottom bar of a static or form region, before Next). Each button is labelled with its position. Previous and Next go to the neighbouring pages, Up and Close to the home page, Help opens an inline dialog; the others do nothing.</p>',
|
|
291
291
|
'{}', null, null),
|
|
292
292
|
(15, 10, 'Every button position', 'static', 12, 'standard', '{}',
|
|
293
293
|
'<p>The buttons of this region use every position. They apply to the <code>standard</code>, <code>collapsible</code> and <code>plain</code> templates; the other templates place every button where they always do.</p>',
|
|
@@ -301,9 +301,9 @@ select pg_temp.page_id('ux-patterns', r.page), r.seq, r.title, r.type, r.cols, r
|
|
|
301
301
|
|
|
302
302
|
-- 16 Sign-in region (page template minimal)
|
|
303
303
|
(16, 5, 'About this page', 'static', 12, 'plain', '{}',
|
|
304
|
-
'<p class="lead">This page has the page template <em>Minimal (no navigation)</em> (<code>minimal</code>): no header, navigation menu or navigation bar, the title and the regions only, centred. The region below is a <strong>Login</strong> region: the application''s sign-in form, as on an own login page (Application settings → Sign-in methods → Create a login page). It posts to the application''s sign-in, so throttling and every sign-in method work as on the built-in page. You are signed in already: signing in again as demo / demo starts a new session and opens the home page. A wrong password shows the built-in sign-in page with its message, because this page is not the application''s login page.</p>',
|
|
304
|
+
'<p class="lead">This page has the page template <em>Minimal (no navigation)</em> (<code>minimal</code>): no header, navigation menu or navigation bar, the title and the regions only, centred. The region below is a <strong>Login</strong> region: the application''s sign-in form, as on an own login page (Application settings → Sign-in methods → Create a login page). It posts to the application''s sign-in, so throttling and every sign-in method work as on the built-in page. You are signed in already: signing in again as demo / demo starts a new session and opens the home page. A wrong password shows the built-in sign-in page with its message, because this page is not the application''s login page. The region has <code>{"autofocus": false}</code>: you are signed in already, so the cursor isn''t put in the user name field (on a real login page leave it out).</p>',
|
|
305
305
|
'{}', 'about-login', null),
|
|
306
|
-
(16, 10, 'Sign in', 'login', 12, 'standard', '{}', null, '{}', null, null)
|
|
306
|
+
(16, 10, 'Sign in', 'login', 12, 'standard', '{}', null, '{"autofocus": false}', null, null)
|
|
307
307
|
) as r (page, seq, title, type, cols, template, topts, source, config, sid, pos);
|
|
308
308
|
|
|
309
309
|
-- buttons
|
package/package.json
CHANGED
package/public/app.css
CHANGED
|
@@ -454,6 +454,8 @@ dialog.region-inline_drawer:target { inset: 0 0 0 auto; }
|
|
|
454
454
|
|
|
455
455
|
/* ---------- forms ---------- */
|
|
456
456
|
.form-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 260px), 1fr)); gap: 1rem 1.25rem; }
|
|
457
|
+
/* (167) the items of a region type that shows content of its own (a tree, chart, map …): under that content */
|
|
458
|
+
.form-grid.region-items { margin-top: 1rem; }
|
|
457
459
|
.page-items { margin-bottom: 1rem; }
|
|
458
460
|
.field { display: flex; flex-direction: column; gap: 0.3rem; min-width: 0; margin: 0; padding: 0; border: 0; }
|
|
459
461
|
.field[data-wide] { grid-column: 1 / -1; }
|
package/public/app.js
CHANGED
|
@@ -1596,6 +1596,8 @@ const pgkilnUnsaved = (() => {
|
|
|
1596
1596
|
if (!row) return;
|
|
1597
1597
|
e.preventDefault();
|
|
1598
1598
|
closeMenus(dup);
|
|
1599
|
+
// (167) a copy is not the template's default values: it is saved even when it matches them
|
|
1600
|
+
row.querySelector('input[name$="_def"]')?.remove();
|
|
1599
1601
|
for (const td of row.querySelectorAll('td[data-col]')) {
|
|
1600
1602
|
const from = src.querySelector(`td[data-col="${td.dataset.col}"]`);
|
|
1601
1603
|
if (from && control(td) && control(from)) setCell(td, control(from).tagName === 'SELECT' ? control(from).value : cellText(from));
|
|
@@ -180,7 +180,7 @@ export const COMPONENTS: Record<string, ComponentSpec> = {
|
|
|
180
180
|
{ name: 'readonly_condition', label: 'Read-only condition (SQL)', kind: 'code', group: 'Security', help: 'When true the item is shown read-only and ignored on submit.' },
|
|
181
181
|
{ name: 'authz', label: 'Authorization', kind: 'authz', group: 'Security', help: AUTHZ_HELP },
|
|
182
182
|
buildOption('Security'),
|
|
183
|
-
{ name: 'config', label: 'Attributes (JSON)', kind: 'json', group: 'Attributes', help: '{"submit_on_change":true,"null_label":"- All -","cascade_parents":"P3_DEPTNO","wide":true}. {"warn_unsaved":false}: a change of this item doesn\'t count for the page\'s "Warn on unsaved changes". Lists of values read at most {"max_rows":5000} rows. File items: {"filename_column":"photo_name","mime_column":"photo_mime","size_column":"photo_size","updated_column":"photo_updated","accept":"image/*,.pdf","max_mb":2,"capture":"environment","max_px":1600} (capture opens the camera on phones; max_px makes photos smaller before upload). {"crop":"1:1"} lets the user crop a picture before upload (free, 1:1, 4:3, 3:4, 16:9, 3:2, 2:3). Several files: {"multiple":true,"max_files":5,"table":"doc.attachment","parent_column":"ticket_id","key_column":"id"} with the content column as source (one row per file). Text items: {"scan":true} adds a barcode/QR scan button where the browser can read codes. Location items hold "lat,lng" with a "Use my location" button. Rich text: sanitised HTML; markdown: Markdown text ({"rows":10}). Rating: {"max":5} stars, stored as 1..max. Combobox: free text with list-of-values suggestions, values colon-separated ({"multiple":false} for one value). Shuttle: two lists (available, chosen) with move and order buttons; the chosen values colon-separated in their order. Select One / Select Many: a search field over the list of values (Select Many: chosen values as chips, colon-separated); {"null_label":"- None -"} or false for Select One. Date range: "from:to" (ISO dates; split_part(:P1_X, \':\', 1)). QR code: shows the value as a QR code ({"ecc":"M","size":200,"show_value":true}). Password: {"reveal":true} adds a show/hide button. Image (display image): the value is an https URL, a path or a data:image URL; or {"sql":"select photo, photo_mime from hr.emp where id = :P3_ID"} shows the first column of the first row (a BLOB); {"alt":"Photo of &P3_NAME.","width":160,"height":160}. Address: {"coordinates_item":"P3_LOCATION","country":"nl"} (a "Check address" button where the installation has a geocoder, GEOCODER_URL; the chosen address\'s position goes into the item as "lat,lng"). Any item: {"persist":"user"} keeps its value per user across sessions (a user preference named like the item). Number and display items: {"format_mask":"999G999G990D00"} shows the number in the language\'s notation (FML999G990D00 adds the currency) and reads it back on submit. · plugin: {"plugin":"name","attributes":{"NAME":"value"}} (an item plug-in: a text field its JavaScript enhances)' },
|
|
183
|
+
{ name: 'config', label: 'Attributes (JSON)', kind: 'json', group: 'Attributes', help: '{"submit_on_change":true,"null_label":"- All -","cascade_parents":"P3_DEPTNO","wide":true}. {"warn_unsaved":false}: a change of this item doesn\'t count for the page\'s "Warn on unsaved changes". Lists of values read at most {"max_rows":5000} rows. File items: {"filename_column":"photo_name","mime_column":"photo_mime","size_column":"photo_size","updated_column":"photo_updated","accept":"image/*,.pdf","max_mb":2,"capture":"environment","max_px":1600} (capture opens the camera on phones; max_px makes photos smaller before upload). {"crop":"1:1"} lets the user crop a picture before upload (free, 1:1, 4:3, 3:4, 16:9, 3:2, 2:3). Several files: {"multiple":true,"max_files":5,"table":"doc.attachment","parent_column":"ticket_id","key_column":"id"} with the content column as source (one row per file). Text items: {"scan":true} adds a barcode/QR scan button where the browser can read codes. Location items hold "lat,lng" with a "Use my location" button. Rich text: sanitised HTML; markdown: Markdown text ({"rows":10}). Rating: {"max":5} stars, stored as 1..max. Combobox: free text with list-of-values suggestions, values colon-separated ({"multiple":false} for one value). Shuttle: two lists (available, chosen) with move and order buttons; the chosen values colon-separated in their order. Select One / Select Many: a search field over the list of values (Select Many: chosen values as chips, colon-separated); {"null_label":"- None -"} or false for Select One. Date range: "from:to" (ISO dates; split_part(:P1_X, \':\', 1)). QR code: shows the value as a QR code ({"ecc":"M","size":200,"show_value":true}). Password: {"reveal":true} adds a show/hide button. Image (display image): the value is an https URL, a path or a data:image URL; or {"sql":"select photo, photo_mime from hr.emp where id = :P3_ID"} shows the first column of the first row (a BLOB); {"alt":"Photo of &P3_NAME.","width":160,"height":160}. Address: {"coordinates_item":"P3_LOCATION","country":"nl"} (a "Check address" button where the installation has a geocoder, GEOCODER_URL; the chosen address\'s position goes into the item as "lat,lng"). Hidden items: {"value_protected":false} renders the item as a hidden input that dynamic actions and JavaScript may set; its posted value is user input, like a text field\'s (protected, the default: the value stays on the server). Any item: {"persist":"user"} keeps its value per user across sessions (a user preference named like the item). Number and display items: {"format_mask":"999G999G990D00"} shows the number in the language\'s notation (FML999G990D00 adds the currency) and reads it back on submit. · plugin: {"plugin":"name","attributes":{"NAME":"value"}} (an item plug-in: a text field its JavaScript enhances)' },
|
|
184
184
|
],
|
|
185
185
|
},
|
|
186
186
|
button: {
|
|
@@ -13,7 +13,7 @@ export const REGION_CONFIG_HELP: [string[], string][] = [
|
|
|
13
13
|
[['chart'], '{"kind":"bar"|"column"|"line"|"area"|"donut","max_rows":1000,"format_mask":"FML999G990"}'],
|
|
14
14
|
[['cards'], '{"style":"metric","link":{...},"max_rows":500}'],
|
|
15
15
|
[['dynamic'], '{"max_rows":1000}'],
|
|
16
|
-
[['grid'], '{"page_size":25,"allow":{"insert":true,"update":true,"delete":true},"readonly":["col"],"columns":{"deptno":{"lov":"LOV:DEPARTMENTS","required":true}},"aggregates":{"sal":["sum","avg"]},"layout":{"order":["col"],"hidden":["col"],"widths":{"col":160},"frozen":1},"row_actions":{"edit":{"page":3,"items":{"P3_ID":"#id#"}},"duplicate":true,"delete":true,"links":[{"label":"…","page":4,"items":{}}]},"select_row":{"column":"id","item":"P1_ID"} (a master),"master":{"item":"P1_ID","column":"parent_id"} (a detail; any region type),"actions":true,"saved_reports":true,"public_reports":"SCHEME","views":["row","group","chart"] (views besides the grid; left out: all three)}'],
|
|
16
|
+
[['grid'], '{"page_size":25,"allow":{"insert":true,"update":true,"delete":true},"readonly":["col"],"columns":{"deptno":{"lov":"LOV:DEPARTMENTS","required":true,"default":"&P1_DEPTNO."},"hiredate":{"default_sql":"select current_date"}} (default / default_sql: the value a new row starts with),"aggregates":{"sal":["sum","avg"]},"layout":{"order":["col"],"hidden":["col"],"widths":{"col":160},"frozen":1},"row_actions":{"edit":{"page":3,"items":{"P3_ID":"#id#"}},"duplicate":true,"delete":true,"links":[{"label":"…","page":4,"items":{}}]},"select_row":{"column":"id","item":"P1_ID"} (a master),"master":{"item":"P1_ID","column":"parent_id"} (a detail; any region type),"actions":true,"saved_reports":true,"public_reports":"SCHEME","views":["row","group","chart"] (views besides the grid; left out: all three)}'],
|
|
17
17
|
[['calendar'], '{"link":{...}}'],
|
|
18
18
|
[['facets'], '{"report":<region id>,"search":true,"facets":[{"column":"job","label":"Job","exclude":true},{"column":"sal","type":"range","ranges":[{"to":1000},{"from":1000}],"custom":true},{"column":"rating","type":"star","max":5}]}'],
|
|
19
19
|
[['smart_filters'], '{"report":<region id>,"suggestions":3,"placeholder":"…","ask":true,"facets":[…as facets]}'],
|
|
@@ -27,7 +27,7 @@ export const REGION_CONFIG_HELP: [string[], string][] = [
|
|
|
27
27
|
[['report'], '{"ai_filter":{}} adds an "Ask in your own words" box: the question becomes the report\'s settings, shown as chips (built-in phrases; {"service":"NAME"} uses an AI service)'],
|
|
28
28
|
[['plugin'], '{"plugin":"name","attributes":{"NAME":"value"}} (a region plug-in; a Source SELECT feeds its template)'],
|
|
29
29
|
[['form'], '{"lost_update":false} saves without checking that the row is unchanged since it was shown'],
|
|
30
|
-
[['login'], '(
|
|
30
|
+
[['login'], '{"autofocus":false} (the cursor is not put in the user name field) · the sign-in form of the application\'s login page (Application settings → Login page): providers, user name and password, "Keep me signed in", "Forgot password?"'],
|
|
31
31
|
[['url'], '{"height":480,"sandbox":["allow-scripts","allow-forms","allow-popups","allow-same-origin"] (the default; false: no sandbox),"fullscreen":true}'],
|
|
32
32
|
[['classic_report'], '{"report_template":"standard"|"value_pairs"|"badges"|"media_list","pagination":"total" (rows X–Y of N, the default)|"range" (rows X–Y)|"none" (one page of at most max_rows, 500),"page_size":15,"max_rows":500,"sortable":true,"searchable":false,"mobile":"reflow"|"scroll","hidden":["col"],"headings":{"col":"Label"},"link":{"column":"id","page":3,"items":{"P3_ID":"#id#"}},"empty":"No rows"} (a report without the Actions menu; badges: label and value columns; media_list: title and description columns)'],
|
|
33
33
|
[['breadcrumb'], '{"current":false} leaves the current page out of the trail (the pages above it come from each page\'s Breadcrumb parent, else Home); put it in the Breadcrumb bar position with template plain'],
|
package/src/runtime/authz.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { savepoint } from '../db.ts';
|
|
|
5
5
|
import { bindValues, publicError, stripSemicolon, type PageContext, type Visibility } from './context.ts';
|
|
6
6
|
import { saveButton, saveRequest } from './grid.ts';
|
|
7
7
|
import { selectionOf } from './report.ts';
|
|
8
|
+
import { unprotectedHidden } from './items.ts';
|
|
8
9
|
import { conditionHolds, conditionOf, serverConditionOf } from './logic.ts';
|
|
9
10
|
|
|
10
11
|
export class Forbidden extends Error {}
|
|
@@ -99,6 +100,12 @@ export async function computeVisibility(ctx: PageContext): Promise<Visibility> {
|
|
|
99
100
|
if (!(await isAuthorized(ctx, i.authz))) continue;
|
|
100
101
|
if (i.condition_type && !(await conditionHolds(ctx, i as Condition, `condition of item ${i.name}`))) continue;
|
|
101
102
|
vis.items.add(i.name);
|
|
103
|
+
// (167) a hidden item with {"value_protected": false} is a field of the page (APEX: Value Protected = No):
|
|
104
|
+
// the browser may set it, and a submit or dynamic action takes its posted value, as any field's
|
|
105
|
+
if (i.type === 'hidden' && unprotectedHidden(i)) {
|
|
106
|
+
vis.editable.add(i.name);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
102
109
|
if (i.type === 'hidden' || i.type === 'display' || i.type === 'qrcode' || i.type === 'image' || i.type === 'percent_graph' || i.type === 'display_map') continue;
|
|
103
110
|
// fail closed: a broken read-only condition makes the item read-only
|
|
104
111
|
if (i.readonly_condition && (await sqlTrue(ctx, i.readonly_condition, `read-only condition of ${i.name}`, true))) continue;
|
package/src/runtime/grid.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { componentSetting } from './component-settings.ts';
|
|
2
2
|
import pg from 'pg';
|
|
3
|
-
import { literal } from '../binds.ts';
|
|
3
|
+
import { applyBinds, literal } from '../binds.ts';
|
|
4
4
|
import { savepoint, type Client } from '../db.ts';
|
|
5
5
|
import { html, raw, type Raw } from '../html.ts';
|
|
6
6
|
import { icon } from '../icons.ts';
|
|
7
7
|
import type { Button, Process, Region } from '../metadata.ts';
|
|
8
8
|
import { checksumValid, urlChecksum } from '../security.ts';
|
|
9
9
|
import { isAuthorized, pageAllowed } from './authz.ts';
|
|
10
|
-
import { publicError, toState, type PageContext } from './context.ts';
|
|
10
|
+
import { bindValues, publicError, stripSemicolon, substitute, toState, type PageContext } from './context.ts';
|
|
11
11
|
import { arrange, cleanLayout, currentLayout, defaultLayout, layoutJson, layoutParam, MAX_FROZEN, MAX_WIDTH, MIN_WIDTH, parseLayout, type GridLayout, type Placed } from './grid-layout.ts';
|
|
12
12
|
import { appDateMask, dateField, lovContains, lovOptions, type LovOption } from './items.ts';
|
|
13
13
|
import { parseDateMask } from './format.ts';
|
|
@@ -22,7 +22,8 @@ import { GRID_VIEWS, renderView, type View } from './report-views.ts';
|
|
|
22
22
|
//
|
|
23
23
|
// config: {"page_size": 25,
|
|
24
24
|
// "allow": {"insert": true, "update": true, "delete": true},
|
|
25
|
-
// "columns": {"deptno": {"lov": "LOV:DEPARTMENTS"}, "sal": {"required": true}
|
|
25
|
+
// "columns": {"deptno": {"lov": "LOV:DEPARTMENTS"}, "sal": {"required": true},
|
|
26
|
+
// "hiredate": {"default_sql": "select current_date"}, "job": {"default": "&P3_JOB."}},
|
|
26
27
|
// "readonly": ["hiredate"],
|
|
27
28
|
// "aggregates": {"sal": ["sum", "avg"], "empno": "count"},
|
|
28
29
|
// "layout": {"order": ["ename", "sal"], "hidden": ["comm"], "widths": {"ename": 180}, "frozen": 1},
|
|
@@ -240,6 +241,14 @@ export async function renderGrid(ctx: PageContext, r: Region): Promise<Raw> {
|
|
|
240
241
|
const insertable = savable && allow(r, 'insert') && cols.some((x) => writable.has(x.name));
|
|
241
242
|
const deletable = savable && allow(r, 'delete');
|
|
242
243
|
const g = `g${r.id}`;
|
|
244
|
+
// (167) the values a new row starts with (APEX: a column's Default)
|
|
245
|
+
let defaults: Record<number, string> = {};
|
|
246
|
+
if (insertable)
|
|
247
|
+
try {
|
|
248
|
+
defaults = await newRowDefaults(ctx, r, cols, writable);
|
|
249
|
+
} catch (e) {
|
|
250
|
+
return html`<div class="alert alert-error" role="alert">${await publicError(ctx, e, `grid "${r.title ?? r.id}" column defaults`)}</div>`;
|
|
251
|
+
}
|
|
243
252
|
const posted = ctx.body && Object.keys(ctx.body).some((k) => k.startsWith(`${g}_`)) ? ctx.body : null;
|
|
244
253
|
const postedByPk = new Map<string, number>();
|
|
245
254
|
if (posted) for (const k of Object.keys(posted)) {
|
|
@@ -360,7 +369,10 @@ export async function renderGrid(ctx: PageContext, r: Region): Promise<Raw> {
|
|
|
360
369
|
});
|
|
361
370
|
|
|
362
371
|
// New rows: re-show posted ones after a failed save, a duplicated row, plus a blank template.
|
|
363
|
-
|
|
372
|
+
// a new row posts the values it started with (_def): one left at them is blank, as an empty one (gridDml)
|
|
373
|
+
const defInput = (name: string, from: Record<number, string>) =>
|
|
374
|
+
Object.keys(from).length ? html`<input type="hidden" name="${name}" value="${JSON.stringify(from)}">` : '';
|
|
375
|
+
const newRow = (j: number, values: Record<number, string> = defaults, started: Record<number, string> = defaults) =>
|
|
364
376
|
html`<tr class="grid-new" data-new-row="${j}">
|
|
365
377
|
${leads.map((k) => html`<td class="${leadCls(k)}" data-label="${k === 'sel' ? t('grid.new') : ''}">${k === 'sel' ? html`<span class="badge-pill">${t('grid.new')}</span>` : ''}</td>`)}
|
|
366
378
|
${placed.map((p, pIdx) => {
|
|
@@ -371,6 +383,7 @@ export async function renderGrid(ctx: PageContext, r: Region): Promise<Raw> {
|
|
|
371
383
|
? html`<td class="${colCls(p, pIdx)}" data-col="${col.ci}" data-label="${lbl}"${hiddenAttr(p)}>${control(ctx, { ...col, editable: true }, `${g}_n${j}_c${col.ci}`, values[col.ci] ?? '', `${lbl}, new row`)}</td>`
|
|
372
384
|
: html`<td class="${colCls(p, pIdx)}" data-col="${col.ci}" data-label="${lbl}"${hiddenAttr(p)}></td>`;
|
|
373
385
|
})}
|
|
386
|
+
${Object.keys(started).length ? html`<td hidden>${defInput(`${g}_n${j}_def`, started)}</td>` : ''}
|
|
374
387
|
</tr>`;
|
|
375
388
|
const postedNew: Raw[] = [];
|
|
376
389
|
const usedNew = posted ? newRowIndexes(posted, g) : [];
|
|
@@ -378,7 +391,8 @@ export async function renderGrid(ctx: PageContext, r: Region): Promise<Raw> {
|
|
|
378
391
|
for (const j of usedNew) {
|
|
379
392
|
const values: Record<number, string> = {};
|
|
380
393
|
cols.forEach((col) => (values[col.ci] = pv(`${g}_n${j}_c${col.ci}`) ?? ''));
|
|
381
|
-
|
|
394
|
+
const started = postedDefaults(pv(`${g}_n${j}_def`));
|
|
395
|
+
if (!blankRow(values, started)) postedNew.push(newRow(j, values, started));
|
|
382
396
|
}
|
|
383
397
|
let nextNew = usedNew.length ? Math.max(...usedNew) + 1 : 0;
|
|
384
398
|
// Duplicate without JavaScript (r<id>_dup=<key>): a row of this page as a new row
|
|
@@ -387,7 +401,7 @@ export async function renderGrid(ctx: PageContext, r: Region): Promise<Raw> {
|
|
|
387
401
|
if (dupRow) {
|
|
388
402
|
const values: Record<number, string> = {};
|
|
389
403
|
for (const col of cols) if (writable.has(col.name)) values[col.ci] = toState(dupRow[col.idx]) ?? '';
|
|
390
|
-
postedNew.push(newRow(nextNew++, values));
|
|
404
|
+
postedNew.push(newRow(nextNew++, values, {}));
|
|
391
405
|
}
|
|
392
406
|
|
|
393
407
|
// The single row view: one row as a form, with the grid's field names (row 0, or new row 0)
|
|
@@ -440,9 +454,10 @@ export async function renderGrid(ctx: PageContext, r: Region): Promise<Raw> {
|
|
|
440
454
|
const col = p.col;
|
|
441
455
|
if (masterCol && col.name === masterCol) return [field(p, masterValue, false)];
|
|
442
456
|
if (!writable.has(col.name)) return [];
|
|
443
|
-
return [field(p, control(ctx, { ...col, editable: true }, `${g}_n0_c${col.ci}`, pv(`${g}_n0_c${col.ci}`) ?? '', label(col.name)), true)];
|
|
457
|
+
return [field(p, control(ctx, { ...col, editable: true }, `${g}_n0_c${col.ci}`, pv(`${g}_n0_c${col.ci}`) ?? defaults[col.ci] ?? '', label(col.name)), true)];
|
|
444
458
|
});
|
|
445
|
-
|
|
459
|
+
const started = pv(`${g}_n0_def`) !== undefined ? postedDefaults(pv(`${g}_n0_def`)) : defaults;
|
|
460
|
+
return html`<div class="grid-record grid-new" data-new-row="0"><div class="form-grid">${fieldsHtml}</div>${defInput(`${g}_n0_def`, started)}</div>
|
|
446
461
|
${recordNav()}`;
|
|
447
462
|
};
|
|
448
463
|
|
|
@@ -681,6 +696,48 @@ export function layoutFromForm(b: Record<string, string | undefined>): GridLayou
|
|
|
681
696
|
});
|
|
682
697
|
}
|
|
683
698
|
|
|
699
|
+
/**
|
|
700
|
+
* (167) The values a new row starts with, by column position: config.columns.<col>.default (text with &ITEM.
|
|
701
|
+
* substitutions, e.g. "&P3_DEPTNO.") or .default_sql (a SELECT: its first column of the first row, with :BINDS),
|
|
702
|
+
* for the columns a new row may set.
|
|
703
|
+
*/
|
|
704
|
+
async function newRowDefaults(ctx: PageContext, r: Region, cols: { name: string; ci: number }[], writable: Set<string>) {
|
|
705
|
+
const cfg = (r.config.columns ?? {}) as Record<string, { default?: unknown; default_sql?: unknown }>;
|
|
706
|
+
const out: Record<number, string> = {};
|
|
707
|
+
for (const col of cols) {
|
|
708
|
+
const c = cfg[col.name];
|
|
709
|
+
if (!c || !writable.has(col.name)) continue;
|
|
710
|
+
if (typeof c.default_sql === 'string' && c.default_sql.trim()) {
|
|
711
|
+
const sql = stripSemicolon(applyBinds(c.default_sql, bindValues(ctx)));
|
|
712
|
+
const client = ctx.client!;
|
|
713
|
+
const res = await savepoint(client, () => client.query({ text: sql, rowMode: 'array' }));
|
|
714
|
+
const v = toState(res.rows[0]?.[0]);
|
|
715
|
+
if (v !== null && v !== '') out[col.ci] = v;
|
|
716
|
+
} else if (typeof c.default === 'string' || typeof c.default === 'number' || typeof c.default === 'boolean') {
|
|
717
|
+
const v = substitute(String(c.default), ctx, (x) => x);
|
|
718
|
+
if (v !== '') out[col.ci] = v;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
return out;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/** A new row's posted _def field: the values it started with (only short strings by column position are kept). */
|
|
725
|
+
function postedDefaults(raw: string | undefined): Record<number, string> {
|
|
726
|
+
if (!raw || raw.length > 20000) return {};
|
|
727
|
+
try {
|
|
728
|
+
const v = JSON.parse(raw) as unknown;
|
|
729
|
+
if (!v || typeof v !== 'object' || Array.isArray(v)) return {};
|
|
730
|
+
return Object.fromEntries(Object.entries(v).filter(([k, x]) => /^\d+$/.test(k) && typeof x === 'string'));
|
|
731
|
+
} catch {
|
|
732
|
+
return {};
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/** A new row nobody filled in: every value empty (or an unticked checkbox) or still the value it started with. */
|
|
737
|
+
function blankRow(values: Record<number, string>, started: Record<number, string>) {
|
|
738
|
+
return Object.entries(values).every(([ci, v]) => v === '' || v === 'false' || (started[Number(ci)] !== undefined && v === started[Number(ci)]));
|
|
739
|
+
}
|
|
740
|
+
|
|
684
741
|
function newRowIndexes(body: Record<string, unknown>, g: string) {
|
|
685
742
|
const out = new Set<number>();
|
|
686
743
|
for (const k of Object.keys(body)) {
|
|
@@ -797,12 +854,15 @@ export async function gridDml(ctx: PageContext, p: Process): Promise<string | nu
|
|
|
797
854
|
if (allow(r, 'insert'))
|
|
798
855
|
for (const j of newRowIndexes(body, g)) {
|
|
799
856
|
const values: [string, string][] = [];
|
|
857
|
+
const posted: Record<number, string> = {};
|
|
800
858
|
cols.forEach((col, ci) => {
|
|
801
859
|
if (!writable.has(col.name)) return;
|
|
802
860
|
const v = norm(ci, one(`${g}_n${j}_c${ci}`));
|
|
861
|
+
posted[ci] = v;
|
|
803
862
|
if (v !== '' && !(col.dataTypeID === 16 && v === 'false')) values.push([col.name, v]);
|
|
804
863
|
});
|
|
805
|
-
|
|
864
|
+
// a blank template row, or one left at its default values (167)
|
|
865
|
+
if (!values.length || blankRow(posted, postedDefaults(one(`${g}_n${j}_def`)))) continue;
|
|
806
866
|
const notOffered = [];
|
|
807
867
|
for (const [n, v] of values) if (!(await offered(n, v))) notOffered.push(n);
|
|
808
868
|
if (notOffered.length) {
|
package/src/runtime/items.ts
CHANGED
|
@@ -260,8 +260,16 @@ function displayMap(ctx: PageContext, item: Item, value: string): Raw {
|
|
|
260
260
|
</figure>`;
|
|
261
261
|
}
|
|
262
262
|
|
|
263
|
+
/** (167) A hidden item the browser may set ({"value_protected": false}): a hidden input, its posted value taken like a field's. */
|
|
264
|
+
export const unprotectedHidden = (i: { type: string; config?: Record<string, any> | null }) => i.type === 'hidden' && i.config?.value_protected === false;
|
|
265
|
+
|
|
263
266
|
export async function renderItem(ctx: PageContext, item: Item, hiddenByDa = false): Promise<Raw | ''> {
|
|
264
|
-
if (
|
|
267
|
+
if (!ctx.vis!.items.has(item.name)) return '';
|
|
268
|
+
if (item.type === 'hidden') {
|
|
269
|
+
// (167) protected (the default): the value stays on the server; {"value_protected": false}: a hidden input
|
|
270
|
+
if (!unprotectedHidden(item)) return '';
|
|
271
|
+
return html`<input type="hidden" id="${item.name}" name="${item.name}" value="${ctx.session.state[item.name] ?? item.default_value ?? ''}" data-item="${item.name}">`;
|
|
272
|
+
}
|
|
265
273
|
const value = ctx.session.state[item.name] ?? item.default_value ?? '';
|
|
266
274
|
const id = item.name;
|
|
267
275
|
const error = ctx.errors.items[item.name];
|
package/src/runtime/regions.ts
CHANGED
|
@@ -104,9 +104,11 @@ export async function renderButton(ctx: PageContext, b: Button) {
|
|
|
104
104
|
return html`<button type="submit" class="${cls}" name="__request" value="${b.name}" data-button="${b.name}"${confirm}>${b.label}${badge}</button>`;
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
export async function buttonsFor(ctx: PageContext, regionId: number | null, only: (b: Button) => boolean = () => true) {
|
|
107
|
+
export async function buttonsFor(ctx: PageContext, regionId: number | null, only: (b: Button) => boolean = () => true, rank: (b: Button) => number = () => 0) {
|
|
108
108
|
const out = [];
|
|
109
|
-
|
|
109
|
+
// in sequence, within their rank (a stable sort: the buttons come in sequence order)
|
|
110
|
+
const shown = [...ctx.vis!.buttons.values()].filter((b) => b.region_id === regionId && b.id > 0 && only(b)).sort((a, b) => rank(a) - rank(b));
|
|
111
|
+
for (const b of shown) out.push(await renderButton(ctx, b));
|
|
110
112
|
return out.some(Boolean) ? html`<div class="buttons">${out}</div>` : '';
|
|
111
113
|
}
|
|
112
114
|
|
|
@@ -314,6 +316,9 @@ async function renderRegionNow(ctx: PageContext, r: Region, hidden: Set<string>)
|
|
|
314
316
|
return regionShell(ctx, r, hidden, body);
|
|
315
317
|
}
|
|
316
318
|
|
|
319
|
+
/** Region types that place their items themselves (and login, whose fields post to a form of their own). */
|
|
320
|
+
const ITEMS_IN_BODY = new Set(['report', 'classic_report', 'form', 'static', 'login']);
|
|
321
|
+
|
|
317
322
|
/** The region's content (what the region's type renders), without its frame and buttons. */
|
|
318
323
|
async function renderBody(ctx: PageContext, r: Region, hidden: Set<string>): Promise<Raw> {
|
|
319
324
|
const items = ctx.page.items.filter((i) => i.region_id === r.id);
|
|
@@ -407,7 +412,7 @@ async function renderBody(ctx: PageContext, r: Region, hidden: Set<string>): Pro
|
|
|
407
412
|
body = await renderSearch(ctx, r);
|
|
408
413
|
break;
|
|
409
414
|
case 'login':
|
|
410
|
-
body = await renderLoginRegion(ctx);
|
|
415
|
+
body = await renderLoginRegion(ctx, r.config ?? {});
|
|
411
416
|
break;
|
|
412
417
|
case 'dynamic':
|
|
413
418
|
// A SELECT returning HTML (like APEX "PL/SQL Dynamic Content"). The
|
|
@@ -425,6 +430,12 @@ async function renderBody(ctx: PageContext, r: Region, hidden: Set<string>): Pro
|
|
|
425
430
|
break;
|
|
426
431
|
}
|
|
427
432
|
}
|
|
433
|
+
// (167) items placed in any other region (a tree, chart, map, cards …) are shown under its content, as in APEX;
|
|
434
|
+
// a login region's fields belong to a form of their own, so its items stay out
|
|
435
|
+
if (items.length && !ITEMS_IN_BODY.has(r.type) && !restFailed) {
|
|
436
|
+
const itemsHtml = await renderItems(ctx, items, hidden);
|
|
437
|
+
if (itemsHtml.length) body = html`${body!}<div class="form-grid region-items">${itemsHtml}</div>`;
|
|
438
|
+
}
|
|
428
439
|
return body!;
|
|
429
440
|
}
|
|
430
441
|
|
|
@@ -511,7 +522,8 @@ async function slottedShell(ctx: PageContext, r: Region, hidden: Set<string>, bo
|
|
|
511
522
|
const plain = (b: Button) => !b.slot || b.slot === 'region';
|
|
512
523
|
const top = await buttonsFor(ctx, r.id, (b) => TOP_SLOTS.has(b.slot ?? '') || (onTop && plain(b)));
|
|
513
524
|
const left = await buttonsFor(ctx, r.id, (b) => LEFT_SLOTS.has(b.slot ?? ''));
|
|
514
|
-
|
|
525
|
+
// (167) Next is the last button of the bar, as in APEX: the region's own buttons come before it
|
|
526
|
+
const right = await buttonsFor(ctx, r.id, (b) => b.slot === 'next' || (!onTop && plain(b)), (b) => (b.slot === 'next' ? 1 : 0));
|
|
515
527
|
const cls = `region region-${r.type} region-${r.template} col-${r.columns}${templateClasses('region', r.template_options)}`;
|
|
516
528
|
const hiddenAttr = hidden.has(`R${r.id}`) ? raw(' hidden') : '';
|
|
517
529
|
const staticAttr = r.static_id ? raw(` data-static-id="${esc(r.static_id)}"`) : '';
|
package/src/runtime/signin.ts
CHANGED
|
@@ -34,7 +34,7 @@ export const LOGIN_USER = '__LOGIN_USER';
|
|
|
34
34
|
|
|
35
35
|
export async function signInForm(
|
|
36
36
|
app: App, locale: Locale, session: Session, next: string,
|
|
37
|
-
o: { error?: string; username?: string; formId?: string } = {},
|
|
37
|
+
o: { error?: string; username?: string; formId?: string; autofocus?: boolean } = {},
|
|
38
38
|
): Promise<Raw> {
|
|
39
39
|
const t = locale.t;
|
|
40
40
|
// database accounts and custom authentication: the user name and password form only
|
|
@@ -48,7 +48,7 @@ export async function signInForm(
|
|
|
48
48
|
const fields = html`<input type="hidden" name="__csrf" value="${session.csrf_token}"${fa}>
|
|
49
49
|
<input type="hidden" name="next" value="${next}"${fa}>
|
|
50
50
|
${app.time_zone_auto ? html`<input type="hidden" name="__tz" value=""${fa}>` : ''}
|
|
51
|
-
<div class="field"><label class="label" for="username">${t('login.username')}</label><input id="username" name="username" value="${o.username ?? ''}" autocomplete="username" autofocus required maxlength="100"${fa}></div>
|
|
51
|
+
<div class="field"><label class="label" for="username">${t('login.username')}</label><input id="username" name="username" value="${o.username ?? ''}" autocomplete="username"${o.autofocus === false ? '' : raw(' autofocus')} required maxlength="100"${fa}></div>
|
|
52
52
|
<div class="field"><label class="label" for="password">${t('login.password')}</label><input id="password" name="password" type="password" autocomplete="current-password" required maxlength="200"${fa}></div>
|
|
53
53
|
${remember ? html`<label class="check"><input type="checkbox" name="remember" value="true"${fa}> ${t('login.remember', { days: remember })}</label>` : ''}
|
|
54
54
|
<button class="btn btn-hot"${fa}>${t('login.submit')}</button>
|
|
@@ -65,8 +65,8 @@ export async function signInForm(
|
|
|
65
65
|
: providers.length ? '' : html`<p>${t('login.none')}</p>`}`;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
/** The "login" region of an application's own login page. */
|
|
69
|
-
export async function renderLoginRegion(ctx: PageContext): Promise<Raw> {
|
|
68
|
+
/** The "login" region of an application's own login page; (167) {"autofocus": false} leaves the focus where the page puts it. */
|
|
69
|
+
export async function renderLoginRegion(ctx: PageContext, config: { autofocus?: unknown } = {}): Promise<Raw> {
|
|
70
70
|
const s = ctx.session.state;
|
|
71
71
|
const error = s[LOGIN_ERROR] ?? undefined;
|
|
72
72
|
const username = s[LOGIN_USER] ?? undefined;
|
|
@@ -76,6 +76,6 @@ export async function renderLoginRegion(ctx: PageContext): Promise<Raw> {
|
|
|
76
76
|
ctx.detached.push(html`<form id="${formId}" method="post" action="/a/${ctx.app.alias}/login"></form>`);
|
|
77
77
|
const next = safeNext(ctx.app, ctx.params.get('next') ?? undefined);
|
|
78
78
|
const here = `${ctx.base}/${ctx.page.page_no}${ctx.params.get('next') ? `?next=${encodeURIComponent(next)}` : ''}`;
|
|
79
|
-
return html`<div class="login-region">${await signInForm(ctx.app, ctx.locale, ctx.session, next, { error, username, formId })}
|
|
79
|
+
return html`<div class="login-region">${await signInForm(ctx.app, ctx.locale, ctx.session, next, { error, username, formId, autofocus: config.autofocus !== false })}
|
|
80
80
|
${languagePicker(ctx.app, ctx.locale, here)}</div>`;
|
|
81
81
|
}
|