diff --git a/CHANGELOG.md b/CHANGELOG.md index db8fe4d0..8baa84bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,10 @@ # CHANGELOG.md -## v0.38.1 +## v0.39.0 - Ability to execute sql for URL paths with another extension. If you create sitemap.xml.sql, it will be executed for example.com/sitemap.xml - Display source line info in errors even when the database does not return a precise error position. In this case, the entire problematic SQL statement is referenced. - The shell with a vertical sidebar can now have "active" elements, just like the horizontal header bar. + - New `edit_url`, `delete_url`, and `custom_actions` properties in the [table](https://sql-page.com/component.sql?component=table) component to easily add nice icon buttons to a table. ## v0.38.0 - Added support for the Open Database Connectivity (ODBC) standard. diff --git a/Cargo.lock b/Cargo.lock index 836767d2..2bfa8581 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4219,7 +4219,7 @@ dependencies = [ [[package]] name = "sqlpage" -version = "0.38.1" +version = "0.39.0" dependencies = [ "actix-multipart", "actix-rt", diff --git a/Cargo.toml b/Cargo.toml index 373c1078..f4a84374 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sqlpage" -version = "0.38.1" +version = "0.39.0" edition = "2021" description = "Build data user interfaces entirely in SQL. A web server that takes .sql files and formats the query result using pre-made configurable professional-looking components." keywords = ["web", "sql", "framework"] diff --git a/examples/official-site/sqlpage/migrations/01_documentation.sql b/examples/official-site/sqlpage/migrations/01_documentation.sql index ee2b8b51..dabd6d76 100644 --- a/examples/official-site/sqlpage/migrations/01_documentation.sql +++ b/examples/official-site/sqlpage/migrations/01_documentation.sql @@ -810,11 +810,15 @@ INSERT INTO parameter(component, name, description, type, top_level, optional) S ('money', 'Name of a numeric column whose values should be displayed as currency amounts, in the currency defined by the `currency` property. This argument can be repeated multiple times.', 'TEXT', TRUE, TRUE), ('currency', 'The ISO 4217 currency code (e.g., USD, EUR, GBP, etc.) to use when formatting monetary values.', 'TEXT', TRUE, TRUE), ('number_format_digits', 'Maximum number of decimal digits to display for numeric values.', 'INTEGER', TRUE, TRUE), + ('edit_url', 'If set, an edit button will be added to each row. The value of this property should be a URL, possibly containing the `{id}` placeholder that will be replaced by the value of the `_sqlpage_id` property for that row. Clicking the edit button will take the user to that URL. Added in v0.39.0', 'TEXT', TRUE, TRUE), + ('delete_url', 'If set, a delete button will be added to each row. The value of this property should be a URL, possibly containing the `{id}` placeholder that will be replaced by the value of the `_sqlpage_id` property for that row. Clicking the delete button will take the user to that URL. Added in v0.39.0', 'TEXT', TRUE, TRUE), + ('custom_actions', 'If set, a column of custom action buttons will be added to each row. The value of this property should be a JSON array of objects, each object defining a button with the following properties: `name` (the text to display on the button), `icon` (the tabler icon name or image link to display on the button), `link` (the URL to navigate to when the button is clicked, possibly containing the `{id}` placeholder that will be replaced by the value of the `_sqlpage_id` property for that row), and `tooltip` (optional text to display when hovering over the button). Added in v0.39.0', 'JSON', TRUE, TRUE), -- row level ('_sqlpage_css_class', 'For advanced users. Sets a css class on the table row. Added in v0.8.0.', 'TEXT', FALSE, TRUE), ('_sqlpage_color', 'Sets the background color of the row. Added in v0.8.0.', 'COLOR', FALSE, TRUE), ('_sqlpage_footer', 'Sets this row as the table footer. It is recommended that this parameter is applied to the last row. Added in v0.34.0.', 'BOOLEAN', FALSE, TRUE), - ('_sqlpage_id', 'Sets the id of the html tabler row element. Allows you to make links targeting a specific row in a table.', 'TEXT', FALSE, TRUE) + ('_sqlpage_id', 'Sets the id of the html tabler row element. Allows you to make links targeting a specific row in a table.', 'TEXT', FALSE, TRUE), + ('_sqlpage_actions', 'Sets custom action buttons for this specific row in addition to any defined at the table level, The value of this property should be a JSON array of objects, each object defining a button with the following properties: `name` (the text to display on the button), `icon` (the tabler icon name or image link to display on the button), `link` (the URL to navigate to when the button is clicked, possibly containing the `{id}` placeholder that will be replaced by the value of the `_sqlpage_id` property for that row), and `tooltip` (optional text to display when hovering over the button). Added in v0.39.0', 'JSON', FALSE, TRUE) ) x; INSERT INTO example(component, description, properties) VALUES @@ -994,7 +998,124 @@ GROUP BY This will generate a table with the stores in the first column, and the items in the following columns, with the quantity sold in each store for each item. ', NULL - ); + ), + ( + 'table', +'## Using Action Buttons in a table. + +### Preset Actions: `edit_url` & `delete_url` +Since edit and delete are common actions, the `table` component has dedicated `edit_url` and `delete_url` properties to add buttons for these actions. +The value of these properties should be a URL, containing the `{id}` placeholder that will be replaced by the value of the `_sqlpage_id` property for that row. + +### Column with fixed action buttons + +You may want to add custom action buttons to your table rows, for instance to view details, download a file, or perform a custom operation. +For this, the `table` component has a `custom_actions` top-level property that lets you define a column of buttons, each button defined by a name, an icon, a link, and an optional tooltip. + +### Column with variable action buttons + +The `table` component also supports the row level `_sqlpage_actions` column in your data table. +This is helpful if you want a more complex logic, for instance to disable a button on some rows, or to change the link or icon based on the row data. + +> WARNING! +> If the number of array items in `_sqlpage_actions` is not consistent across all rows, the table may not render correctly. +> You can leave blank spaces by including an object with only the `name` property. + +The table has a column of buttons, each button defined by the `_sqlpage_actions` column at the table level, and by the `_sqlpage_actions` property at the row level. +### `custom_actions` & `_sqlpage_actions` JSON properties. +Each button is defined by the following properties: +* `name`: sets the column header and the tooltip if no tooltip is provided, +* `tooltip`: text to display when hovering over the button, +* `link`: the URL to navigate to when the button is clicked, possibly containing the `{id}` placeholder that will be replaced by the value of the `_sqlpage_id` property for that row, +* `icon`: the tabler icon name or image link to display on the button + +### Example using all of the above +' + , + json('[ + { + "component": "table", + "edit_url": "/examples/show_variables.sql?action=edit&update_id={id}", + "delete_url": "/examples/show_variables.sql?action=delete&delete_id={id}", + "custom_actions": [ + { + "name": "history", + "tooltip": "View Standard History", + "link": "/examples/show_variables.sql?action=history&standard_id={id}", + "icon": "history" + } + ] + }, + { + "name": "CalStd", + "vendor": "PharmaCo", + "Product": "P1234", + "lot number": "T23523", + "status": "Available", + "expires on": "2026-10-13", + "_sqlpage_id": 32, + "_sqlpage_actions": [ + { + "name": "View PDF", + "tooltip": "View Presentation", + "link": "https://sql-page.com/pgconf/2024-sqlpage-badass.pdf", + "icon": "file-type-pdf" + }, + { + "name": "Action", + "tooltip": "Set In Use", + "link": "/examples/show_variables.sql?action=set_in_use&standard_id=32", + "icon": "caret-right" + } + ] + }, + { + "name": "CalStd", + "vendor": "PharmaCo", + "Product": "P1234", + "lot number": "T2352", + "status": "In Use", + "expires on": "2026-10-14", + "_sqlpage_id": 33, + "_sqlpage_actions": [ + { + "name": "View PDF", + "tooltip": "View Presentation", + "link": "https://sql-page.com/pgconf/2024-sqlpage-badass.pdf", + "icon": "file-type-pdf" + }, + { + "name": "Action", + "tooltip": "Retire Standard", + "link": "/examples/show_variables.sql?action=retire&standard_id=33", + "icon": "test-pipe-off" + } + ] + }, + { + "name": "CalStd", + "vendor": "PharmaCo", + "Product": "P1234", + "lot number": "A123", + "status": "Discarded", + "expires on": "2026-09-30", + "_sqlpage_id": 31, + "_sqlpage_actions": [ + { + "name": "View PDF", + "tooltip": "View Presentation", + "link": "https://sql-page.com/pgconf/2024-sqlpage-badass.pdf", + "icon": "file-type-pdf" + }, + { + "name": "Action" + } + ] + } +]' +) +); + INSERT INTO component(name, icon, description) VALUES diff --git a/sqlpage/templates/table.handlebars b/sqlpage/templates/table.handlebars index 90958dfe..bf1cd3e4 100644 --- a/sqlpage/templates/table.handlebars +++ b/sqlpage/templates/table.handlebars @@ -3,6 +3,7 @@ {{#if (or search initial_search_value)}}
{{/if}} {{/each}} + {{#if ../edit_url}}Edit{{/if}} + {{#if ../delete_url}}Delete{{/if}} + {{#if ../custom_actions}} + {{#each ../custom_actions}} + {{this.name}} + {{/each}} + {{/if}} + {{#if _sqlpage_actions}} + {{#each _sqlpage_actions}} + {{this.name}} + {{/each}} + {{/if}} {{#delay}}{{/delay}} @@ -78,6 +91,38 @@ {{/if~}} {{~/each~}} + {{#if ../edit_url}} + + + {{~icon_img 'edit'~}} + + + {{/if}} + {{#if ../delete_url}} + + + {{~icon_img 'trash'~}} + + + {{/if}} + {{#if ../custom_actions}} + {{#each ../custom_actions}} + + {{!Title property sets the tooltip text}} + {{~icon_img this.icon~}} + + + {{/each}} + {{/if}} + {{#if _sqlpage_actions}} + {{#each _sqlpage_actions}} + + + {{~icon_img this.icon~}} + + + {{/each}} + {{/if}} {{!~ After this has been rendered, if this was a footer, we need to reopen a new diff --git a/tests/end-to-end/official-site.spec.ts b/tests/end-to-end/official-site.spec.ts index 7d840811..eda20e7e 100644 --- a/tests/end-to-end/official-site.spec.ts +++ b/tests/end-to-end/official-site.spec.ts @@ -213,3 +213,87 @@ test("modal", async ({ page }) => { await modal.getByRole("button", { label: "Close" }).first().click(); await expect(modal).not.toBeVisible(); }); + +test("table action buttons - edit_url and delete_url", async ({ page }) => { + await page.goto(`${BASE}/documentation.sql?component=table`); + const tableSection = page.locator(".table-responsive", { + has: page.getByRole("cell", { name: "PharmaCo" }), + }); + + const editButton = tableSection.getByTitle("Edit").first(); + await expect(editButton).toBeVisible(); + await expect(editButton).toHaveAttribute("href", /action=edit&update_id=\d+/); + + const deleteButton = tableSection.getByTitle("Delete").first(); + await expect(deleteButton).toBeVisible(); + await expect(deleteButton).toHaveAttribute( + "href", + /action=delete&delete_id=\d+/, + ); +}); + +test("table action buttons - custom_actions", async ({ page }) => { + await page.goto(`${BASE}/documentation.sql?component=table`); + const tableSection = page.locator(".table-responsive", { + has: page.getByRole("cell", { name: "PharmaCo" }), + }); + + const historyButton = tableSection + .getByTitle("View Standard History") + .first(); + await expect(historyButton).toBeVisible(); + await expect(historyButton).toHaveAttribute( + "href", + /action=history&standard_id=\d+/, + ); +}); + +test("table action buttons - _sqlpage_actions", async ({ page }) => { + await page.goto(`${BASE}/documentation.sql?component=table`); + const tableSection = page.locator(".table-responsive", { + has: page.getByRole("cell", { name: "PharmaCo" }), + }); + + const pdfButtons = tableSection.getByTitle("View Presentation"); + await expect(pdfButtons.first()).toBeVisible(); + await expect(pdfButtons).toHaveCount(3); + + const firstPdfButton = pdfButtons.first(); + await expect(firstPdfButton).toHaveAttribute( + "href", + "https://sql-page.com/pgconf/2024-sqlpage-badass.pdf", + ); + + const setInUseButton = tableSection.getByTitle("Set In Use"); + await expect(setInUseButton).toBeVisible(); + await expect(setInUseButton).toHaveAttribute( + "href", + /action=set_in_use&standard_id=32/, + ); + + const retireButton = tableSection.getByTitle("Retire Standard"); + await expect(retireButton).toBeVisible(); + await expect(retireButton).toHaveAttribute( + "href", + /action=retire&standard_id=33/, + ); +}); + +test("table action buttons - disabled action", async ({ page }) => { + await page.goto(`${BASE}/documentation.sql?component=table`); + const tableSection = page.locator(".table-responsive", { + has: page.getByRole("cell", { name: "PharmaCo" }), + }); + + const viewPresentationButtons = tableSection.getByTitle("View Presentation"); + await expect(viewPresentationButtons).toHaveCount(3); + + const actionColumnButtons = tableSection.locator( + "td._col_Action a[data-action='Action']", + ); + await expect(actionColumnButtons).toHaveCount(3); + + const emptyActionButton = actionColumnButtons.last(); + await expect(emptyActionButton).toHaveAttribute("href", "null"); + await expect(emptyActionButton).toHaveAttribute("title", "Action"); +}); diff --git a/tests/end-to-end/package-lock.json b/tests/end-to-end/package-lock.json index d424cb49..875db3cc 100644 --- a/tests/end-to-end/package-lock.json +++ b/tests/end-to-end/package-lock.json @@ -10,17 +10,17 @@ "license": "ISC", "devDependencies": { "@playwright/test": "^1.45.3", - "@types/node": "^22.1.0" + "@types/node": "^24.9.1" } }, "node_modules/@playwright/test": { - "version": "1.53.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.53.1.tgz", - "integrity": "sha512-Z4c23LHV0muZ8hfv4jw6HngPJkbbtZxTkxPNIg7cJcTc9C28N/p2q7g3JZS2SiKBBHJ3uM1dgDye66bB7LEk5w==", + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz", + "integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.53.1" + "playwright": "1.56.1" }, "bin": { "playwright": "cli.js" @@ -30,13 +30,13 @@ } }, "node_modules/@types/node": { - "version": "22.15.33", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.33.tgz", - "integrity": "sha512-wzoocdnnpSxZ+6CjW4ADCK1jVmd1S/J3ArNWfn8FDDQtRm8dkDg7TA+mvek2wNrfCgwuZxqEOiB9B1XCJ6+dbw==", + "version": "24.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz", + "integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.21.0" + "undici-types": "~7.16.0" } }, "node_modules/fsevents": { @@ -55,13 +55,13 @@ } }, "node_modules/playwright": { - "version": "1.53.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.53.1.tgz", - "integrity": "sha512-LJ13YLr/ocweuwxyGf1XNFWIU4M2zUSo149Qbp+A4cpwDjsxRPj7k6H25LBrEHiEwxvRbD8HdwvQmRMSvquhYw==", + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz", + "integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.53.1" + "playwright-core": "1.56.1" }, "bin": { "playwright": "cli.js" @@ -74,9 +74,9 @@ } }, "node_modules/playwright-core": { - "version": "1.53.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.53.1.tgz", - "integrity": "sha512-Z46Oq7tLAyT0lGoFx4DOuB1IA9D1TPj0QkYxpPVUnGDqHHvDpCftu1J2hM2PiWsNMoZh8+LQaarAWcDfPBc6zg==", + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz", + "integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -87,9 +87,9 @@ } }, "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "dev": true, "license": "MIT" } diff --git a/tests/end-to-end/package.json b/tests/end-to-end/package.json index 26647081..caa9a84c 100644 --- a/tests/end-to-end/package.json +++ b/tests/end-to-end/package.json @@ -12,6 +12,6 @@ "license": "ISC", "devDependencies": { "@playwright/test": "^1.45.3", - "@types/node": "^22.1.0" + "@types/node": "^24.9.1" } }