Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"]
Expand Down
125 changes: 123 additions & 2 deletions examples/official-site/sqlpage/migrations/01_documentation.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
45 changes: 45 additions & 0 deletions sqlpage/templates/table.handlebars
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
{{#if (or search initial_search_value)}}
<div class="p-3">
<input
id="{{component_index}}-search"
type="search"
class="form-control form-control-rounded fs-6 search"
placeholder="{{default search_placeholder 'Search…'}}"
Expand Down Expand Up @@ -52,6 +53,18 @@
</th>
{{/if}}
{{/each}}
{{#if ../edit_url}}<th class="_col_edit text-center">Edit</th>{{/if}}
{{#if ../delete_url}}<th class="_col_delete text-center">Delete</th>{{/if}}
{{#if ../custom_actions}}
{{#each ../custom_actions}}
<th class="_col_custom text-center">{{this.name}}</th>
{{/each}}
{{/if}}
{{#if _sqlpage_actions}}
{{#each _sqlpage_actions}}
<th class="_col_action text-center">{{this.name}}</th>
{{/each}}
{{/if}}
</tr>
</thead>
<tbody class="table-tbody list">{{#delay}}</tbody>{{/delay}}
Expand All @@ -78,6 +91,38 @@
</td>
{{/if~}}
{{~/each~}}
{{#if ../edit_url}}
<td class="align-middle _col_edit">
<a href="{{replace ../edit_url '{id}' _sqlpage_id}}" class="align-middle link-secondary _col_edit" data-action="edit" title="Edit">
{{~icon_img 'edit'~}}
</a>
</td>
{{/if}}
{{#if ../delete_url}}
<td class="align-middle _col_delete text-center">
<a href="{{replace ../delete_url '{id}' _sqlpage_id}}" class="align-middle link-secondary _col_delete" data-action="delete" title="Delete">
{{~icon_img 'trash'~}}
</a>
</td>
{{/if}}
{{#if ../custom_actions}}
{{#each ../custom_actions}}
<td class="align-middle _col_{{this.name}} text-center">
<a href="{{replace this.link '{id}' ../_sqlpage_id}}" class="align-middle link-secondary _col_{{this.name}}" data-action="{{this.name}}" title="{{default this.tooltip this.name}}">{{!Title property sets the tooltip text}}
{{~icon_img this.icon~}}
</a>
</td>
{{/each}}
{{/if}}
{{#if _sqlpage_actions}}
{{#each _sqlpage_actions}}
<td class="align-middle _col_{{this.name}} text-center">
<a href="{{replace this.link '{id}' ../_sqlpage_id}}" class="align-middle link-secondary _col_{{this.name}}" data-action="{{this.name}}" title="{{default this.tooltip this.name}}">
{{~icon_img this.icon~}}
</a>
</td>
{{/each}}
{{/if}}
</tr>
{{!~
After this <tr> has been rendered, if this was a footer, we need to reopen a new <tbody>
Expand Down
84 changes: 84 additions & 0 deletions tests/end-to-end/official-site.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
Loading