chore: import upstream snapshot with attribution
CI / compile_and_lint (push) Failing after 0s
CI / docker_build (linux/amd64, -linux-amd64-duckdb, duckdb) (push) Failing after 0s
CI / docker_build (linux/arm64, -linux-arm64, minimal) (push) Failing after 2s
CI / docker_build (linux/arm64, -linux-arm64-duckdb, duckdb) (push) Failing after 1s
CI / docker_build (linux/amd64, minimal) (push) Failing after 1s
CI / test (, sqlite, sqlite::memory:) (push) Has been skipped
CI / test (mssql, mssql, mssql://root:Password123!@127.0.0.1/sqlpage) (push) Has been skipped
CI / test (mysql, mysql, mysql://root:Password123!@127.0.0.1/sqlpage) (push) Has been skipped
CI / test (oracle, oracle, Driver=Oracle 21 ODBC driver;Dbq=//127.0.0.1:1521/FREEPDB1;Uid=root;Pwd=Password123!) (push) Has been skipped
CI / test (postgres, odbc, Driver=PostgreSQL Unicode;Server=127.0.0.1;Port=5432;Database=sqlpage;UID=root;PWD=Password123!, true) (push) Has been skipped
CI / test (postgres, postgres, postgres://root:Password123!@127.0.0.1/sqlpage) (push) Has been skipped
CI / playwright (push) Has been skipped
CI / docker_build (linux/arm/v7, -linux-arm-v7, minimal) (push) Failing after 0s
CI / hurl_examples (push) Failing after 8s
deploy website / deploy_official_site (push) Failing after 1s
CI / hurl (${{ matrix.example }}) (push) Has been skipped
CI / docker_push (duckdb) (push) Has been cancelled
CI / docker_push (minimal) (push) Has been cancelled
CI / windows_test (push) Has been cancelled
@@ -0,0 +1,292 @@
|
||||
This demo/template defines a basic CRUD application with authentication designed for use with SQLite. The primary goal of this demo is to explore some of the SQLPage features and figure out how to implement various aspects of a data-centric application. The template contains both public pages ([**SQLite Introspection**](www/intro.sql)) and pages requiring authentication. A user management GUI is not available (database migrations, included in the project, create a default login admin/admin).
|
||||
|
||||
The website root is set to _./www_, and the database is in the _./db/_ directory. The database contains one data table, "currencies", and its content is listed on a login-protected [**page**](www/currencies_list.sql). After successful authentication, you can see the list of records and access the form for adding/editing/deleting records.
|
||||
|
||||
## Authentication process
|
||||
|
||||
Three files (login.sql, logout.sql, and create_session.sql) implement authentication mostly following the code provided in other examples. The login.sql defines the actual login form, and the two other files do not have any associated GUI but perform appropriate processing and redirect to designated targets. Given a protected page, the general authentication flow is as follows.
|
||||
|
||||
1. The user attempts to open a protected page (e.g., currencies_list.sql)
|
||||
2. Session checking code snippet at the top of the protected page checks if a valid session token (cookie) is set. In this example, the SET statement sets a local variable, `$_username`, for later use:
|
||||
```sql
|
||||
-- Checks if a valid session token cookie is available
|
||||
set _username = (
|
||||
SELECT username
|
||||
FROM sessions
|
||||
WHERE sqlpage.cookie('session_token') = id
|
||||
AND created_at > datetime('now', '-1 day')
|
||||
);
|
||||
```
|
||||
3. Redirect to login page (login.sql) if no session is available (`$_username IS NULL`) and the starting page requires authentication (by setting `set _session_required = 1;` before executing the session checking code; see, e.g., the top of currencies_item_form.sql and currencies_list.sql):
|
||||
```sql
|
||||
SELECT
|
||||
'redirect' AS component,
|
||||
sqlpage.link('/login.sql', json_object('path', $_curpath)) AS link
|
||||
WHERE $_username IS NULL AND $_session_required;
|
||||
```
|
||||
4. The login page renders the login form, accepts the user credentials, and redirects to create_session.sql, passing the login credentials as POST variables.
|
||||
5. create_session.sql checks credentials. If this check fails, it redirects back to the login form. If the check succeeds, it generates a session token and performs the final redirect.
|
||||
|
||||
## Header module
|
||||
|
||||
### Controlling execution of parts in a loaded script
|
||||
|
||||
Because the same code is used for session token check for all protected pages, it makes sense to place it in a separate module (header_shell_session.sql) and execute it via run_sql() at the top of protected files:
|
||||
|
||||
```sql
|
||||
set _curpath = sqlpage.path();
|
||||
set _session_required = 1;
|
||||
|
||||
SELECT
|
||||
'dynamic' AS component,
|
||||
sqlpage.run_sql('header_shell_session.sql') AS properties;
|
||||
```
|
||||
|
||||
The second line above sets the local variable $\_session_required, which indicates whether authentication is required for a particular page. This variable, like the GET/POST variables, is then accessible to the loaded "header" module header_shell_session.sql. This way, if other common code is placed in the header module, it can be executed by a non-protected page while skipping the authentication part (by setting $\_session_required = 0, which prevents redirect to the login form even if no valid session token is available).
|
||||
|
||||
Another "filter" variable (`$_shell_enabled`) controls the execution of another section in the header module, as discussed below.
|
||||
|
||||
### Tracking the calling page
|
||||
|
||||
The first line above sets another useful variable $\_curpath, which makes it possible to redirect back to the starting page after the authentication process is completed, rather than redirecting to the front page. The loaded header module has access to this variable as well, and if a login redirect is required, this variable is passed alone as a GET URL parameter (as a part of the "link" property):
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
'redirect' AS component,
|
||||
'/login.sql?path=' || $_curpath AS link
|
||||
WHERE $_username IS NULL AND $_session_required;
|
||||
```
|
||||
|
||||
The login form passes it further in a similar fashion to the create_session.sql script (as part of the "action" property):
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
'form' AS component,
|
||||
'Login' AS title,
|
||||
'create_session.sql' || ifnull('?path=' || $path, '') AS action;
|
||||
```
|
||||
If authentication fails, create_session.sql redirects back to login.sql and makes sure to pass the $path value alone (as a part of the "link" property):
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
'authentication' AS component,
|
||||
'login.sql?' || ifnull('path=' || $path, '') || '&error=1' AS link,
|
||||
:password AS password,
|
||||
(SELECT password_hash
|
||||
FROM "accounts"
|
||||
WHERE username = :username) AS password_hash;
|
||||
```
|
||||
|
||||
If authentication succeeds, create_session.sql redirects back to starting page using the $path value as the final redirect target:
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
'redirect' AS component,
|
||||
ifnull($path, '/') AS link;
|
||||
```
|
||||
|
||||
### Adding User/Login/Logout buttons to the page menu
|
||||
|
||||
It is customary to show the "User Profile" button in the top right corner when the user is authenticated. Also, it is customary to show the "Logout" button next to the "User Profile" button or the "Login" button when no active session is available. The associated code is common to all pages, and it makes sense to place it in the same header module.
|
||||
|
||||
The "shell" component is responsible for constructing the top menu, but the standard component does not support menu buttons. The simplest solution to this "limitation" is to modify the standard shell.handlebars template found in the "sqlpage/templates" directory of the SQLPage source code repository and place it inside the project "sqlpage/templates" directory.
|
||||
|
||||
To extend the "shell" component with button items in the menu, I have added a hybrid section of code mostly constructed from template code defining menu items and the "button" component. In the present implementation, menu buttons are defined as a JSON array value to the "menu_buttons" property. Each array member is a JSON object defining a single button and may include "shape", "color", "size", "outline", "link", "tooltip", and "title" properties (see description of these properties in the official "button" component docs.
|
||||
|
||||
Note how the `$_curpath` variable, which is set in core page modules (such as currencies_list.sql) is used to define links for the Login/Logout buttons. These links are irrelevant for protected pages, but for non-protected pages, such as intro.sql, these links make sure that the user remains on the same page after he/she presses on Logout/Login buttons (and completes authentication in the latter case).
|
||||
|
||||
The `$_username` variable set during the authentication process is then used to decide which buttons (Login or User/Logout) should be shown.
|
||||
|
||||
The `$_shell_enabled` variable controls the execution of the custom shell component. This feature is necessary because the header module is also loaded by the currencies_item_dml.sql module, which should only be accessible to authenticated users. However, the currencies_item_dml.sql module is a no-GUI module, which performs database operations and uses redirects after the requested operations are completed. At the same time, if the loaded header module executes the custom shell component, generating GUI buttons, the redirection mechanism in currencies_item_dml.sql will fail.
|
||||
|
||||
### Required variable guards
|
||||
|
||||
The header modules expects that the calling module sets several variables. The SET statement makes it possible to check if the variables are set appropriately in one place at the beginning of the module, rather then placing guards every time theses variables are used. Hence, the top section of the header file includes
|
||||
|
||||
```sql
|
||||
set _curpath = ifnull($_curpath, '/');
|
||||
set _session_required = ifnull($_session_required, 1);
|
||||
set _shell_enabled = ifnull($_shell_enabled, 1);
|
||||
```
|
||||
In this case, if any required variable is not set, a suitable default value is defined, so that the following code would not have to check for NULL values. Alternatively, a redirect to an error page may be used, to inform the programmer about the potential issue.
|
||||
|
||||
## Footer module - debug information
|
||||
|
||||
POST/GET/SET variables may provide helpful information for debugging purposes. In earlier [post](https://reddit.com/r/SQLpage/comments/1dh1siw/structuring_code_showing_debug_info/), I described the code I use to output variables in a convenient way. Briefly, I use `sqlpage.variables('GET')` and `sqlpage.variables('POST')` to get all variables, and I distinguish between the GET variables and local SET variables by prefixing SET variable names with an underscore. Initially, I copy-pasted the code snippets at the bottom of pages, but later I moved it to a separate file, footer_debug_post-get-set.sql, which I load via
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
'dynamic' AS component,
|
||||
sqlpage.run_sql('footer_debug_post-get-set.sql') AS properties
|
||||
WHERE $DEBUG OR $error IS NOT NULL;
|
||||
```
|
||||
## Structuring code modules
|
||||
|
||||
The "currencies" table is handled by three modules:
|
||||
|
||||
- "table" view - __currencies_list.sql__
|
||||
Displays the entire table using the powerful "table" component. One way to extend this module is, possibly, to hide certain less important columns, especially for wide tables.
|
||||
- "detail" view - __currencies_item_form.sql__
|
||||
This is the "detail" view. It shows all fields for a single record. In this case, it is an "editable" form, though the fields maybe made conditionally read-only. Another possible option for a read-only detail view is to use the "datagrid" component.
|
||||
- database DML processor - __currencies_item_dml.sql__
|
||||
This is a no-GUI module, which only processes database modification operations using data submitted to the currencies_item_form.sql form. Presently, all DML statements (INSERT/UPDATE/DELETE) are processed by this module. If necessary, this module maybe split into more specialized modules.
|
||||
|
||||
Let us briefly go over the code block in these modules.
|
||||
|
||||
### Debug information (bottom section)
|
||||
|
||||
All three module load the footer module discussed above that produces a conditional output of GET/POST/SET variables.
|
||||
|
||||
### Authentication (top section)
|
||||
|
||||
All three modules provide access to the database and are treated as protected: they are only accessible to authenticated users. Hence, they start with (mostly) the same code block:
|
||||
|
||||
```sql
|
||||
set _curpath = sqlpage.path();
|
||||
set _session_required = 1;
|
||||
|
||||
SELECT
|
||||
'dynamic' AS component,
|
||||
sqlpage.run_sql('header_shell_session.sql') AS properties;
|
||||
```
|
||||
|
||||
This code discussed above sets the current path variable (necessary for correct redirects), authentication flag before loading the header module that takes care of authentication and common settings, such as top menu buttons.
|
||||
|
||||
The "no-GUI" currencies_item_dml.sql module does not set `$_curpath`, since it cannot be a start/end point in a redirect chain, but it sets the `$_shell_enabled` flag to suppress top menu buttons generation, as discussed earlier.
|
||||
|
||||
### Common variables
|
||||
|
||||
The second section may generally be used to set additional common variables, such as the name of the "table" view inside the "detail" view and the other way around (to switch between the two).
|
||||
|
||||
The "detail" view also uses the "&path" GET URL parameter, if provided (e.g., by the "table" view). This way, if a record is modified/deleted starting from, e.g., the "table" view, the same view is set as the final redirect target after the DML operation is completed.
|
||||
|
||||
### Table view
|
||||
|
||||
The rest of the table view module is fairly basic. It defines two alerts for displaying confirmation and error messages, a "new record" button, and the table itself. The last "actions" column is added to the table, designated as markdown, and includes shortcuts to edit/delete the corresponding record.
|
||||
|
||||

|
||||
|
||||
### Detail view
|
||||
|
||||
The detail view module is more elaborate. If "&id" GET URL parameter is provided, the form shows the corresponding record. Otherwise, the ID field is rendered as a dropdown list populated from the database, but is set to NULL. The remaining fields are either blank or contain dummy values.
|
||||
|
||||
The first step (after the previously discussed common sections), therefore, is to filter invalid id values.
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
'redirect' AS component,
|
||||
$_curpath AS link
|
||||
WHERE $id = '' OR CAST($id AS INT) = 0;
|
||||
|
||||
set error_msg = sqlpage.url_encode('Bad {id = ' || $id || '} provided');
|
||||
SELECT
|
||||
'redirect' AS component,
|
||||
$_curpath || '?error=' || $error_msg AS link
|
||||
WHERE $id NOT IN (SELECT currencies.id FROM currencies);
|
||||
```
|
||||
|
||||
The blank string and zero are considered the equivalents of NULL, so redirect to itself is activated, removing the id parameter. If no id is provided or id is set to an integer value, the first check does not trigger. The second check above triggers when there is no record with provided id. This check resets id and displays an error message.
|
||||
|
||||
Another accepted GET URL parameter is $values, which may be set to a JSON representation of the record. This parameter is returned from the currencies_item_dml.sql script if the database operation fails. Then the detail view will display an error message, but the form will remain populated with the user-submitted data. If $values is set, it takes precedence. This check throws an error if $values is set, but does not represent a valid JSON.
|
||||
|
||||
```sql
|
||||
set _err_msg =
|
||||
sqlpage.url_encode('Values is set to bad JSON: __ ') || $values || ' __';
|
||||
|
||||
SELECT
|
||||
'redirect' AS component,
|
||||
$_curpath || '?error=' || $_err_msg AS link
|
||||
WHERE NOT json_valid($values);
|
||||
```
|
||||
The detail view maybe called with zero, one, or two (\$id/\$values) parameters. Invalid values are filtered out at this point, so the next step is to check provided parameters and determine the dataset that should go into the form.
|
||||
|
||||
```sql
|
||||
set _values = (
|
||||
WITH
|
||||
fields AS (
|
||||
SELECT id, name, to_rub
|
||||
FROM currencies
|
||||
WHERE id = CAST($id AS INT) AND $values IS NULL
|
||||
UNION ALL
|
||||
SELECT NULL, '@', 1
|
||||
WHERE $id IS NULL AND $values IS NULL
|
||||
UNION ALL
|
||||
SELECT
|
||||
$values ->> '$.id' AS id,
|
||||
$values ->> '$.name' AS name,
|
||||
$values ->> '$.to_rub' AS to_rub
|
||||
WHERE json_valid($values)
|
||||
)
|
||||
SELECT
|
||||
json_object(
|
||||
'id', CAST(fields.id AS INT),
|
||||
'name', fields.name,
|
||||
'to_rub', CAST(CAST(fields.to_rub AS TEXT) AS NUMERIC)
|
||||
)
|
||||
FROM fields
|
||||
);
|
||||
```
|
||||
|
||||
Each of the three united SELECTs in the "fields" CTE returns a single row and only one of them is selected for any given combination of \$id/\$values using the WHERE clauses. This query returns the "final" set of fields as a JSON object.
|
||||
|
||||

|
||||
|
||||
Now that the input parameters are validated and the "final" dataset is determined, it is the time to define the form GUI elements. First, I define the button to switch to the table view. Note that the same form is used to confirm record deletion, and when this happens, the "Browse" button is not shown.
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
'button' AS component,
|
||||
'pill' AS shape,
|
||||
'lg' AS size,
|
||||
'end' AS justify;
|
||||
SELECT
|
||||
'BROWSE' AS title,
|
||||
'browse_rec' AS id,
|
||||
'corner-down-left' AS icon,
|
||||
'corner-down-left' AS icon_after,
|
||||
'green' AS outline,
|
||||
$_table_list AS link,
|
||||
'Browse full table' AS tooltip
|
||||
WHERE NOT ifnull($action = 'DELETE', FALSE);
|
||||
```
|
||||
|
||||
The following section defines the main form with record fields. First the $\_valid_ids variable is constructed as the source for the drop-down id field. The code also adds the NULL value used for defining a new record. Note that, when this form is opened from the table view via the "New Record" button, the $action variable is set to "INSERT" and the id field is set to the empty array in the first assignment via the alternative UINION and to the single NULL in the second assignment. The two queries can also be combined relatively straightforwardly using CTEs.
|
||||
|
||||
```sql
|
||||
set _valid_ids = (
|
||||
SELECT json_group_array(
|
||||
json_object('label', CAST(id AS TEXT), 'value', id) ORDER BY id
|
||||
)
|
||||
FROM currencies
|
||||
WHERE ifnull($action, '') <> 'INSERT'
|
||||
UNION ALL
|
||||
SELECT '[]'
|
||||
WHERE $action = 'INSERT'
|
||||
);
|
||||
set _valid_ids = (
|
||||
json_insert($_valid_ids, '$[#]',
|
||||
json_object('label', 'NULL', 'value', json('null'))
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
The next part defines form fields via the "dynamic" component (for some reason I am having issues with POST variables when the form is defined directly via the "form" component. Note how the $values variable prepared in previous blocks is used to populate the form. Without the SET statement, everything would need to be incorporated in a single query (which is feasible thanks to CTEs, but would still be significantly more difficult to develop and maintain).
|
||||
|
||||
Also note that this single form definition actually combines two forms (the second being the record delete confirmation form). If the $action variable is set to "DELETE" (after the delete operation is initiated from either the table or detail view), buttons are adjusted appropriately and all fields are set to read-only. Whether this is a good design is a separate question. Perhaps, defining two separate forms is a better approach.
|
||||
|
||||

|
||||
|
||||
|
||||
After the main form fields goes the delete confirmation alert, displayed after the delete operation is completed.
|
||||
|
||||
The last big section defines the main form buttons, which are adjusted based on the type of operation (similarly to the form fields above).
|
||||
|
||||
The final section includes a general confirmation alert (used after INSERT/UPDATE operations) and an error alert.
|
||||
|
||||
### Coding style conventions
|
||||
|
||||
Consistent code style is important for code readability. Because SQLPage module maybe a mix of SQL code and sizeable text fragments, which may contain plain text, Markdown, JSON, HTML, etc., it might be difficult to follow a fixed set of rules. In fact, dynamically generated webpages regardless of specific technologies used tend to get messy. At the very least I strive to
|
||||
|
||||
- keep all SQL keywords always in the UPPER case,
|
||||
- have reasonably sensible code alignment (though some alignment approaches may not be generally advisable)
|
||||
- keep large static text pieces in separate appropriate dedicated files and load them via `sqlpage.read_file_as_text()` (e.g., the text of this file comes from Readme.md, where it can be properly edited by any Markdown editor and version-controlled; similarly, static JSON should go in \*.json files or in a dedicated database table with a designated JSON column).
|
||||
@@ -0,0 +1,10 @@
|
||||
services:
|
||||
web:
|
||||
image: lovasoa/sqlpage:main
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- ./www:/var/www
|
||||
- ./sqlpage:/etc/sqlpage
|
||||
environment:
|
||||
DATABASE_URL: sqlite:///tmp/components.db?mode=rwc
|
||||
@@ -0,0 +1,39 @@
|
||||
DROP TABLE IF EXISTS _sqlx_migrations;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS _sqlx_migrations (
|
||||
version INTEGER PRIMARY KEY,
|
||||
description TEXT COLLATE NOCASE NOT NULL,
|
||||
installed_on TEXT COLLATE NOCASE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
success INTEGER NOT NULL,
|
||||
checksum BLOB NOT NULL,
|
||||
execution_time INTEGER NOT NULL
|
||||
);
|
||||
|
||||
|
||||
-- The path field should be relative to the www root. Do not
|
||||
-- include absolute paths pointing to files outside the www root.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sqlpage_files (
|
||||
path TEXT COLLATE NOCASE NOT NULL UNIQUE
|
||||
GENERATED ALWAYS AS (
|
||||
iif(prefix IS NOT NULL AND length(prefix) > 0, prefix || '/', '') ||
|
||||
name
|
||||
),
|
||||
contents BLOB,
|
||||
last_modified TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||
prefix TEXT COLLATE NOCASE NOT NULL DEFAULT '',
|
||||
name TEXT COLLATE NOCASE NOT NULL,
|
||||
tag TEXT COLLATE NOCASE,
|
||||
src_url TEXT COLLATE NOCASE,
|
||||
PRIMARY KEY(prefix, name)
|
||||
);
|
||||
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS sqlpage_files_update
|
||||
AFTER UPDATE OF path, contents ON sqlpage_files
|
||||
WHEN old.last_modified = new.last_modified
|
||||
BEGIN
|
||||
UPDATE sqlpage_files
|
||||
SET last_modified = CURRENT_TIMESTAMP
|
||||
WHERE last_modified = new.last_modified;
|
||||
END;
|
||||
@@ -0,0 +1,17 @@
|
||||
CREATE TABLE IF NOT EXISTS "accounts" (
|
||||
"username" TEXT COLLATE NOCASE PRIMARY KEY,
|
||||
"password_hash" TEXT COLLATE BINARY NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "sessions" (
|
||||
"id" TEXT COLLATE NOCASE PRIMARY KEY,
|
||||
"username" TEXT COLLATE NOCASE NOT NULL
|
||||
REFERENCES "accounts"("username"),
|
||||
"created_at" TEXT COLLATE NOCASE NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
|
||||
-- Creates an initial user with the username `admin` and the password `admin` (hashed using sqlpage.hash_password('admin'))
|
||||
|
||||
INSERT OR IGNORE INTO "accounts"("username", "password_hash") VALUES
|
||||
('admin', '$argon2id$v=19$m=19456,t=2,p=1$4lu3hSvaqXK0dMCPZLOIPg$PUFJSB6L3J5eZ33z9WX7y0nOH6KawV2FdW0abMuPE7o');
|
||||
@@ -0,0 +1,10 @@
|
||||
CREATE TABLE IF NOT EXISTS "currencies" (
|
||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
"name" TEXT COLLATE NOCASE NOT NULL UNIQUE,
|
||||
"to_rub" REAL NOT NULL
|
||||
);
|
||||
|
||||
INSERT OR IGNORE INTO "currencies"("name", "to_rub") VALUES
|
||||
('RUR', 1),
|
||||
('USD', 90),
|
||||
('CNY', 12.34);
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"listen_on": "0.0.0.0:8080",
|
||||
"database_url": "sqlite://./db/Components.sqlite?mode=rwc",
|
||||
"allow_exec": false,
|
||||
"web_root": "./www"
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{language}}" style="font-size: {{default font_size 18}}px" {{#if class}}class="{{class}}"{{/if}}>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<title>{{default title "SQLPage"}}</title>
|
||||
{{#if favicon}}
|
||||
<link rel="icon" href="{{favicon}}">
|
||||
{{/if}}
|
||||
{{#if manifest}}
|
||||
<link rel="manifest" href="{{manifest}}">
|
||||
{{/if}}
|
||||
<link rel="stylesheet" href="{{static_path 'sqlpage.css'}}">
|
||||
{{#each (to_array css)}}
|
||||
{{#if this}}
|
||||
<link rel="stylesheet" href="{{this}}">
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
|
||||
{{#if font}}
|
||||
{{#if (startsWith font "/")}}
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: 'LocalFont';
|
||||
src: url('{{font}}') format('woff2');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
:root {
|
||||
--tblr-font-sans-serif: 'LocalFont', Arial, sans-serif;
|
||||
}
|
||||
</style>
|
||||
{{else}}
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family={{font}}&display=fallback">
|
||||
<style>:root { --tblr-font-sans-serif: '{{font}}', Arial, sans-serif;}</style>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
<script src="{{static_path 'sqlpage.js'}}" defer></script>
|
||||
{{#each (to_array javascript)}}
|
||||
{{#if this}}
|
||||
<script src="{{this}}" defer></script>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<meta name="description" content="{{description}}"/>
|
||||
{{#if norobot}}
|
||||
<meta name="robots" content="noindex,nofollow">
|
||||
{{/if}}
|
||||
|
||||
{{#if refresh}}
|
||||
<meta http-equiv="refresh" content="{{refresh}}">
|
||||
{{/if}}
|
||||
{{#if rss}}
|
||||
<link rel="alternate" type="application/rss+xml" title="{{title}}" href="{{rss}}">
|
||||
{{/if}}
|
||||
<meta name="generator" content="SQLPage"/>
|
||||
|
||||
{{#if social_image}}
|
||||
<meta property="og:image" content="{{social_image}}"/>
|
||||
{{/if}}
|
||||
</head>
|
||||
|
||||
<body class="layout-{{default layout 'boxed'}}" {{#if theme}}data-bs-theme="{{theme}}"{{/if}}>
|
||||
<div class="page">
|
||||
{{#if title}}
|
||||
<header id="sqlpage_header">
|
||||
<nav class="navbar navbar-expand-md navbar-light{{#if fixed_top_menu}} fixed-top{{/if}}">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand flex-grow-1 overflow-hidden" href="{{#if link}}{{link}}{{else}}/{{/if}}">
|
||||
{{#if image}}
|
||||
<img src="{{image}}" alt="{{title}}" width="32" height="32"
|
||||
class="navbar-brand-image">
|
||||
{{/if}}
|
||||
{{#if icon}}
|
||||
{{~icon_img icon~}}
|
||||
{{/if}}
|
||||
<h1 class="mb-0 w-0 fs-2">{{title}}</h1>
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse"
|
||||
data-bs-target="#navbar-menu" aria-controls="navbar-menu" aria-expanded="false"
|
||||
aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbar-menu">
|
||||
<ul class="navbar-nav ms-auto">
|
||||
{{#each (to_array menu_item)}}
|
||||
{{#if (or (eq (typeof this) 'object') (and (eq (typeof this) 'string') (starts_with this '{')))}}
|
||||
{{#with (parse_json this)}}
|
||||
{{#if (or (gt (len this.title) 0) (gt (len this.icon) 0) (gt (len this.image) 0))}}
|
||||
<li class="nav-item{{#if this.submenu}} dropdown{{/if}}{{#if this.button}} px-2{{/if}}">
|
||||
<a class="
|
||||
{{~#if this.button~}}
|
||||
btn text-wrap me-1
|
||||
{{~#if this.color}} btn-{{this.color}}{{/if}}
|
||||
{{~#if this.size}} btn-{{this.size}}{{/if}}
|
||||
{{~#if this.outline}} btn-outline-{{this.outline}}{{/if}}
|
||||
{{~#if this.shape}} btn-{{this.shape}}{{/if}}
|
||||
{{~#if this.narrow}} btn-icon{{/if}}
|
||||
{{~else~}}
|
||||
nav-link
|
||||
{{~/if}}
|
||||
{{~#if this.submenu}} dropdown-toggle{{/if}}"
|
||||
href="{{#if this.link}}{{this.link}}{{else}}#{{/if}}"
|
||||
{{#if this.submenu}}data-bs-toggle="dropdown" data-bs-auto-close="outside"{{/if}}
|
||||
role="button"
|
||||
>
|
||||
{{~#if this.image~}}
|
||||
<span {{~#if this.title}} class="me-1"{{/if}}>
|
||||
{{~#if (eq this.size 'sm')}}
|
||||
<img width=16 height=16 src="{{this.image}}">
|
||||
{{~else~}}
|
||||
<img width=24 height=24 src="{{this.image}}">
|
||||
{{~/if~}}
|
||||
</span>
|
||||
{{~/if~}}
|
||||
{{#if this.icon}}
|
||||
{{#if this.title}}<span class="me-1">{{/if}}
|
||||
{{~icon_img this.icon~}}
|
||||
{{#if this.title}}</span>{{/if}}
|
||||
{{/if}}
|
||||
{{this.title}}
|
||||
</a>
|
||||
{{#if this.submenu}}
|
||||
<div class="dropdown-menu dropdown-menu-end" data-bs-popper="static" style="min-width: inherit;">
|
||||
{{#each this.submenu}}
|
||||
{{#if (or (gt (len this.title) 0) (gt (len this.icon) 0) (gt (len this.image) 0))}}
|
||||
{{~#if this.button~}}<div class="mx-2" style="text-align: center;">{{/if}}
|
||||
<a class="
|
||||
{{~#if this.button~}}
|
||||
btn text-wrap my-1
|
||||
{{~#if this.color}} btn-{{this.color}}{{/if}}
|
||||
{{~#if this.size}} btn-{{this.size}}{{/if}}
|
||||
{{~#if this.outline}} btn-outline-{{this.outline}}{{/if}}
|
||||
{{~#if this.shape}} btn-{{this.shape}}{{/if}}
|
||||
{{~#if this.narrow}} btn-icon{{/if}}
|
||||
{{~else~}}
|
||||
dropdown-item
|
||||
{{~/if}}" style=" min-width: inherit;"
|
||||
{{~#if tooltip}} data-bs-toggle="tooltip" data-bs-placement="top" title="{{tooltip}}"{{/if}}
|
||||
href="{{this.link}}"
|
||||
>
|
||||
{{~#if this.image~}}
|
||||
<span {{~#if this.title}} class="me-1"{{/if}}>
|
||||
{{~#if (eq this.size 'sm')}}
|
||||
<img width=16 height=16 src="{{this.image}}">
|
||||
{{~else~}}
|
||||
<img width=24 height=24 src="{{this.image}}">
|
||||
{{~/if~}}
|
||||
</span>
|
||||
{{~/if~}}
|
||||
{{#if this.icon}}
|
||||
{{#if this.title}}<span class="me-1">{{/if}}
|
||||
{{~icon_img this.icon~}}
|
||||
{{#if this.title}}</span>{{/if}}
|
||||
{{/if}}
|
||||
{{this.title}}
|
||||
</a>
|
||||
{{~#if this.button~}}</div>{{/if}}
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</li>
|
||||
{{/if}}
|
||||
{{/with}}
|
||||
{{~else}}
|
||||
{{~#if (gt (len this) 0)~}}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-capitalize" href="{{this}}.sql">{{this}}</a>
|
||||
</li>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{#if search_target}}
|
||||
<form class="d-flex" role="search" action="{{search_target}}">
|
||||
<input class="form-control me-2" type="search" placeholder="Search" aria-label="Search" name="search" value="{{search_value}}">
|
||||
<button class="btn btn-outline-success" type="submit">Search</button>
|
||||
</form>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
{{/if}}
|
||||
<main class="page-wrapper container-xl pt-3 px-md-5 px-sm-3 {{#if fixed_top_menu}} mt-5{{/if}}" id="sqlpage_main_wrapper">
|
||||
{{~#each_row~}}{{~/each_row~}}
|
||||
</main>
|
||||
</div>
|
||||
<footer class="w-100 text-center fs-6 my-2 text-secondary" id="sqlpage_footer">
|
||||
{{#if footer}}
|
||||
{{{markdown footer}}}
|
||||
{{else}}
|
||||
<!-- You can change this footer using the 'footer' parameter of the 'shell' component -->
|
||||
Built with <a class="text-reset" href="https://sql-page.com" title="SQLPage v{{buildinfo 'CARGO_PKG_VERSION'}}">SQLPage</a>
|
||||
{{/if}}
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,135 @@
|
||||
# Public homepage is reachable without a session.
|
||||
GET http://localhost:8080
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
header "Content-Type" contains "text/html"
|
||||
body contains "Demo/Template CRUD with Authentication"
|
||||
body not contains "An error occurred"
|
||||
|
||||
# The currencies CRUD page is protected.
|
||||
GET http://localhost:8080/currencies_list.sql
|
||||
HTTP 302
|
||||
[Asserts]
|
||||
header "Location" == "/login.sql?path=/currencies_list.sql"
|
||||
|
||||
GET http://localhost:8080/login.sql?path=/currencies_list.sql
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body contains "Login"
|
||||
body contains "Username"
|
||||
body contains "Password"
|
||||
body not contains "An error occurred"
|
||||
|
||||
POST http://localhost:8080/create_session.sql?path=/currencies_list.sql
|
||||
[FormParams]
|
||||
username: admin
|
||||
password: wrong
|
||||
HTTP 302
|
||||
[Asserts]
|
||||
header "Location" contains "login.sql"
|
||||
header "Location" contains "error=1"
|
||||
|
||||
GET http://localhost:8080/currencies_list.sql
|
||||
HTTP 302
|
||||
[Asserts]
|
||||
header "Location" == "/login.sql?path=/currencies_list.sql"
|
||||
|
||||
# Demo credentials from the example migration: admin/admin.
|
||||
POST http://localhost:8080/create_session.sql?path=/currencies_list.sql
|
||||
[FormParams]
|
||||
username: admin
|
||||
password: admin
|
||||
HTTP 302
|
||||
[Asserts]
|
||||
header "Set-Cookie" contains "session_token="
|
||||
header "Location" == "/currencies_list.sql"
|
||||
|
||||
# Hurl's cookie store reuses the session_token set above.
|
||||
GET http://localhost:8080/currencies_list.sql
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body contains "Currencies"
|
||||
body contains "New Record"
|
||||
body contains "RUR"
|
||||
body contains "USD"
|
||||
body not contains "An error occurred"
|
||||
|
||||
GET http://localhost:8080/currencies_item_form.sql?id=1
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body contains "Currency"
|
||||
body contains "Exchange Rate to RUR"
|
||||
body contains "RUR"
|
||||
body not contains "An error occurred"
|
||||
|
||||
GET http://localhost:8080/currencies_item_form.sql?action=INSERT
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body contains "Currency"
|
||||
body contains "Exchange Rate to RUR"
|
||||
body contains "BROWSE"
|
||||
body not contains "An error occurred"
|
||||
|
||||
POST http://localhost:8080/currencies_item_dml.sql?path=currencies_list.sql&action=UPDATE
|
||||
[FormParams]
|
||||
id:
|
||||
name: HURLTEST
|
||||
to_rub: 7.89
|
||||
HTTP 302
|
||||
[Captures]
|
||||
currency_id: header "Location" regex "id=(\\d+)"
|
||||
[Asserts]
|
||||
header "Location" contains "currencies_list.sql"
|
||||
header "Location" contains "info=INSERT"
|
||||
|
||||
GET http://localhost:8080/currencies_list.sql
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body contains "HURLTEST"
|
||||
body contains "7.89"
|
||||
body htmlUnescape contains "currencies_item_form.sql?&path=/currencies_list.sql&id={{currency_id}}"
|
||||
body not contains "An error occurred"
|
||||
|
||||
POST http://localhost:8080/currencies_item_dml.sql?path=currencies_list.sql&action=UPDATE
|
||||
[FormParams]
|
||||
id: {{currency_id}}
|
||||
name: HURLEDITED
|
||||
to_rub: 8.91
|
||||
HTTP 302
|
||||
[Asserts]
|
||||
header "Location" contains "currencies_list.sql"
|
||||
header "Location" contains "id={{currency_id}}"
|
||||
header "Location" contains "info=UPDATE"
|
||||
|
||||
GET http://localhost:8080/currencies_item_form.sql?id={{currency_id}}
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body contains "HURLEDITED"
|
||||
body contains "8.91"
|
||||
body not contains "An error occurred"
|
||||
|
||||
POST http://localhost:8080/currencies_item_dml.sql?path=currencies_list.sql&action=DELETE
|
||||
[FormParams]
|
||||
id: {{currency_id}}
|
||||
name: HURLEDITED
|
||||
to_rub: 8.91
|
||||
HTTP 302
|
||||
[Asserts]
|
||||
header "Location" contains "currencies_list.sql"
|
||||
header "Location" contains "info=DELETE"
|
||||
|
||||
GET http://localhost:8080/currencies_list.sql
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body not contains "HURLEDITED"
|
||||
body not contains "An error occurred"
|
||||
|
||||
GET http://localhost:8080/logout.sql?path=currencies_list.sql
|
||||
HTTP 302
|
||||
[Asserts]
|
||||
header "Location" == "currencies_list.sql"
|
||||
|
||||
GET http://localhost:8080/currencies_list.sql
|
||||
HTTP 302
|
||||
[Asserts]
|
||||
header "Location" == "/login.sql?path=/currencies_list.sql"
|
||||
@@ -0,0 +1,292 @@
|
||||
This demo/template defines a basic CRUD application with authentication designed for use with SQLite. The primary goal of this demo is to explore some of the SQLPage features and figure out how to implement various aspects of a data-centric application. The template contains both public pages ([**SQLite Introspection**](intro.sql)) and pages requiring authentication. A user management GUI is not available (database migrations, included in the project, create a default login admin/admin).
|
||||
|
||||
The website root is set to _./www_, and the database is in the _./db/_ directory. The database contains one data table, "currencies", and its content is listed on a login-protected [**page**](currencies_list.sql). After successful authentication, you can see the list of records and access the form for adding/editing/deleting records.
|
||||
|
||||
## Authentication process
|
||||
|
||||
Three files (login.sql, logout.sql, and create_session.sql) implement authentication mostly following the code provided in other examples. The login.sql defines the actual login form, and the two other files do not have any associated GUI but perform appropriate processing and redirect to designated targets. Given a protected page, the general authentication flow is as follows.
|
||||
|
||||
1. The user attempts to open a protected page (e.g., currencies_list.sql)
|
||||
2. Session checking code snippet at the top of the protected page checks if a valid session token (cookie) is set. In this example, the SET statement sets a local variable, `$_username`, for later use:
|
||||
```sql
|
||||
-- Checks if a valid session token cookie is available
|
||||
set _username = (
|
||||
SELECT username
|
||||
FROM sessions
|
||||
WHERE sqlpage.cookie('session_token') = id
|
||||
AND created_at > datetime('now', '-1 day')
|
||||
);
|
||||
```
|
||||
3. Redirect to login page (login.sql) if no session is available (`$_username IS NULL`) and the starting page requires authentication (by setting `set _session_required = 1;` before executing the session checking code; see, e.g., the top of currencies_item_form.sql and currencies_list.sql):
|
||||
```sql
|
||||
SELECT
|
||||
'redirect' AS component,
|
||||
'/login.sql?path=' || $_curpath AS link
|
||||
WHERE $_username IS NULL AND $_session_required;
|
||||
```
|
||||
4. The login page renders the login form, accepts the user credentials, and redirects to create_session.sql, passing the login credentials as POST variables.
|
||||
5. create_session.sql checks credentials. If this check fails, it redirects back to the login form. If the check succeeds, it generates a session token and performs the final redirect.
|
||||
|
||||
## Header module
|
||||
|
||||
### Controlling execution of parts in a loaded script
|
||||
|
||||
Because the same code is used for session token check for all protected pages, it makes sense to place it in a separate module (header_shell_session.sql) and execute it via run_sql() at the top of protected files:
|
||||
|
||||
```sql
|
||||
set _curpath = sqlpage.path();
|
||||
set _session_required = 1;
|
||||
|
||||
SELECT
|
||||
'dynamic' AS component,
|
||||
sqlpage.run_sql('header_shell_session.sql') AS properties;
|
||||
```
|
||||
|
||||
The second line above sets the local variable $\_session_required, which indicates whether authentication is required for a particular page. This variable, like the GET/POST variables, is then accessible to the loaded "header" module header_shell_session.sql. This way, if other common code is placed in the header module, it can be executed by a non-protected page while skipping the authentication part (by setting $\_session_required = 0, which prevents redirect to the login form even if no valid session token is available).
|
||||
|
||||
Another "filter" variable (`$_shell_enabled`) controls the execution of another section in the header module, as discussed below.
|
||||
|
||||
### Tracking the calling page
|
||||
|
||||
The first line above sets another useful variable $\_curpath, which makes it possible to redirect back to the starting page after the authentication process is completed, rather than redirecting to the front page. The loaded header module has access to this variable as well, and if a login redirect is required, this variable is passed alone as a GET URL parameter (as a part of the "link" property):
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
'redirect' AS component,
|
||||
'/login.sql?path=' || $_curpath AS link
|
||||
WHERE $_username IS NULL AND $_session_required;
|
||||
```
|
||||
|
||||
The login form passes it further in a similar fashion to the create_session.sql script (as part of the "action" property):
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
'form' AS component,
|
||||
'Login' AS title,
|
||||
'create_session.sql' || ifnull('?path=' || $path, '') AS action;
|
||||
```
|
||||
If authentication fails, create_session.sql redirects back to login.sql and makes sure to pass the $path value alone (as a part of the "link" property):
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
'authentication' AS component,
|
||||
'login.sql?' || ifnull('path=' || $path, '') || '&error=1' AS link,
|
||||
:password AS password,
|
||||
(SELECT password_hash
|
||||
FROM "accounts"
|
||||
WHERE username = :username) AS password_hash;
|
||||
```
|
||||
|
||||
If authentication succeeds, create_session.sql redirects back to starting page using the $path value as the final redirect target:
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
'redirect' AS component,
|
||||
ifnull($path, '/') AS link;
|
||||
```
|
||||
|
||||
### Adding User/Login/Logout buttons to the page menu
|
||||
|
||||
It is customary to show the "User Profile" button in the top right corner when the user is authenticated. Also, it is customary to show the "Logout" button next to the "User Profile" button or the "Login" button when no active session is available. The associated code is common to all pages, and it makes sense to place it in the same header module.
|
||||
|
||||
The "shell" component is responsible for constructing the top menu, but the standard component does not support menu buttons. The simplest solution to this "limitation" is to modify the standard shell.handlebars template found in the "sqlpage/templates" directory of the SQLPage source code repository and place it inside the project "sqlpage/templates" directory.
|
||||
|
||||
To extend the "shell" component with button items in the menu, I have added a hybrid section of code mostly constructed from template code defining menu items and the "button" component. In the present implementation, menu buttons are defined as a JSON array value to the "menu_buttons" property. Each array member is a JSON object defining a single button and may include "shape", "color", "size", "outline", "link", "tooltip", and "title" properties (see description of these properties in the official "button" component docs.
|
||||
|
||||
Note how the `$_curpath` variable, which is set in core page modules (such as currencies_list.sql) is used to define links for the Login/Logout buttons. These links are irrelevant for protected pages, but for non-protected pages, such as intro.sql, these links make sure that the user remains on the same page after he/she presses on Logout/Login buttons (and completes authentication in the latter case).
|
||||
|
||||
The `$_username` variable set during the authentication process is then used to decide which buttons (Login or User/Logout) should be shown.
|
||||
|
||||
The `$_shell_enabled` variable controls the execution of the custom shell component. This feature is necessary because the header module is also loaded by the currencies_item_dml.sql module, which should only be accessible to authenticated users. However, the currencies_item_dml.sql module is a no-GUI module, which performs database operations and uses redirects after the requested operations are completed. At the same time, if the loaded header module executes the custom shell component, generating GUI buttons, the redirection mechanism in currencies_item_dml.sql will fail.
|
||||
|
||||
### Required variable guards
|
||||
|
||||
The header modules expects that the calling module sets several variables. The SET statement makes it possible to check if the variables are set appropriately in one place at the beginning of the module, rather then placing guards every time theses variables are used. Hence, the top section of the header file includes
|
||||
|
||||
```sql
|
||||
set _curpath = ifnull($_curpath, '/');
|
||||
set _session_required = ifnull($_session_required, 1);
|
||||
set _shell_enabled = ifnull($_shell_enabled, 1);
|
||||
```
|
||||
In this case, if any required variable is not set, a suitable default value is defined, so that the following code would not have to check for NULL values. Alternatively, a redirect to an error page may be used, to inform the programmer about the potential issue.
|
||||
|
||||
## Footer module - debug information
|
||||
|
||||
POST/GET/SET variables may provide helpful information for debugging purposes. In earlier [post](https://reddit.com/r/SQLpage/comments/1dh1siw/structuring_code_showing_debug_info/), I described the code I use to output variables in a convenient way. Briefly, I use `sqlpage.variables('GET')` and `sqlpage.variables('POST')` to get all variables, and I distinguish between the GET variables and local SET variables by prefixing SET variable names with an underscore. Initially, I copy-pasted the code snippets at the bottom of pages, but later I moved it to a separate file, footer_debug_post-get-set.sql, which I load via
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
'dynamic' AS component,
|
||||
sqlpage.run_sql('footer_debug_post-get-set.sql') AS properties
|
||||
WHERE $DEBUG OR $error IS NOT NULL;
|
||||
```
|
||||
## Structuring code modules
|
||||
|
||||
The "currencies" table is handled by three modules:
|
||||
|
||||
- "table" view - __currencies_list.sql__
|
||||
Displays the entire table using the powerful "table" component. One way to extend this module is, possibly, to hide certain less important columns, especially for wide tables.
|
||||
- "detail" view - __currencies_item_form.sql__
|
||||
This is the "detail" view. It shows all fields for a single record. In this case, it is an "editable" form, though the fields maybe made conditionally read-only. Another possible option for a read-only detail view is to use the "datagrid" component.
|
||||
- database DML processor - __currencies_item_dml.sql__
|
||||
This is a no-GUI module, which only processes database modification operations using data submitted to the currencies_item_form.sql form. Presently, all DML statements (INSERT/UPDATE/DELETE) are processed by this module. If necessary, this module maybe split into more specialized modules.
|
||||
|
||||
Let us briefly go over the code block in these modules.
|
||||
|
||||
### Debug information (bottom section)
|
||||
|
||||
All three module load the footer module discussed above that produces a conditional output of GET/POST/SET variables.
|
||||
|
||||
### Authentication (top section)
|
||||
|
||||
All three modules provide access to the database and are treated as protected: they are only accessible to authenticated users. Hence, they start with (mostly) the same code block:
|
||||
|
||||
```sql
|
||||
set _curpath = sqlpage.path();
|
||||
set _session_required = 1;
|
||||
|
||||
SELECT
|
||||
'dynamic' AS component,
|
||||
sqlpage.run_sql('header_shell_session.sql') AS properties;
|
||||
```
|
||||
|
||||
This code discussed above sets the current path variable (necessary for correct redirects), authentication flag before loading the header module that takes care of authentication and common settings, such as top menu buttons.
|
||||
|
||||
The "no-GUI" currencies_item_dml.sql module does not set `$_curpath`, since it cannot be a start/end point in a redirect chain, but it sets the `$_shell_enabled` flag to suppress top menu buttons generation, as discussed earlier.
|
||||
|
||||
### Common variables
|
||||
|
||||
The second section may generally be used to set additional common variables, such as the name of the "table" view inside the "detail" view and the other way around (to switch between the two).
|
||||
|
||||
The "detail" view also uses the "&path" GET URL parameter, if provided (e.g., by the "table" view). This way, if a record is modified/deleted starting from, e.g., the "table" view, the same view is set as the final redirect target after the DML operation is completed.
|
||||
|
||||
### Table view
|
||||
|
||||
The rest of the table view module is fairly basic. It defines two alerts for displaying confirmation and error messages, a "new record" button, and the table itself. The last "actions" column is added to the table, designated as markdown, and includes shortcuts to edit/delete the corresponding record.
|
||||
|
||||

|
||||
|
||||
### Detail view
|
||||
|
||||
The detail view module is more elaborate. If "&id" GET URL parameter is provided, the form shows the corresponding record. Otherwise, the ID field is rendered as a dropdown list populated from the database, but is set to NULL. The remaining fields are either blank or contain dummy values.
|
||||
|
||||
The first step (after the previously discussed common sections), therefore, is to filter invalid id values.
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
'redirect' AS component,
|
||||
$_curpath AS link
|
||||
WHERE $id = '' OR CAST($id AS INT) = 0;
|
||||
|
||||
set error_msg = sqlpage.url_encode('Bad {id = ' || $id || '} provided');
|
||||
SELECT
|
||||
'redirect' AS component,
|
||||
$_curpath || '?error=' || $error_msg AS link
|
||||
WHERE $id NOT IN (SELECT currencies.id FROM currencies);
|
||||
```
|
||||
|
||||
The blank string and zero are considered the equivalents of NULL, so redirect to itself is activated, removing the id parameter. If no id is provided or id is set to an integer value, the first check does not trigger. The second check above triggers when there is no record with provided id. This check resets id and displays an error message.
|
||||
|
||||
Another accepted GET URL parameter is $values, which may be set to a JSON representation of the record. This parameter is returned from the currencies_item_dml.sql script if the database operation fails. Then the detail view will display an error message, but the form will remain populated with the user-submitted data. If $values is set, it takes precedence. This check throws an error if $values is set, but does not represent a valid JSON.
|
||||
|
||||
```sql
|
||||
set _err_msg =
|
||||
sqlpage.url_encode('Values is set to bad JSON: __ ') || $values || ' __';
|
||||
|
||||
SELECT
|
||||
'redirect' AS component,
|
||||
$_curpath || '?error=' || $_err_msg AS link
|
||||
WHERE NOT json_valid($values);
|
||||
```
|
||||
The detail view maybe called with zero, one, or two (\$id/\$values) parameters. Invalid values are filtered out at this point, so the next step is to check provided parameters and determine the dataset that should go into the form.
|
||||
|
||||
```sql
|
||||
set _values = (
|
||||
WITH
|
||||
fields AS (
|
||||
SELECT id, name, to_rub
|
||||
FROM currencies
|
||||
WHERE id = CAST($id AS INT) AND $values IS NULL
|
||||
UNION ALL
|
||||
SELECT NULL, '@', 1
|
||||
WHERE $id IS NULL AND $values IS NULL
|
||||
UNION ALL
|
||||
SELECT
|
||||
$values ->> '$.id' AS id,
|
||||
$values ->> '$.name' AS name,
|
||||
$values ->> '$.to_rub' AS to_rub
|
||||
WHERE json_valid($values)
|
||||
)
|
||||
SELECT
|
||||
json_object(
|
||||
'id', CAST(fields.id AS INT),
|
||||
'name', fields.name,
|
||||
'to_rub', CAST(CAST(fields.to_rub AS TEXT) AS NUMERIC)
|
||||
)
|
||||
FROM fields
|
||||
);
|
||||
```
|
||||
|
||||
Each of the three united SELECTs in the "fields" CTE returns a single row and only one of them is selected for any given combination of \$id/\$values using the WHERE clauses. This query returns the "final" set of fields as a JSON object.
|
||||
|
||||

|
||||
|
||||
Now that the input parameters are validated and the "final" dataset is determined, it is the time to define the form GUI elements. First, I define the button to switch to the table view. Note that the same form is used to confirm record deletion, and when this happens, the "Browse" button is not shown.
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
'button' AS component,
|
||||
'pill' AS shape,
|
||||
'lg' AS size,
|
||||
'end' AS justify;
|
||||
SELECT
|
||||
'BROWSE' AS title,
|
||||
'browse_rec' AS id,
|
||||
'corner-down-left' AS icon,
|
||||
'corner-down-left' AS icon_after,
|
||||
'green' AS outline,
|
||||
$_table_list AS link,
|
||||
'Browse full table' AS tooltip
|
||||
WHERE NOT ifnull($action = 'DELETE', FALSE);
|
||||
```
|
||||
|
||||
The following section defines the main form with record fields. First the $\_valid_ids variable is constructed as the source for the drop-down id field. The code also adds the NULL value used for defining a new record. Note that, when this form is opened from the table view via the "New Record" button, the $action variable is set to "INSERT" and the id field is set to the empty array in the first assignment via the alternative UINION and to the single NULL in the second assignment. The two queries can also be combined relatively straightforwardly using CTEs.
|
||||
|
||||
```sql
|
||||
set _valid_ids = (
|
||||
SELECT json_group_array(
|
||||
json_object('label', CAST(id AS TEXT), 'value', id) ORDER BY id
|
||||
)
|
||||
FROM currencies
|
||||
WHERE ifnull($action, '') <> 'INSERT'
|
||||
UNION ALL
|
||||
SELECT '[]'
|
||||
WHERE $action = 'INSERT'
|
||||
);
|
||||
set _valid_ids = (
|
||||
json_insert($_valid_ids, '$[#]',
|
||||
json_object('label', 'NULL', 'value', json('null'))
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
The next part defines form fields via the "dynamic" component (for some reason I am having issues with POST variables when the form is defined directly via the "form" component. Note how the $values variable prepared in previous blocks is used to populate the form. Without the SET statement, everything would need to be incorporated in a single query (which is feasible thanks to CTEs, but would still be significantly more difficult to develop and maintain).
|
||||
|
||||
Also note that this single form definition actually combines two forms (the second being the record delete confirmation form). If the $action variable is set to "DELETE" (after the delete operation is initiated from either the table or detail view), buttons are adjusted appropriately and all fields are set to read-only. Whether this is a good design is a separate question. Perhaps, defining two separate forms is a better approach.
|
||||
|
||||

|
||||
|
||||
|
||||
After the main form fields goes the delete confirmation alert, displayed after the delete operation is completed.
|
||||
|
||||
The last big section defines the main form buttons, which are adjusted based on the type of operation (similarly to the form fields above).
|
||||
|
||||
The final section includes a general confirmation alert (used after INSERT/UPDATE operations) and an error alert.
|
||||
|
||||
### Coding style conventions
|
||||
|
||||
Consistent code style is important for code readability. Because SQLPage module maybe a mix of SQL code and sizeable text fragments, which may contain plain text, Markdown, JSON, HTML, etc., it might be difficult to follow a fixed set of rules. In fact, dynamically generated webpages regardless of specific technologies used tend to get messy. At the very least I strive to
|
||||
|
||||
- keep all SQL keywords always in the UPPER case,
|
||||
- have reasonably sensible code alignment (though some alignment approaches may not be generally advisable)
|
||||
- keep large static text pieces in separate appropriate dedicated files and load them via `sqlpage.read_file_as_text()` (e.g., the text of this file comes from Readme.md, where it can be properly edited by any Markdown editor and version-controlled; similarly, static JSON should go in \*.json files or in a dedicated database table with a designated JSON column).
|
||||
@@ -0,0 +1,27 @@
|
||||
-- Redirect to the login page if the password is not correct
|
||||
|
||||
SELECT
|
||||
'authentication' AS component,
|
||||
'login.sql?' || ifnull('path=' || sqlpage.url_encode($path), '') || '&error=1' AS link,
|
||||
:password AS password,
|
||||
(SELECT password_hash
|
||||
FROM accounts
|
||||
WHERE username = :username) AS password_hash;
|
||||
|
||||
-- The code after this point is only executed if the user has sent the correct password
|
||||
-- Generate a random session token and set via the "cookie" component in the RETURNING
|
||||
-- clause.
|
||||
|
||||
INSERT INTO sessions (id, username)
|
||||
VALUES (sqlpage.random_string(32), :username)
|
||||
RETURNING
|
||||
'cookie' AS component,
|
||||
'session_token' AS name,
|
||||
id AS value;
|
||||
|
||||
-- The user browser will now have a cookie named `session_token` that we can check later
|
||||
-- to see if the user is logged in.
|
||||
|
||||
SELECT
|
||||
'redirect' AS component,
|
||||
ifnull($path, '/') AS link;
|
||||
@@ -0,0 +1,92 @@
|
||||
.token.comment,
|
||||
.token.prolog,
|
||||
.token.doctype,
|
||||
.token.cdata {
|
||||
color: var(--tblr-gray-300);
|
||||
}
|
||||
|
||||
.token.punctuation {
|
||||
color: var(--tblr-gray-500);
|
||||
}
|
||||
|
||||
.namespace {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.token.property,
|
||||
.token.tag {
|
||||
color: #f92672;
|
||||
|
||||
/* We need to reset the 'tag' styles set by tabler */
|
||||
border: 0;
|
||||
display: inherit;
|
||||
height: inherit;
|
||||
border-radius: inherit;
|
||||
padding: 0;
|
||||
background: inherit;
|
||||
box-shadow: inherit;
|
||||
}
|
||||
|
||||
.token.number {
|
||||
color: #ea9999;
|
||||
}
|
||||
|
||||
.token.boolean {
|
||||
color: #ae81ff;
|
||||
}
|
||||
|
||||
.token.selector,
|
||||
.token.attr-name,
|
||||
.token.string {
|
||||
color: #97e1a3;
|
||||
}
|
||||
|
||||
.token.operator,
|
||||
.token.entity,
|
||||
.token.url,
|
||||
.language-css .token.string,
|
||||
.style .token.string {
|
||||
color: #f8f8f2;
|
||||
}
|
||||
|
||||
.token.atrule,
|
||||
.token.attr-value {
|
||||
color: #e6db74;
|
||||
}
|
||||
|
||||
.token.keyword {
|
||||
color: #95d1ff;
|
||||
}
|
||||
|
||||
.token.regex,
|
||||
.token.important {
|
||||
color: var(--tblr-yellow);
|
||||
}
|
||||
|
||||
.token.important {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.token.entity {
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
.token {
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
code::selection,
|
||||
code ::selection {
|
||||
background: var(--tblr-yellow);
|
||||
color: var(--tblr-gray-900);
|
||||
border-radius: 0.1em;
|
||||
}
|
||||
|
||||
code .token.keyword::selection,
|
||||
code .token.punctuation::selection {
|
||||
color: var(--tblr-gray-800);
|
||||
}
|
||||
|
||||
pre code {
|
||||
padding: 0;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
.menu_options_slim {
|
||||
min-width: inherit !important;
|
||||
}
|
||||
|
||||
.menu_language_slim {
|
||||
min-width: inherit !important;
|
||||
}
|
||||
|
||||
.menu_language {
|
||||
min-width: 200%;
|
||||
}
|
||||
|
||||
div.dropdown-menu- {
|
||||
min-width: inherit !important;
|
||||
}
|
||||
|
||||
a.dropdown-item- {
|
||||
min-width: inherit !important;
|
||||
}
|
||||
|
||||
.slim_item {
|
||||
min-width: inherit !important;
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
-- Procesess CREATE/UPDATE/DELETE operations.
|
||||
|
||||
-- =============================================================================
|
||||
-- =========================== Module Setting ==================================
|
||||
-- =========================== Login / Logout ==================================
|
||||
-- =============================================================================
|
||||
|
||||
-- $_curpath and $_session_required are required for header_shell_session.sql.
|
||||
|
||||
set _session_required = 1;
|
||||
set _shell_enabled = 0;
|
||||
|
||||
SELECT
|
||||
'dynamic' AS component,
|
||||
sqlpage.run_sql('header_shell_session.sql') AS properties;
|
||||
|
||||
-- =============================================================================
|
||||
-- Redirect target must be passed as $path
|
||||
-- =============================================================================
|
||||
|
||||
set _err_msg = '&path URL GET parameter (redirect target) is not set!';
|
||||
|
||||
SELECT
|
||||
'alert' AS component,
|
||||
'red' AS color,
|
||||
'alert-triangle' AS icon,
|
||||
'Error' AS title,
|
||||
$_err_msg AS description,
|
||||
TRUE AS dismissible
|
||||
WHERE
|
||||
$path IS NULL AND $DEBUG IS NULL;
|
||||
|
||||
-- =============================================================================
|
||||
-- Check new values for validity:
|
||||
-- - UPDATE existing record:
|
||||
-- :id IS NOT NULL
|
||||
-- If :name is in the database, :id must match
|
||||
-- If attempting to change :name, operation may fail due to FK constraint
|
||||
-- - INSERT new record:
|
||||
-- :id IS NULL
|
||||
-- :name is not in the database
|
||||
-- =============================================================================
|
||||
|
||||
-- Pass new values back as JSON object in $values GET variable for form population.
|
||||
--
|
||||
-- For new records, the id (INTEGER PRIMARY KEY AUTOINCREMENT) should be set to NULL.
|
||||
-- The id field is set as hidden in the record edit form and passed as the :id POST
|
||||
-- variable. NULL, however, cannot be passed as such and is converted to blank string.
|
||||
-- Check :id for '' and set id (:id will return the same value).
|
||||
|
||||
set _id = iif(typeof(:id) = 'text' AND :id = '', NULL, :id);
|
||||
|
||||
set _values = json_object(
|
||||
'id', CAST($_id AS INT),
|
||||
'name', :name,
|
||||
'to_rub', CAST(:to_rub AS NUMERIC)
|
||||
);
|
||||
|
||||
set _op = iif($_id IS NULL, 'INSERT', 'UPDATE');
|
||||
set _err_msg = sqlpage.url_encode('New currency already in the database');
|
||||
|
||||
SELECT
|
||||
'redirect' AS component,
|
||||
$path || '?' ||
|
||||
'&op=' || $_op ||
|
||||
'&values=' || $_values ||
|
||||
'&error=' || $_err_msg AS link
|
||||
FROM currencies
|
||||
WHERE currencies.name = :name
|
||||
AND ($_id IS NULL OR currencies.id <> $_id);
|
||||
|
||||
-- =============================================================================
|
||||
-- UPSERT: If everything is OK and "UPDATE" is indicated, update the database
|
||||
-- =============================================================================
|
||||
|
||||
INSERT INTO currencies(id, name, to_rub)
|
||||
SELECT CAST($_id AS INT), :name, CAST(:to_rub AS NUMERIC)
|
||||
WHERE $action = 'UPDATE'
|
||||
ON CONFLICT(id) DO
|
||||
UPDATE SET name = excluded.name, to_rub = excluded.to_rub
|
||||
RETURNING
|
||||
'redirect' AS component,
|
||||
$path || '?' ||
|
||||
'&id=' || id ||
|
||||
'&info=' || $_op || ' completed successfully' AS link;
|
||||
|
||||
-- =============================================================================
|
||||
-- DELETE
|
||||
-- =============================================================================
|
||||
|
||||
DELETE FROM currencies
|
||||
WHERE $action = 'DELETE' AND id = $_id
|
||||
RETURNING
|
||||
'redirect' AS component,
|
||||
$path || '?' ||
|
||||
'&info=DELETE completed successfully' AS link;
|
||||
|
||||
-- =============================================================================
|
||||
-- DEBUG
|
||||
-- =============================================================================
|
||||
|
||||
SELECT
|
||||
'dynamic' AS component,
|
||||
sqlpage.run_sql('footer_debug_post-get-set.sql') AS properties;
|
||||
@@ -0,0 +1,309 @@
|
||||
-- Reads an item from the database if valid id is provided and
|
||||
-- populates the form. Otherwise, an empty form is presented.
|
||||
|
||||
-- =============================================================================
|
||||
-- =========================== Module Setting ==================================
|
||||
-- =========================== Login / Logout ==================================
|
||||
-- =============================================================================
|
||||
|
||||
-- $_curpath and $_session_required are required for header_shell_session.sql.
|
||||
|
||||
set _curpath = sqlpage.path();
|
||||
set _session_required = 1;
|
||||
|
||||
SELECT
|
||||
'dynamic' AS component,
|
||||
sqlpage.run_sql('header_shell_session.sql') AS properties;
|
||||
|
||||
-- =============================================================================
|
||||
-- =============================== Module vars =================================
|
||||
-- =============================================================================
|
||||
|
||||
set _getpath = '?path=' || ifnull($path, $_curpath);
|
||||
set _action_target = 'currencies_item_dml.sql' || $_getpath;
|
||||
set _table_list = 'currencies_list.sql';
|
||||
|
||||
-- =============================================================================
|
||||
-- ========================== Filter invalid $id ===============================
|
||||
-- =============================================================================
|
||||
--
|
||||
-- NULL is passed as 0 or '' via POST
|
||||
|
||||
SELECT
|
||||
'redirect' AS component,
|
||||
$_curpath AS link
|
||||
WHERE $id = '' OR CAST($id AS INT) = 0;
|
||||
|
||||
-- If $id is set, it must be a valid PKEY value.
|
||||
|
||||
set error_msg = sqlpage.url_encode('Bad {id = ' || $id || '} provided');
|
||||
|
||||
SELECT
|
||||
'redirect' AS component,
|
||||
$_curpath || '?error=' || $error_msg AS link
|
||||
|
||||
-- If $id IS NULL, NOT IN returns NULL and redirect is NOT selected.
|
||||
|
||||
WHERE $id NOT IN (SELECT currencies.id FROM currencies);
|
||||
|
||||
-- =============================================================================
|
||||
-- ======================== Filter invalid $values =============================
|
||||
-- =============================================================================
|
||||
--
|
||||
-- If $values is provided, it must contain a valid JSON.
|
||||
|
||||
set _err_msg =
|
||||
sqlpage.url_encode('Values is set to bad JSON: __ ') || $values || ' __';
|
||||
|
||||
SELECT
|
||||
'redirect' AS component,
|
||||
$_curpath || '?error=' || $_err_msg AS link
|
||||
|
||||
-- Covers $values IS NULL..
|
||||
|
||||
WHERE NOT json_valid($values);
|
||||
|
||||
-- =============================================================================
|
||||
-- ============================= Prepare data ==================================
|
||||
-- =============================================================================
|
||||
--
|
||||
-- Field values may be provided via the $values GET variable formatted as JSON
|
||||
-- object. If $values contains a valid JSON, use it to populate the form.
|
||||
-- Otherwise, if $id is set to a valid value, retrieve the record from the
|
||||
-- database and set values. If not, set values to all NULLs.
|
||||
|
||||
set _values = (
|
||||
WITH
|
||||
fields AS (
|
||||
-- If valid "id" is supplied as a GET variable, retrieve the record and
|
||||
-- populate the form.
|
||||
|
||||
SELECT id, name, to_rub
|
||||
FROM currencies
|
||||
WHERE id = CAST($id AS INT) AND $values IS NULL
|
||||
|
||||
-- If no "id" is supplied, the first part does not return any records,
|
||||
-- so add a dummy record.
|
||||
|
||||
UNION ALL
|
||||
SELECT NULL, '@', 1
|
||||
WHERE $id IS NULL AND $values IS NULL
|
||||
|
||||
-- If $value contains a valid JSON, use it to populate the form
|
||||
|
||||
UNION ALL
|
||||
SELECT
|
||||
$values ->> '$.id' AS id,
|
||||
$values ->> '$.name' AS name,
|
||||
$values ->> '$.to_rub' AS to_rub
|
||||
WHERE json_valid($values)
|
||||
)
|
||||
SELECT
|
||||
json_object(
|
||||
'id', CAST(fields.id AS INT),
|
||||
'name', fields.name,
|
||||
'to_rub', CAST(CAST(fields.to_rub AS TEXT) AS NUMERIC)
|
||||
)
|
||||
FROM fields
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- ========================= Browse Records Button =============================
|
||||
-- =============================================================================
|
||||
--
|
||||
SELECT
|
||||
'button' AS component,
|
||||
'square' AS shape,
|
||||
'sm' AS size,
|
||||
'end' AS justify;
|
||||
SELECT
|
||||
'BROWSE' AS title,
|
||||
'browse_rec' AS id,
|
||||
'corner-down-left' AS icon,
|
||||
'corner-down-left' AS icon_after,
|
||||
'green' AS outline,
|
||||
TRUE AS narrow,
|
||||
$_table_list AS link,
|
||||
'Browse full table' AS tooltip
|
||||
WHERE NOT ifnull($action = 'DELETE', FALSE);
|
||||
|
||||
-- =============================================================================
|
||||
-- ============================== Main Form ====================================
|
||||
-- =============================================================================
|
||||
--
|
||||
-- When confirming record deletion, set all fields to read-only and id type to
|
||||
-- number. No need to worry about the field values: all fields. including id are
|
||||
-- passed back as POST variables, and the code above sets the $_values variable
|
||||
-- for proper initialization of the reloaded form.
|
||||
|
||||
set _valid_ids = (
|
||||
SELECT json_group_array(
|
||||
json_object('label', CAST(id AS TEXT), 'value', id) ORDER BY id
|
||||
)
|
||||
FROM currencies
|
||||
WHERE ifnull($action, '') <> 'INSERT'
|
||||
UNION ALL
|
||||
SELECT '[]'
|
||||
WHERE $action = 'INSERT'
|
||||
);
|
||||
set _valid_ids = (
|
||||
json_insert($_valid_ids, '$[#]',
|
||||
json_object('label', 'NULL', 'value', json('null'))
|
||||
)
|
||||
);
|
||||
|
||||
SELECT
|
||||
'dynamic' AS component,
|
||||
json_array(
|
||||
json_object(
|
||||
'component', 'form',
|
||||
'title', 'Currency',
|
||||
'class', 'form_class',
|
||||
'id', 'detail_view',
|
||||
'validate', '',
|
||||
'action', $_action_target
|
||||
),
|
||||
json_object(
|
||||
'name', 'id',
|
||||
'label', 'ID',
|
||||
'type', iif(ifnull($action = 'DELETE', FALSE), 'number', 'select'),
|
||||
'name', 'id',
|
||||
'value', $_values ->> '$.id',
|
||||
'options', $_valid_ids,
|
||||
'width', 4,
|
||||
'readonly', ifnull($action = 'DELETE', FALSE),
|
||||
'required', json('false')
|
||||
),
|
||||
json_object(
|
||||
'name', 'name',
|
||||
'label', 'Currency',
|
||||
'value', $_values ->> '$.name',
|
||||
'placeholder', 'RUR',
|
||||
'width', 4,
|
||||
'readonly', ifnull($action = 'DELETE', FALSE),
|
||||
'required', json('true')
|
||||
),
|
||||
json_object(
|
||||
'type', 'number',
|
||||
'step', 0.01,
|
||||
'name', 'to_rub',
|
||||
'label', 'Exchange Rate to RUR',
|
||||
'value', $_values ->> '$.to_rub',
|
||||
'placeholder', 1,
|
||||
'width', 4,
|
||||
'readonly', ifnull($action = 'DELETE', FALSE),
|
||||
'required', json('true')
|
||||
)
|
||||
) AS properties
|
||||
;
|
||||
|
||||
-- =============================================================================
|
||||
-- ===================== Display DELETE confirmation ===========================
|
||||
-- =============================================================================
|
||||
|
||||
SELECT
|
||||
'alert' AS component,
|
||||
'warning' AS color,
|
||||
'alert-triangle' AS icon,
|
||||
TRUE AS important,
|
||||
'Warning' AS title,
|
||||
'Confirm record deletion' AS description
|
||||
WHERE $action = 'DELETE';
|
||||
|
||||
-- =============================================================================
|
||||
-- ========================== Main Form Buttons ================================
|
||||
-- =============================================================================
|
||||
--
|
||||
-- When confirming record deletion, disable the UPDATE button, replace
|
||||
-- the Reload button with the Cancel button, invert the DELETE button by
|
||||
-- removing the outline color, and ajust the POST target.
|
||||
|
||||
|
||||
SELECT
|
||||
'button' AS component,
|
||||
'pill' AS shape,
|
||||
'' AS size,
|
||||
'center' AS justify;
|
||||
|
||||
SELECT -- Default button
|
||||
'(Re)load' AS title,
|
||||
'read_rec' AS id,
|
||||
'database' AS icon,
|
||||
'database' AS icon_after,
|
||||
'green' AS outline,
|
||||
TRUE AS narrow,
|
||||
$_curpath AS link,
|
||||
'detail_view' AS form,
|
||||
TRUE AS space_after
|
||||
WHERE NOT ifnull($action = 'DELETE', FALSE);
|
||||
|
||||
SELECT -- Cancel DELETE button
|
||||
'Cancel' AS title,
|
||||
'read_rec' AS id,
|
||||
'alert-triangle' AS icon,
|
||||
'alert-triangle' AS icon_after,
|
||||
'primary' AS color,
|
||||
TRUE AS narrow,
|
||||
$_curpath AS link,
|
||||
'detail_view' AS form,
|
||||
TRUE AS space_after
|
||||
WHERE ifnull($action = 'DELETE', FALSE);
|
||||
|
||||
SELECT
|
||||
'Update' AS title, -- UPDATE / INSERT button
|
||||
'update_rec' AS id,
|
||||
'device-floppy' AS icon,
|
||||
'device-floppy' AS icon_after,
|
||||
'azure' AS outline,
|
||||
TRUE AS narrow,
|
||||
$_action_target || '&action=UPDATE' AS link,
|
||||
'detail_view' AS form,
|
||||
ifnull($action = 'DELETE', FALSE) AS disabled,
|
||||
TRUE AS space_after;
|
||||
|
||||
SELECT -- DELETE button
|
||||
'DELETE' AS title,
|
||||
'delete_rec' AS id,
|
||||
'alert-triangle' AS icon,
|
||||
'trash' AS icon_after,
|
||||
TRUE AS narrow,
|
||||
iif(ifnull($action = 'DELETE', FALSE), NULL, 'danger') AS outline,
|
||||
iif(ifnull($action = 'DELETE', FALSE),
|
||||
$_action_target, $_curpath || '?') || '&action=DELETE' AS link,
|
||||
'danger' AS color,
|
||||
'detail_view' AS form,
|
||||
FALSE AS space_after;
|
||||
|
||||
-- =============================================================================
|
||||
-- ======================== Display confirmation ===============================
|
||||
-- =============================================================================
|
||||
|
||||
SELECT
|
||||
'alert' AS component,
|
||||
'green' AS color,
|
||||
'check' AS icon,
|
||||
'Success' AS title,
|
||||
$info AS description,
|
||||
True AS dismissible
|
||||
WHERE $info IS NOT NULL;
|
||||
|
||||
-- =============================================================================
|
||||
-- ======================== Display error message ==============================
|
||||
-- =============================================================================
|
||||
|
||||
SELECT
|
||||
'alert' AS component,
|
||||
'red' AS color,
|
||||
'thumb-down' AS icon,
|
||||
$op || ' error' AS title,
|
||||
$error AS description,
|
||||
True AS dismissible
|
||||
WHERE $error IS NOT NULL;
|
||||
|
||||
-- =============================================================================
|
||||
-- DEBUG
|
||||
-- =============================================================================
|
||||
|
||||
SELECT
|
||||
'dynamic' AS component,
|
||||
sqlpage.run_sql('footer_debug_post-get-set.sql') AS properties;
|
||||
@@ -0,0 +1,114 @@
|
||||
-- =============================================================================
|
||||
-- =========================== Module Setting ==================================
|
||||
-- =========================== Login / Logout ==================================
|
||||
-- =============================================================================
|
||||
|
||||
-- $_curpath and $_session_required are required for header_shell_session.sql.
|
||||
|
||||
set _curpath = sqlpage.path();
|
||||
set _session_required = 1;
|
||||
|
||||
SELECT
|
||||
'dynamic' AS component,
|
||||
sqlpage.run_sql('header_shell_session.sql') AS properties;
|
||||
|
||||
-- =============================================================================
|
||||
-- =============================== Module vars =================================
|
||||
-- =============================================================================
|
||||
|
||||
set _getpath = '&path=' || $_curpath;
|
||||
set _item_form = 'currencies_item_form.sql';
|
||||
|
||||
-- =============================================================================
|
||||
-- ======================== Display confirmation ===============================
|
||||
-- =============================================================================
|
||||
|
||||
SELECT
|
||||
'alert' AS component,
|
||||
'green' AS color,
|
||||
'check' AS icon,
|
||||
'Success' AS title,
|
||||
$info AS description,
|
||||
TRUE AS dismissible
|
||||
WHERE $info IS NOT NULL;
|
||||
|
||||
-- =============================================================================
|
||||
-- ======================== Display error message ==============================
|
||||
-- =============================================================================
|
||||
|
||||
SELECT
|
||||
'alert' AS component,
|
||||
'red' AS color,
|
||||
'thumb-down' AS icon,
|
||||
$op || ' error' AS title,
|
||||
$error AS description,
|
||||
TRUE AS dismissible
|
||||
WHERE $error IS NOT NULL;
|
||||
|
||||
-- =============================================================================
|
||||
-- ========================== New record button ================================
|
||||
-- =============================================================================
|
||||
|
||||
SELECT
|
||||
'button' AS component,
|
||||
'pill' AS shape,
|
||||
'lg' AS size,
|
||||
'end' AS justify;
|
||||
SELECT
|
||||
'New Record' AS title,
|
||||
'insert_rec' AS id,
|
||||
'circle-plus' AS icon,
|
||||
'circle-plus' AS icon_after,
|
||||
'green' AS outline,
|
||||
$_item_form || '?' || $_getpath || '&action=INSERT' AS link
|
||||
;
|
||||
|
||||
-- =============================================================================
|
||||
-- ============================= Show the table ================================
|
||||
-- =============================================================================
|
||||
|
||||
SELECT
|
||||
'divider' AS component,
|
||||
'currencies' AS contents;
|
||||
|
||||
-- =============================================================================
|
||||
|
||||
SELECT
|
||||
'title' AS component,
|
||||
'Currencies' AS contents,
|
||||
4 AS level,
|
||||
TRUE AS center,
|
||||
'title_class' AS class,
|
||||
'title_id' AS id;
|
||||
|
||||
-- =============================================================================
|
||||
-- TABLE
|
||||
|
||||
SELECT
|
||||
'table' AS component,
|
||||
TRUE AS sort,
|
||||
TRUE AS search,
|
||||
TRUE AS border,
|
||||
TRUE AS hover,
|
||||
TRUE AS striped_columns,
|
||||
TRUE AS striped_rows,
|
||||
'table_class' AS class,
|
||||
'table_id' AS id,
|
||||
'actions' AS markdown;
|
||||
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
to_rub,
|
||||
'[](' || $_item_form || '?' || $_getpath || '&id=' || id || ') ' ||
|
||||
'[](' || $_item_form || '?' || $_getpath || '&id=' || id || '&action=DELETE)' AS actions
|
||||
FROM currencies
|
||||
ORDER BY id;
|
||||
|
||||
-- =============================================================================
|
||||
-- DEBUG
|
||||
-- =============================================================================
|
||||
|
||||
SELECT
|
||||
'dynamic' AS component,
|
||||
sqlpage.run_sql('footer_debug_post-get-set.sql') AS properties;
|
||||
@@ -0,0 +1,65 @@
|
||||
-- =============================================================================
|
||||
-- Displays GET/POST/SET variables in sorted tables for debug purposes. The
|
||||
-- variables are displayed if URL GET variable &DEBUG=1 is set OR &error is
|
||||
-- defined and not empty.
|
||||
--
|
||||
-- ## Usage
|
||||
--
|
||||
-- Execute this script at the bottom of a page script via
|
||||
--
|
||||
-- ```sql
|
||||
-- SELECT
|
||||
-- 'dynamic' AS component,
|
||||
-- sqlpage.run_sql('footer_debug_post-get-set.sql') AS properties;
|
||||
-- ```
|
||||
|
||||
-- GET VARIABLES --
|
||||
|
||||
SELECT
|
||||
'title' AS component,
|
||||
'GET Variables' AS contents,
|
||||
3 AS level,
|
||||
TRUE AS center
|
||||
WHERE $DEBUG OR $error IS NOT NULL;
|
||||
|
||||
SELECT
|
||||
'table' AS component,
|
||||
TRUE AS sort,
|
||||
TRUE AS search,
|
||||
TRUE AS border,
|
||||
TRUE AS hover,
|
||||
FALSE AS striped_columns,
|
||||
TRUE AS striped_rows,
|
||||
'value' AS markdown
|
||||
WHERE $DEBUG OR $error IS NOT NULL;
|
||||
|
||||
SELECT "key" AS variable, value
|
||||
FROM json_each(sqlpage.variables('GET'))
|
||||
WHERE $DEBUG OR $error IS NOT NULL
|
||||
ORDER BY substr("key", 1, 1) = '_', "key";
|
||||
|
||||
|
||||
-- POST VARIABLES --
|
||||
|
||||
SELECT
|
||||
'title' AS component,
|
||||
'POST Variables' AS contents,
|
||||
3 AS level,
|
||||
TRUE AS center
|
||||
WHERE $DEBUG OR $error IS NOT NULL;
|
||||
|
||||
SELECT
|
||||
'table' AS component,
|
||||
TRUE AS sort,
|
||||
TRUE AS search,
|
||||
TRUE AS border,
|
||||
TRUE AS hover,
|
||||
FALSE AS striped_columns,
|
||||
TRUE AS striped_rows,
|
||||
'value' AS markdown
|
||||
WHERE $DEBUG OR $error IS NOT NULL;
|
||||
|
||||
SELECT "key" AS variable, value
|
||||
FROM json_each(sqlpage.variables('POST'))
|
||||
WHERE $DEBUG OR $error IS NOT NULL
|
||||
ORDER BY "key";
|
||||
@@ -0,0 +1,175 @@
|
||||
-- =============================================================================
|
||||
-- Checks for the availablity of an active session and redirects to the login
|
||||
-- page, if necessary. Using a customized shell_ex component, shows "user" and
|
||||
-- login/logout buttons appropriately in the top-right corner.
|
||||
--
|
||||
-- Note, any additonal "shell" component settings must also be included here in
|
||||
-- the same component. It may require extending this script in a flexible way or
|
||||
-- creating a page-specific copy, which is less desirable, as it would cause code
|
||||
-- duplication.
|
||||
--
|
||||
-- ## Usage
|
||||
--
|
||||
-- Execute this script via
|
||||
--
|
||||
-- ```sql
|
||||
-- SELECT
|
||||
-- 'dynamic' AS component,
|
||||
-- sqlpage.run_sql('header_shell_session.sql') AS properties;
|
||||
-- ```
|
||||
--
|
||||
-- at the top of the page script, but AFTER setting the required variables
|
||||
--
|
||||
-- ```sql
|
||||
-- set _curpath = sqlpage.path();
|
||||
-- set _session_required = 1;
|
||||
-- set _shell_enabled = 1;
|
||||
-- ```
|
||||
--
|
||||
-- ## Reuired SET Variables
|
||||
--
|
||||
-- $_curpath
|
||||
-- - indicates redirect target passed to the login script
|
||||
-- $_session_required
|
||||
-- - 1 - require valid active session for non-public pages
|
||||
-- - 0 - ignore active session for public pages
|
||||
-- $_shell_enabled
|
||||
-- - 1 - execute the shell component in this script (default, if not defined)
|
||||
-- - 0 - do not execute the shell component in this script
|
||||
-- Define this value to use page-specific shell component.
|
||||
-- It id also necessary for no-GUI pages, which are called via a redirect and
|
||||
-- normally redirect back after the necessary processing is completed. Such
|
||||
-- pages may still require this script to check for active session, but they
|
||||
-- will not be able to redirect back if this script outputs GUI buttons.
|
||||
|
||||
-- =============================================================================
|
||||
-- ======================= Check required variables ============================
|
||||
-- =============================================================================
|
||||
--
|
||||
-- Set default values (for now) for required variables.
|
||||
-- Probably should instead show appropriate error messages and abort rendering.
|
||||
|
||||
set _curpath = ifnull($_curpath, '/');
|
||||
set _session_required = ifnull($_session_required, 1);
|
||||
set _shell_enabled = ifnull($_shell_enabled, 1);
|
||||
|
||||
-- =============================================================================
|
||||
-- ========================= Check active session ==============================
|
||||
-- =============================================================================
|
||||
--
|
||||
-- Check if session is available.
|
||||
-- Require the user to log in again after 1 day
|
||||
|
||||
set _username = (
|
||||
SELECT username
|
||||
FROM sessions
|
||||
WHERE sqlpage.cookie('session_token') = id
|
||||
AND created_at > datetime('now', '-1 day')
|
||||
);
|
||||
|
||||
-- Redirect to the login page if the user is not logged in.
|
||||
-- Unprotected pages must
|
||||
-- set _session_required = (SELECT FALSE);
|
||||
-- before running this script
|
||||
|
||||
SELECT
|
||||
'redirect' AS component,
|
||||
'/login.sql?path=' || $_curpath AS link
|
||||
WHERE $_username IS NULL AND $_session_required;
|
||||
|
||||
-- =============================================================================
|
||||
-- ==================== Add User and Login/Logout buttons ======================
|
||||
-- =============================================================================
|
||||
--
|
||||
|
||||
SELECT
|
||||
'dynamic' AS component,
|
||||
json_array(
|
||||
json_object(
|
||||
'component', 'shell',
|
||||
'title', 'CRUD with Authentication',
|
||||
'icon', 'database',
|
||||
'description', 'Description',
|
||||
'layout', 'fluid',
|
||||
|
||||
'css',
|
||||
json_array(
|
||||
'/css/prism-tabler-theme.css', -- Load for code highlighting
|
||||
'/css/style.css'
|
||||
),
|
||||
|
||||
'javascript',
|
||||
json_array(
|
||||
|
||||
-- Code highlighting scripts
|
||||
|
||||
'https://cdn.jsdelivr.net/npm/prismjs@1/components/prism-core.min.js',
|
||||
'https://cdn.jsdelivr.net/npm/prismjs@1/plugins/autoloader/prism-autoloader.min.js'
|
||||
),
|
||||
|
||||
'menu_item',
|
||||
iif($_username IS NOT NULL,
|
||||
json_array(
|
||||
json_object(
|
||||
'button', FALSE,
|
||||
'title', 'Settings',
|
||||
'icon', 'settings',
|
||||
'submenu', json_array(
|
||||
json_object(
|
||||
'button', TRUE,
|
||||
'title', '',
|
||||
'icon', 'user-circle',
|
||||
'shape', 'pill',
|
||||
'size', 'sm',
|
||||
'narrow', TRUE,
|
||||
'color', 'yellow',
|
||||
'outline', '',
|
||||
'link', '#',
|
||||
'tooltip', 'User profile - Not Implemented'
|
||||
),
|
||||
json_object(
|
||||
'button', TRUE,
|
||||
'title', '',
|
||||
'icon', 'logout',
|
||||
'shape', 'pill',
|
||||
'size', 'sm',
|
||||
'narrow', TRUE,
|
||||
'color', 'green',
|
||||
'outline', '',
|
||||
'link', '/logout.sql?path=' || $_curpath,
|
||||
'tooltip', 'Logout'
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
json_array(
|
||||
json_object(
|
||||
'button', TRUE,
|
||||
'title', '',
|
||||
'icon', 'user-scan',
|
||||
'shape', 'pill',
|
||||
'size', 'sm',
|
||||
'narrow', TRUE,
|
||||
'color', 'warning',
|
||||
'outline', '',
|
||||
'link', '#',
|
||||
'tooltip', 'Sign Up - Not Implemented'
|
||||
),
|
||||
json_object(
|
||||
'button', TRUE,
|
||||
'title', '',
|
||||
'icon', 'login',
|
||||
'shape', 'pill',
|
||||
'size', 'sm',
|
||||
'narrow', TRUE,
|
||||
'color', '',
|
||||
'outline', 'cyan',
|
||||
'link', '/login.sql?path=' || $_curpath,
|
||||
'tooltip', 'Login'
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
)
|
||||
) AS properties
|
||||
WHERE CAST($_shell_enabled AS INT) <> 0;
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 122.88 122.88" style="enable-background:new 0 0 122.88 122.88" xml:space="preserve"><style type="text/css"><![CDATA[
|
||||
.st0{fill-rule:evenodd;clip-rule:evenodd;fill:#A1E367;}
|
||||
.st1{fill-rule:evenodd;clip-rule:evenodd;fill:#2394E0;}
|
||||
]]></style><g><path class="st1" d="M61.44,0c33.93,0,61.44,27.51,61.44,61.44c0,33.93-27.51,61.44-61.44,61.44C27.51,122.88,0,95.37,0,61.44 C0,27.51,27.51,0,61.44,0L61.44,0L61.44,0z"/><path class="st0" d="M11.76,93.18c-3.62-5.49-6.33-11.64-7.92-18.23l10.82,5.22l0.06,3.23c0,1.19-2.02,3.71-2.67,4.67L11.76,93.18 L11.76,93.18L11.76,93.18z M78.31,6.5c20.62,6.85,36.09,25,39.07,47.1l-1.95-0.21c-0.35,1.5-0.67,1.53-0.67,3.33c0,1.59,2,2.65,2,6 c0,0.9-2.11,2.69-2.2,3.01l-3.13-3.67v4.67l-0.47-0.02l-0.84-8.62l-1.75,0.55l-2.08-6.41l-6.85,7.16l-0.08,5.24l-2.24,1.5 l-2.38-13.44l-1.42,1.04l-3.22-4.35l-4.81,0.14l-1.84-2.11l-1.89,0.52l-3.71-4.25l-0.72,0.49l2.3,5.88h2.67v-1.33l1.33,0 c0.96,2.66,2,1.08,2,2.67c0,5.55-6.85,9.63-11.34,10.67c0.24,1,0.15,2,1.33,2c2.51,0,1.26-0.44,4-0.67 c-0.13,5.67-6.5,12.44-9.22,16.65l1.22,8.69c0.32,1.89-3.92,3.88-5.36,6.01l0.69,3.33l-1.95,0.79c-0.34,3.42-3.66,7.21-7.39,7.21 h-4c0-4.68-3.34-11.37-3.34-14.68c0-2.81,1.33-3.19,1.33-6.67c0-3.22-3.33-7.83-3.33-8.67v-5.34H45.4c-0.4-1.49-0.15-2-2-2h-0.67 c-2.91,0-2.42,1.33-5.34,1.33h-2.67c-2.41,0-6.67-7.72-6.67-8.67v-8c0-3.45,3.16-7.21,5.34-8.67v-3.33l3-3.05l1.67-0.28 c3.58,0,3.15-2,5.34-2l6,0v4.67l6.6,2.82l0.62-2.85c2.99,0.7,3.77,2.03,7.45,2.03h1.33c2.53,0,2.67-3.36,2.67-6l-5.34,0.53 l-2.33-5.06l-2.31,0.61c0.42,1.81,0.64,1.06,0.64,2.59c0,0.9-0.74,1-1.34,1.33l-2.31-5.86l-4.97-3.55l-0.66,0.65l4.23,4.45 c-0.56,1.6-0.63,6.21-2.96,2.98l2.18-1.05l-5.44-5.7l-3.26,1.27l-3.22,3.08c-0.34,2.48-1.01,3.73-3.61,3.73 c-1.73,0-0.69-0.45-3.34-0.67v-6.67h6l-1.95-4.44l-0.72,0.44v-1.34l9.75-4.49c-0.18-1.4-0.41-0.65-0.41-2.18 c0-0.09,0.65-1.32,0.67-1.34l2.52,1.57l-0.6-2.87l-3.89,0.8l-0.72-3.49c3.08-1.62,9.87-7.34,12.03-7.34l2,0 c2.11,0,7.75,2.08,8.67,3.33l-5.35-0.54l3.97,3.27l0.38-1.4l2.96-0.81l0.04-1.85h1.34v2L78.31,6.5L78.31,6.5L78.31,6.5z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
@@ -0,0 +1,17 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="icon icon-tabler icons-tabler-outline icon-tabler-device-floppy"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M6 4h10l4 4v10a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2v-12a2 2 0 0 1 2 -2" />
|
||||
<path d="M12 14m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
|
||||
<path d="M14 4l0 4l-6 0l0 -4" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 510 B |
@@ -0,0 +1,21 @@
|
||||
<!--
|
||||
category: Design
|
||||
tags: [pencil, change, update]
|
||||
version: "1.0"
|
||||
unicode: "ea98"
|
||||
-->
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M7 7h-1a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-1" />
|
||||
<path d="M20.385 6.585a2.1 2.1 0 0 0 -2.97 -2.97l-8.415 8.385v3h3l8.385 -8.415z" />
|
||||
<path d="M16 5l3 3" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 481 B |
@@ -0,0 +1,17 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="icon icon-tabler icons-tabler-outline icon-tabler-table"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M3 5a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v14a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-14z" />
|
||||
<path d="M3 10h18" />
|
||||
<path d="M10 3v18" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 470 B |
@@ -0,0 +1,19 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="icon icon-tabler icons-tabler-outline icon-tabler-trash"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M4 7l16 0" />
|
||||
<path d="M10 11l0 6" />
|
||||
<path d="M14 11l0 6" />
|
||||
<path d="M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2 -2l1 -12" />
|
||||
<path d="M9 7v-3a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v3" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 522 B |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 26 KiB |
@@ -0,0 +1,34 @@
|
||||
SELECT
|
||||
'dynamic' AS component,
|
||||
json_array(
|
||||
json_object(
|
||||
'component', 'shell',
|
||||
'title', 'CRUD with Authentication',
|
||||
'icon', 'database',
|
||||
'description', 'Description',
|
||||
|
||||
'css',
|
||||
json_array(
|
||||
'/css/prism-tabler-theme.css'
|
||||
),
|
||||
|
||||
'javascript',
|
||||
json_array(
|
||||
'https://cdn.jsdelivr.net/npm/prismjs@1/components/prism-core.min.js',
|
||||
'https://cdn.jsdelivr.net/npm/prismjs@1/plugins/autoloader/prism-autoloader.min.js'
|
||||
)
|
||||
|
||||
)
|
||||
) AS properties;
|
||||
|
||||
-- =============================================================================
|
||||
|
||||
SELECT
|
||||
'text' AS component,
|
||||
TRUE AS center,
|
||||
2 AS level,
|
||||
'Demo/Template CRUD with Authentication' AS title,
|
||||
sqlpage.read_file_as_text('./README.md') AS contents_md;
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,354 @@
|
||||
-- =============================================================================
|
||||
-- =========================== Module Setting ==================================
|
||||
-- =========================== Login / Logout ==================================
|
||||
-- =============================================================================
|
||||
|
||||
-- $_curpath and $_session_required are required for header_shell_session.sql.
|
||||
|
||||
set _curpath = sqlpage.path();
|
||||
set _session_required = 0;
|
||||
|
||||
SELECT
|
||||
'dynamic' AS component,
|
||||
sqlpage.run_sql('header_shell_session.sql') AS properties;
|
||||
|
||||
-- =============================================================================
|
||||
|
||||
SELECT
|
||||
'text' AS component,
|
||||
TRUE AS center,
|
||||
2 AS level,
|
||||
'SQLite Introspection Information' AS title;
|
||||
|
||||
SELECT
|
||||
'divider' AS component,
|
||||
'Password Hash' AS contents;
|
||||
|
||||
-- =============================================================================
|
||||
-- Password Hash
|
||||
|
||||
SELECT
|
||||
'alert' AS component,
|
||||
'green' AS color,
|
||||
'edit' AS icon,
|
||||
'Password Hash: sqlpage.hash_password(''admin'')' AS title,
|
||||
sqlpage.hash_password('admin') AS description,
|
||||
TRUE AS dismissible;
|
||||
|
||||
-- =============================================================================
|
||||
-- ============================ Alert Template =================================
|
||||
-- =============================================================================
|
||||
|
||||
SELECT
|
||||
'divider' AS component,
|
||||
'Alert Template' AS contents;
|
||||
|
||||
-- =============================================================================
|
||||
-- ALERT
|
||||
|
||||
SELECT
|
||||
'alert' AS component,
|
||||
'green' AS color,
|
||||
'Alert Title' AS title,
|
||||
'Description' AS description,
|
||||
'**Bold MD**' AS description_md,
|
||||
'alert_class' AS class,
|
||||
'alert_id' AS id,
|
||||
TRUE AS dismissible,
|
||||
FALSE AS important,
|
||||
'user' AS icon,
|
||||
'https://google.com' AS link,
|
||||
'LINK TEXT' AS link_text;
|
||||
|
||||
-- =============================================================================
|
||||
-- ============================ IDs ============================================
|
||||
-- =============================================================================
|
||||
|
||||
SELECT
|
||||
'divider' AS component,
|
||||
'IDs' AS contents;
|
||||
|
||||
-- =============================================================================
|
||||
|
||||
SELECT
|
||||
'title' AS component,
|
||||
'IDs' AS contents,
|
||||
4 AS level,
|
||||
TRUE AS center,
|
||||
'title_class' AS class,
|
||||
'title_id' AS id;
|
||||
|
||||
-- =============================================================================
|
||||
-- TABLE
|
||||
|
||||
SELECT
|
||||
'table' AS component,
|
||||
TRUE AS sort,
|
||||
TRUE AS search,
|
||||
TRUE AS border,
|
||||
TRUE AS hover,
|
||||
TRUE AS striped_columns,
|
||||
TRUE AS striped_rows,
|
||||
'table_class' AS class,
|
||||
'table_id' AS id;
|
||||
|
||||
SELECT
|
||||
sqlite_version() AS "SQLite Version",
|
||||
(SELECT * FROM pragma_application_id()) AS app_id,
|
||||
(SELECT * FROM pragma_user_version()) AS user_version,
|
||||
(SELECT * FROM pragma_schema_version()) AS schema_version;
|
||||
|
||||
-- =============================================================================
|
||||
-- ============================ SQLite_Master ==================================
|
||||
-- =============================================================================
|
||||
|
||||
SELECT
|
||||
'divider' AS component,
|
||||
'SQLite_Master' AS contents;
|
||||
|
||||
-- =============================================================================
|
||||
|
||||
SELECT
|
||||
'title' AS component,
|
||||
'SQLite_Master' AS contents,
|
||||
4 AS level,
|
||||
TRUE AS center,
|
||||
'title_class' AS class,
|
||||
'title_id' AS id;
|
||||
|
||||
-- =============================================================================
|
||||
-- TABLE
|
||||
|
||||
SELECT
|
||||
'table' AS component,
|
||||
TRUE AS sort,
|
||||
TRUE AS search,
|
||||
TRUE AS border,
|
||||
TRUE AS hover,
|
||||
TRUE AS striped_columns,
|
||||
TRUE AS striped_rows,
|
||||
TRUE AS small,
|
||||
'table_class' AS class,
|
||||
TRUE AS overflow;
|
||||
|
||||
SELECT * FROM sqlite_master;
|
||||
|
||||
-- =============================================================================
|
||||
-- ============================ Function List ==================================
|
||||
-- =============================================================================
|
||||
|
||||
SELECT
|
||||
'divider' AS component,
|
||||
'Function List' AS contents;
|
||||
|
||||
-- =============================================================================
|
||||
|
||||
SELECT
|
||||
'Function List. Total ' || (SELECT count(DISTINCT name)
|
||||
FROM pragma_function_list())
|
||||
|| ' distinct' AS contents,
|
||||
'title' AS component,
|
||||
4 AS level,
|
||||
TRUE AS center,
|
||||
'title_class' AS class,
|
||||
'title_id' AS id;
|
||||
|
||||
-- =============================================================================
|
||||
-- TABLE
|
||||
|
||||
SELECT
|
||||
'table' AS component,
|
||||
TRUE AS sort,
|
||||
TRUE AS search,
|
||||
TRUE AS border,
|
||||
TRUE AS hover,
|
||||
TRUE AS striped_columns,
|
||||
TRUE AS striped_rows,
|
||||
TRUE AS small,
|
||||
'table_class' AS class,
|
||||
'table_id' AS id;
|
||||
|
||||
SELECT * FROM pragma_function_list() ORDER BY name, narg;
|
||||
|
||||
-- =============================================================================
|
||||
-- ============================ Collation List =================================
|
||||
-- =============================================================================
|
||||
|
||||
SELECT
|
||||
'divider' AS component,
|
||||
'Collation List' AS contents;
|
||||
|
||||
-- =============================================================================
|
||||
|
||||
SELECT
|
||||
'Collation List' AS contents,
|
||||
'title' AS component,
|
||||
4 AS level,
|
||||
TRUE AS center,
|
||||
'title_class' AS class,
|
||||
'title_id' AS id;
|
||||
|
||||
-- =============================================================================
|
||||
-- TABLE
|
||||
|
||||
SELECT
|
||||
'table' AS component,
|
||||
TRUE AS sort,
|
||||
TRUE AS search,
|
||||
TRUE AS border,
|
||||
TRUE AS hover,
|
||||
TRUE AS striped_columns,
|
||||
TRUE AS striped_rows,
|
||||
TRUE AS small,
|
||||
'table_class' AS class,
|
||||
'table_id' AS id;
|
||||
|
||||
SELECT * FROM pragma_collation_list() ORDER BY rowid;
|
||||
|
||||
-- =============================================================================
|
||||
-- ============================ Pragma List ====================================
|
||||
-- =============================================================================
|
||||
|
||||
SELECT
|
||||
'divider' AS component,
|
||||
'Pragma List' AS contents;
|
||||
|
||||
-- =============================================================================
|
||||
|
||||
SELECT
|
||||
'Pragma List. Total ' || (SELECT count(*) FROM pragma_pragma_list()) AS contents,
|
||||
'title' AS component,
|
||||
4 AS level,
|
||||
TRUE AS center,
|
||||
'title_class' AS class,
|
||||
'title_id' AS id;
|
||||
|
||||
-- =============================================================================
|
||||
-- TABLE
|
||||
|
||||
SELECT
|
||||
'table' AS component,
|
||||
TRUE AS sort,
|
||||
TRUE AS search,
|
||||
TRUE AS border,
|
||||
TRUE AS hover,
|
||||
TRUE AS striped_columns,
|
||||
TRUE AS striped_rows,
|
||||
TRUE AS small,
|
||||
'table_class' AS class,
|
||||
'table_id' AS id;
|
||||
|
||||
SELECT * FROM pragma_pragma_list() AS functions ORDER BY name;
|
||||
|
||||
-- =============================================================================
|
||||
-- ============================ Module List ====================================
|
||||
-- =============================================================================
|
||||
|
||||
SELECT
|
||||
'divider' AS component,
|
||||
'Pragma List' AS contents;
|
||||
|
||||
-- =============================================================================
|
||||
|
||||
SELECT
|
||||
'Module List. Total ' || (SELECT count(*) FROM pragma_module_list()) AS contents,
|
||||
'title' AS component,
|
||||
4 AS level,
|
||||
TRUE AS center,
|
||||
'title_class' AS class,
|
||||
'title_id' AS id;
|
||||
|
||||
-- =============================================================================
|
||||
-- TABLE
|
||||
|
||||
SELECT
|
||||
'table' AS component,
|
||||
TRUE AS sort,
|
||||
TRUE AS search,
|
||||
TRUE AS border,
|
||||
TRUE AS hover,
|
||||
TRUE AS striped_columns,
|
||||
TRUE AS striped_rows,
|
||||
TRUE AS small,
|
||||
'table_class' AS class,
|
||||
'table_id' AS id;
|
||||
|
||||
SELECT * FROM pragma_module_list() ORDER BY name;
|
||||
|
||||
-- =============================================================================
|
||||
-- ============================ Table List =====================================
|
||||
-- =============================================================================
|
||||
|
||||
SELECT
|
||||
'divider' AS component,
|
||||
'Table List' AS contents;
|
||||
|
||||
-- =============================================================================
|
||||
|
||||
SELECT
|
||||
'Table List' AS contents,
|
||||
'title' AS component,
|
||||
4 AS level,
|
||||
TRUE AS center,
|
||||
'title_class' AS class,
|
||||
'title_id' AS id;
|
||||
|
||||
-- =============================================================================
|
||||
-- TABLE
|
||||
|
||||
SELECT
|
||||
'table' AS component,
|
||||
TRUE AS sort,
|
||||
TRUE AS search,
|
||||
TRUE AS border,
|
||||
TRUE AS hover,
|
||||
TRUE AS striped_columns,
|
||||
TRUE AS striped_rows,
|
||||
TRUE AS small,
|
||||
'table_class' AS class,
|
||||
'table_id' AS id;
|
||||
|
||||
SELECT * FROM pragma_table_list() ORDER BY type, name;
|
||||
|
||||
-- =============================================================================
|
||||
-- ============================ Database List ==================================
|
||||
-- =============================================================================
|
||||
|
||||
SELECT
|
||||
'divider' AS component,
|
||||
'Database List' AS contents;
|
||||
|
||||
-- =============================================================================
|
||||
|
||||
SELECT
|
||||
'Database List' AS contents,
|
||||
'title' AS component,
|
||||
4 AS level,
|
||||
TRUE AS center,
|
||||
'title_class' AS class,
|
||||
'title_id' AS id;
|
||||
|
||||
-- =============================================================================
|
||||
-- TABLE
|
||||
|
||||
SELECT
|
||||
'table' AS component,
|
||||
TRUE AS sort,
|
||||
TRUE AS search,
|
||||
TRUE AS border,
|
||||
TRUE AS hover,
|
||||
TRUE AS striped_columns,
|
||||
TRUE AS striped_rows,
|
||||
TRUE AS small,
|
||||
'table_class' AS class,
|
||||
'table_id' AS id;
|
||||
|
||||
SELECT * FROM pragma_database_list() ORDER BY seq;
|
||||
|
||||
-- =============================================================================
|
||||
-- DEBUG
|
||||
-- =============================================================================
|
||||
|
||||
SELECT
|
||||
'dynamic' AS component,
|
||||
sqlpage.run_sql('footer_debug_post-get-set.sql') AS properties;
|
||||
@@ -0,0 +1,48 @@
|
||||
-- Authentication Fence
|
||||
|
||||
set username = (
|
||||
SELECT username
|
||||
FROM sessions
|
||||
WHERE sqlpage.cookie('session_token') = id
|
||||
AND created_at > datetime('now', '-1 day') -- require the user to log in again after 1 day
|
||||
);
|
||||
|
||||
SELECT
|
||||
'redirect' AS component,
|
||||
'/' AS link -- redirect to the front page if the user is logged in
|
||||
WHERE $username IS NOT NULL;
|
||||
|
||||
-- =============================================================================
|
||||
|
||||
SELECT
|
||||
'shell' AS component,
|
||||
'CRUD with Authentication' AS title,
|
||||
'database' AS icon;
|
||||
|
||||
-- =============================================================================
|
||||
-- ================================= Login =====================================
|
||||
-- =============================================================================
|
||||
|
||||
SELECT
|
||||
'form' AS component,
|
||||
'Login' AS title,
|
||||
'create_session.sql' || ifnull('?path=' || $path, '') AS action;
|
||||
|
||||
-- Define form fields
|
||||
|
||||
SELECT
|
||||
column1 AS name, column2 AS label,
|
||||
column3 AS type, column4 AS required
|
||||
FROM (VALUES
|
||||
('username', 'Username', 'text', TRUE),
|
||||
('password', 'Password', 'password', TRUE)
|
||||
);
|
||||
|
||||
-- Show alert on failed authentication.
|
||||
|
||||
SELECT
|
||||
'alert' AS component,
|
||||
'danger' AS color,
|
||||
'You are not logged in' AS title,
|
||||
'Sorry, we could not log you in. Please try again.' AS description
|
||||
WHERE $error IS NOT NULL;
|
||||
@@ -0,0 +1,8 @@
|
||||
SELECT
|
||||
'cookie' AS component,
|
||||
'session_token' AS name,
|
||||
TRUE AS remove;
|
||||
|
||||
SELECT
|
||||
'redirect' AS component,
|
||||
ifnull($path, '/login.sql') AS link -- redirect to the login page after the user logs out
|
||||
@@ -0,0 +1,22 @@
|
||||
select
|
||||
'shell' as component,
|
||||
'SQLPage' as title,
|
||||
'database' as icon,
|
||||
'/' as link,
|
||||
'Top' as menu_item,
|
||||
'{"title":"About","submenu":[{"link":"/safety.sql","title":"Security"},{"link":"/performance.sql","title":"Performance"},{"link":"//github.com/sqlpage/SQLPage/blob/main/LICENSE.txt","title":"License"},{"link":"/blog.sql","title":"Articles"}]}' as menu_item,
|
||||
NULL as menu_item,
|
||||
'{"title":"Examples","submenu":[{"link":"/examples/tabs/","title":"Tabs"},{"link":"/examples/layouts.sql","title":"Layouts"},{"link":"/examples/multistep-form","title":"Forms"},{"link":"/examples/handle_picture_upload.sql","title":"File uploads"},{"link":"/examples/hash_password.sql","title":"Password protection"},{"link":"//github.com/sqlpage/SQLPage/blob/main/examples/","title":"All examples & demos"}]}' as menu_item,
|
||||
'{"title":"z", "icon": "settings"}' as menu_item,
|
||||
'{"title":"", "icon": ""}' as menu_item,
|
||||
'{"title":"Community","submenu":[{"link":"blog.sql","title":"Blog"},{"link":"//github.com/sqlpage/SQLPage/issues","title":"Report a bug"},{"link":"//github.com/sqlpage/SQLPage/discussions","title":"Discussions"},{"link":"//github.com/sqlpage/SQLPage","title":"Github"}]}' as menu_item,
|
||||
NULL as menu_item,
|
||||
'{"title":"Documentation","submenu":[{"link":"/your-first-sql-website","title":"Getting started"},{"link":"/components.sql","title":"All Components"},{"link":"/functions.sql","title":"SQLPage Functions"},{"link":"/custom_components.sql","title":"Custom Components"},{"link":"//github.com/sqlpage/SQLPage/blob/main/configuration.md#configuring-sqlpage","title":"Configuration"}]}' as menu_item,
|
||||
'boxed' as layout,
|
||||
'en-US' as language,
|
||||
'Documentation for the SQLPage low-code web application framework.' as description,
|
||||
'Poppins' as font,
|
||||
'https://cdn.jsdelivr.net/npm/prismjs@1/components/prism-core.min.js' as javascript,
|
||||
'https://cdn.jsdelivr.net/npm/prismjs@1/plugins/autoloader/prism-autoloader.min.js' as javascript,
|
||||
'/prism-tabler-theme.css' as css,
|
||||
'Official [SQLPage](https://sql-page.com) documentation' as footer;
|
||||
@@ -0,0 +1,271 @@
|
||||
set _get_vars = (
|
||||
SELECT
|
||||
json_group_object(
|
||||
"key",
|
||||
iif(CAST(CAST(value AS NUMERIC) AS TEXT) = value, CAST(value AS NUMERIC), value)
|
||||
) AS get_var
|
||||
FROM
|
||||
json_each(sqlpage.variables('GET'))
|
||||
WHERE NOT "key" like '\_%' ESCAPE '\'
|
||||
);
|
||||
|
||||
|
||||
set _locale_code = $lang; -- 'en', 'ru', 'de', 'fr', 'zh-cn'
|
||||
set _theme = 'fancy'; --$theme; -- 'default', 'fancy'
|
||||
set _hide_language_names = $hide_language_names; -- 0, 1 (BOOLEAN)
|
||||
set _authenticated = $authenticated; -- 0, 1 (BOOLEAN)
|
||||
|
||||
-- =============================================================================
|
||||
-- =============================================================================
|
||||
|
||||
WITH
|
||||
|
||||
-- test_values(_locale_code, _theme, _hide_language_names, _authenticated ) AS (VALUES
|
||||
-- ( 'en', 'fancy', TRUE, FALSE)
|
||||
-- ( NULL, NULL, NULL, NULL)
|
||||
-- ( 'fr', 'default', TRUE, TRUE)
|
||||
-- ),
|
||||
|
||||
-- Replaces values with appropriate variables
|
||||
-- ifnull guuards from invalid JSON errors
|
||||
|
||||
config_user AS (
|
||||
SELECT
|
||||
lower(ifnull($_locale_code, '_')) AS locale_code,
|
||||
lower(ifnull($_theme, '_')) AS theme,
|
||||
CAST($_hide_language_names AS INTEGER) AS hide_language_names,
|
||||
CAST(ifnull($_authenticated, FALSE) AS INTEGER) AS authenticated
|
||||
-- FROM test_values
|
||||
),
|
||||
|
||||
-- Inputs data guards
|
||||
|
||||
config_guards AS (
|
||||
SELECT
|
||||
(
|
||||
SELECT iif(contents ->> ('$.' || locale_code) IS NULL, NULL, locale_code)
|
||||
FROM sqlpage_files
|
||||
WHERE path = 'locales/locales.json'
|
||||
) AS locale_code,
|
||||
(
|
||||
SELECT iif(contents ->> ('$.' || theme) IS NULL, NULL, theme)
|
||||
FROM sqlpage_files
|
||||
WHERE path = 'themes/themes.json'
|
||||
) AS theme,
|
||||
hide_language_names,
|
||||
authenticated
|
||||
FROM config_user
|
||||
),
|
||||
|
||||
-- Retrieves locale and theme JSON data
|
||||
|
||||
config AS (
|
||||
SELECT
|
||||
iif(locale_code IS NULL, NULL, (
|
||||
SELECT contents ->> '$.menu'
|
||||
FROM sqlpage_files
|
||||
WHERE path = 'locales/' || locale_code || '/locale.json'
|
||||
)) AS locale,
|
||||
iif(theme IS NULL, NULL, (
|
||||
SELECT contents ->> '$.menu'
|
||||
FROM sqlpage_files
|
||||
WHERE path = 'themes/' || theme || '/theme.json'
|
||||
)) AS theme,
|
||||
(
|
||||
SELECT contents ->> '$.menu'
|
||||
FROM sqlpage_files
|
||||
WHERE path = 'themes/default/theme.json'
|
||||
) AS theme_default,
|
||||
(
|
||||
SELECT contents ->> '$.meta.label'
|
||||
FROM sqlpage_files
|
||||
WHERE path = 'locales/' || locale_code || '/locale.json'
|
||||
) AS locale_label,
|
||||
hide_language_names,
|
||||
authenticated
|
||||
FROM config_guards
|
||||
),
|
||||
|
||||
-- Prepares language items.
|
||||
-- This is a dynamically generated menu item.
|
||||
|
||||
locale_codes AS (
|
||||
SELECT "key" AS code, value AS position
|
||||
FROM sqlpage_files, json_each(contents)
|
||||
WHERE sqlpage_files.path = 'locales/locales.json'
|
||||
),
|
||||
languages AS (
|
||||
SELECT
|
||||
position,
|
||||
code,
|
||||
contents ->> '$.meta.label' AS label
|
||||
FROM sqlpage_files, locale_codes
|
||||
WHERE path like 'locales/%/locale.json'
|
||||
AND contents ->> '$.meta.code' = code COLLATE NOCASE
|
||||
ORDER BY position
|
||||
),
|
||||
|
||||
-- Prepares a list of top menu items with default icons.
|
||||
-- The language menu includes the "global/neutral/undefinded" icon.
|
||||
|
||||
top_menus_global AS (
|
||||
SELECT
|
||||
position,
|
||||
label,
|
||||
|
||||
-- Hide top menu with submenu if a particular filter is included in
|
||||
-- state_filter and its current value does match the specified value.
|
||||
--
|
||||
-- Note that the "class" attribute is set to two classes:
|
||||
-- 'menu_' || lower(label) and the same with '_slim' suffix.
|
||||
-- These classes are applied to respective submenus for css-based fine-tuning.
|
||||
|
||||
CASE
|
||||
WHEN (state_filter ->> '$.authenticated') IS NULL
|
||||
OR state_filter ->> '$.authenticated' = ifnull(authenticated, FALSE) THEN
|
||||
json_object(
|
||||
'title', iif(icon_only IS TRUE, NULL, ifnull(locale ->> ('$.' || label), label)),
|
||||
iif(theme IS NULL, 'icon', 'image'),
|
||||
iif(theme IS NULL, tabler_icon, '/' || (theme ->> ('$.' || label))),
|
||||
'class',
|
||||
-- Handles special cases: only sets the '_slim' class on the 'Language'
|
||||
-- menu when language names (labels) are hidden. Only set the base class
|
||||
-- for English localization (the extra whitespaces are painfull...)
|
||||
iif(ifnull(locale_label, 'English') IN ('English', 'Chinese'),
|
||||
' menu_' || lower(label), '') ||
|
||||
iif(label = 'Language' AND hide_language_names IS NOT TRUE, '',
|
||||
' menu_' || lower(label) || '_slim'
|
||||
)
|
||||
)
|
||||
ELSE
|
||||
NULL
|
||||
END AS top_item
|
||||
FROM menus, config
|
||||
WHERE parent_label IS NULL
|
||||
ORDER BY position
|
||||
),
|
||||
|
||||
-- The sole purpose of this separate modification is to set the icon
|
||||
-- of the top language menu to reflect the current locale
|
||||
|
||||
top_menus AS (
|
||||
SELECT
|
||||
position,
|
||||
label,
|
||||
iif(label <> 'Language' OR locale IS NULL OR top_item IS NULL,
|
||||
top_item,
|
||||
json_set(
|
||||
top_item,
|
||||
'$.image',
|
||||
'/' || (theme ->> ('$.' || locale_label))
|
||||
)
|
||||
) AS top_item
|
||||
FROM top_menus_global, config
|
||||
ORDER BY position
|
||||
),
|
||||
|
||||
-- Prepares a list of submenu lines.
|
||||
|
||||
menu_lines AS (
|
||||
SELECT
|
||||
parent_label,
|
||||
position,
|
||||
|
||||
-- Hides menu line if a particular filter is included in state_filter
|
||||
-- and its current value does match the specified value
|
||||
|
||||
CASE
|
||||
WHEN (state_filter ->> '$.authenticated') IS NULL
|
||||
OR state_filter ->> '$.authenticated' = ifnull(authenticated, FALSE) THEN
|
||||
json_object(
|
||||
'title', iif(icon_only IS TRUE, NULL, ifnull(locale ->> ('$.' || label), label)),
|
||||
iif(theme IS NULL, 'icon', 'image'),
|
||||
iif(theme IS NULL, tabler_icon, '/' || (theme ->> ('$.' || label))),
|
||||
'link', link
|
||||
)
|
||||
END AS menu_line
|
||||
FROM menus, config
|
||||
WHERE parent_label IS NOT NULL
|
||||
|
||||
-- Generates and appends the Language submenu lines
|
||||
|
||||
UNION ALL
|
||||
SELECT
|
||||
'Language' AS parent_label,
|
||||
position,
|
||||
json_object(
|
||||
'title',
|
||||
iif(hide_language_names IS TRUE, NULL, ifnull(locale ->> ('$.' || label), label)),
|
||||
'image', '/' || (iif(theme IS NULL, theme_default, theme) ->> ('$.' || label)),
|
||||
'link', '?lang=' || code
|
||||
) AS menu_line
|
||||
FROM languages, config
|
||||
ORDER BY parent_label, position
|
||||
),
|
||||
|
||||
-- Groups menu lines into submenus
|
||||
|
||||
menu_lists AS (
|
||||
SELECT
|
||||
parent_label,
|
||||
json_group_array(
|
||||
json(menu_line) ORDER BY position
|
||||
) AS menu_list
|
||||
FROM menu_lines
|
||||
GROUP BY parent_label
|
||||
),
|
||||
|
||||
-- Combines submenus with corresponding top menu lines yielding complete menu_item objects
|
||||
|
||||
menu_sets AS (
|
||||
SELECT
|
||||
position,
|
||||
label,
|
||||
json_set(json(top_item), '$.submenu', json(menu_list)) AS menu_set
|
||||
FROM top_menus, menu_lists
|
||||
WHERE top_menus.label = menu_lists.parent_label
|
||||
ORDER BY position
|
||||
),
|
||||
|
||||
-- Prepares final array of menu_item objects to be used with the "dynamic" component
|
||||
|
||||
menu AS (
|
||||
SELECT
|
||||
json_group_array(json(menu_set) ORDER BY position) AS menu
|
||||
FROM menu_sets
|
||||
),
|
||||
|
||||
-- shell_dynamic_static is included for debugging purposes. Call
|
||||
-- it to generate "static" SQL for inclusion in an SQLPage script.
|
||||
|
||||
shell_dynamic_static AS (
|
||||
SELECT
|
||||
'SELECT' || x'0A' || ' ''dynamic'' AS component,' || x'0A' ||
|
||||
quote(json_pretty(json_object(
|
||||
'component', 'shell',
|
||||
'title', 'SQLPage',
|
||||
'icon', 'database',
|
||||
'link', '/',
|
||||
'css', '/css/style.css',
|
||||
'menu_item', json(menu)
|
||||
))) || ' AS properties' || x'0A0A' AS properties
|
||||
FROM menu
|
||||
),
|
||||
|
||||
-- Call shell_dynamic if this script is processed directly by SQLPage.
|
||||
-- After copy-pasting adjust the input controls in the first CTE.
|
||||
|
||||
shell_dynamic AS (
|
||||
SELECT
|
||||
'dynamic' AS component,
|
||||
json_object(
|
||||
'component', 'shell',
|
||||
'title', 'SQLPage',
|
||||
'icon', 'database',
|
||||
'link', '/',
|
||||
'css', '/css/style.css',
|
||||
'menu_item', json(menu)
|
||||
) AS properties
|
||||
FROM menu
|
||||
)
|
||||
SELECT * FROM shell_dynamic;
|
||||
@@ -0,0 +1,258 @@
|
||||
-- set _locale_code = $lang; -- 'en', 'ru', 'de', 'fr', 'zh-cn'
|
||||
-- set _theme = $theme; -- 'default', 'fancy'
|
||||
-- set _hide_language_names = $hide_language_names; -- 0, 1 (BOOLEAN)
|
||||
-- set _authenticated = $authenticated; -- 0, 1 (BOOLEAN)
|
||||
|
||||
-- =============================================================================
|
||||
-- =============================================================================
|
||||
|
||||
WITH
|
||||
|
||||
test_values(_locale_code, _theme, _hide_language_names, _authenticated ) AS (VALUES
|
||||
-- ( 'en', 'fancy', TRUE, FALSE)
|
||||
-- ( NULL, NULL, NULL, NULL)
|
||||
( 'fr', 'default', TRUE, TRUE)
|
||||
),
|
||||
|
||||
-- Replaces values with appropriate variables
|
||||
-- ifnull guuards from invalid JSON errors
|
||||
|
||||
config_user AS (
|
||||
SELECT
|
||||
lower(ifnull(_locale_code, '_')) AS locale_code,
|
||||
lower(ifnull(_theme, '_')) AS theme,
|
||||
CAST(_hide_language_names AS INTEGER) AS hide_language_names,
|
||||
CAST(ifnull(_authenticated, FALSE) AS INTEGER) AS authenticated
|
||||
FROM test_values
|
||||
),
|
||||
|
||||
-- Inputs data guards
|
||||
|
||||
config_guards AS (
|
||||
SELECT
|
||||
(
|
||||
SELECT iif(contents ->> ('$.' || locale_code) IS NULL, NULL, locale_code)
|
||||
FROM sqlpage_files
|
||||
WHERE path = 'locales/locales.json'
|
||||
) AS locale_code,
|
||||
(
|
||||
SELECT iif(contents ->> ('$.' || theme) IS NULL, NULL, theme)
|
||||
FROM sqlpage_files
|
||||
WHERE path = 'themes/themes.json'
|
||||
) AS theme,
|
||||
hide_language_names,
|
||||
authenticated
|
||||
FROM config_user
|
||||
),
|
||||
|
||||
-- Retrieves locale and theme JSON data
|
||||
|
||||
config AS (
|
||||
SELECT
|
||||
iif(locale_code IS NULL, NULL, (
|
||||
SELECT contents ->> '$.menu'
|
||||
FROM sqlpage_files
|
||||
WHERE path = 'locales/' || locale_code || '/locale.json'
|
||||
)) AS locale,
|
||||
iif(theme IS NULL, NULL, (
|
||||
SELECT contents ->> '$.menu'
|
||||
FROM sqlpage_files
|
||||
WHERE path = 'themes/' || theme || '/theme.json'
|
||||
)) AS theme,
|
||||
(
|
||||
SELECT contents ->> '$.menu'
|
||||
FROM sqlpage_files
|
||||
WHERE path = 'themes/default/theme.json'
|
||||
) AS theme_default,
|
||||
(
|
||||
SELECT contents ->> '$.meta.label'
|
||||
FROM sqlpage_files
|
||||
WHERE path = 'locales/' || locale_code || '/locale.json'
|
||||
) AS locale_label,
|
||||
hide_language_names,
|
||||
authenticated
|
||||
FROM config_guards
|
||||
),
|
||||
|
||||
-- Prepares language items.
|
||||
-- This is a dynamically generated menu item.
|
||||
|
||||
locale_codes AS (
|
||||
SELECT "key" AS code, value AS position
|
||||
FROM sqlpage_files, json_each(contents)
|
||||
WHERE sqlpage_files.path = 'locales/locales.json'
|
||||
),
|
||||
languages AS (
|
||||
SELECT
|
||||
position,
|
||||
code,
|
||||
contents ->> '$.meta.label' AS label
|
||||
FROM sqlpage_files, locale_codes
|
||||
WHERE path like 'locales/%/locale.json'
|
||||
AND contents ->> '$.meta.code' = code COLLATE NOCASE
|
||||
ORDER BY position
|
||||
),
|
||||
|
||||
-- Prepares a list of top menu items with default icons.
|
||||
-- The language menu includes the "global/neutral/undefinded" icon.
|
||||
|
||||
top_menus_global AS (
|
||||
SELECT
|
||||
position,
|
||||
label,
|
||||
|
||||
-- Hide top menu with submenu if a particular filter is included in
|
||||
-- state_filter and its current value does match the specified value.
|
||||
--
|
||||
-- Note that the "class" attribute is set to two classes:
|
||||
-- 'menu_' || lower(label) and the same with '_slim' suffix.
|
||||
-- These classes are applied to respective submenus for css-based fine-tuning.
|
||||
|
||||
CASE
|
||||
WHEN (state_filter ->> '$.authenticated') IS NULL
|
||||
OR state_filter ->> '$.authenticated' = ifnull(authenticated, FALSE) THEN
|
||||
json_object(
|
||||
'title', iif(icon_only IS TRUE, NULL, ifnull(locale ->> ('$.' || label), label)),
|
||||
iif(theme IS NULL, 'icon', 'image'),
|
||||
iif(theme IS NULL, tabler_icon, '/' || (theme ->> ('$.' || label))),
|
||||
'class',
|
||||
-- Handles special cases: only sets the '_slim' class on the 'Language'
|
||||
-- menu when language names (labels) are hidden. Only set the base class
|
||||
-- for English localization (the extra whitespaces are painfull...)
|
||||
iif(locale_label = 'English', ' menu_' || lower(label), '') ||
|
||||
iif(label = 'Language' AND hide_language_names IS NOT TRUE, '',
|
||||
' menu_' || lower(label) || '_slim'
|
||||
)
|
||||
)
|
||||
ELSE
|
||||
NULL
|
||||
END AS top_item
|
||||
FROM menus, config
|
||||
WHERE parent_label IS NULL
|
||||
ORDER BY position
|
||||
),
|
||||
|
||||
-- The sole purpose of this separate modification is to set the icon
|
||||
-- of the top language menu to reflect the current locale
|
||||
|
||||
top_menus AS (
|
||||
SELECT
|
||||
position,
|
||||
label,
|
||||
iif(label <> 'Language' OR locale IS NULL OR top_item IS NULL,
|
||||
top_item,
|
||||
json_set(
|
||||
top_item,
|
||||
'$.image',
|
||||
'/' || (theme ->> ('$.' || locale_label))
|
||||
)
|
||||
) AS top_item
|
||||
FROM top_menus_global, config
|
||||
ORDER BY position
|
||||
),
|
||||
|
||||
-- Prepares a list of submenu lines.
|
||||
|
||||
menu_lines AS (
|
||||
SELECT
|
||||
parent_label,
|
||||
position,
|
||||
|
||||
-- Hides menu line if a particular filter is included in state_filter
|
||||
-- and its current value does match the specified value
|
||||
|
||||
CASE
|
||||
WHEN (state_filter ->> '$.authenticated') IS NULL
|
||||
OR state_filter ->> '$.authenticated' = ifnull(authenticated, FALSE) THEN
|
||||
json_object(
|
||||
'title', iif(icon_only IS TRUE, NULL, ifnull(locale ->> ('$.' || label), label)),
|
||||
iif(theme IS NULL, 'icon', 'image'),
|
||||
iif(theme IS NULL, tabler_icon, '/' || (theme ->> ('$.' || label))),
|
||||
'link', link
|
||||
)
|
||||
END AS menu_line
|
||||
FROM menus, config
|
||||
WHERE parent_label IS NOT NULL
|
||||
|
||||
-- Generates and appends the Language submenu lines
|
||||
|
||||
UNION ALL
|
||||
SELECT
|
||||
'Language' AS parent_label,
|
||||
position,
|
||||
json_object(
|
||||
'title',
|
||||
iif(hide_language_names IS TRUE, NULL, ifnull(locale ->> ('$.' || label), label)),
|
||||
'image', '/' || (iif(theme IS NULL, theme_default, theme) ->> ('$.' || label)),
|
||||
'link', '/locales/locale.sql?lang=' || code
|
||||
) AS menu_line
|
||||
FROM languages, config
|
||||
ORDER BY parent_label, position
|
||||
),
|
||||
|
||||
-- Groups menu lines into submenus
|
||||
|
||||
menu_lists AS (
|
||||
SELECT
|
||||
parent_label,
|
||||
json_group_array(
|
||||
json(menu_line) ORDER BY position
|
||||
) AS menu_list
|
||||
FROM menu_lines
|
||||
GROUP BY parent_label
|
||||
),
|
||||
|
||||
-- Combines submenus with corresponding top menu lines yielding complete menu_item objects
|
||||
|
||||
menu_sets AS (
|
||||
SELECT
|
||||
position,
|
||||
label,
|
||||
json_set(json(top_item), '$.submenu', json(menu_list)) AS menu_set
|
||||
FROM top_menus, menu_lists
|
||||
WHERE top_menus.label = menu_lists.parent_label
|
||||
ORDER BY position
|
||||
),
|
||||
|
||||
-- Prepares final array of menu_item objects to be used with the "dynamic" component
|
||||
|
||||
menu AS (
|
||||
SELECT
|
||||
json_group_array(json(menu_set) ORDER BY position) AS menu
|
||||
FROM menu_sets
|
||||
),
|
||||
|
||||
-- shell_dynamic_static is included for debugging purposes. Call
|
||||
-- it to generate "static" SQL for inclusion in an SQLPage script.
|
||||
|
||||
shell_dynamic_static AS (
|
||||
SELECT
|
||||
'SELECT' || x'0A' || ' ''dynamic'' AS component,' || x'0A' ||
|
||||
quote(json_pretty(json_object(
|
||||
'component', 'shell',
|
||||
'title', 'SQLPage',
|
||||
'icon', 'database',
|
||||
'link', '/',
|
||||
'css', '/css/style.css',
|
||||
'menu_item', json(menu)
|
||||
))) || ' AS properties' || x'0A0A' AS properties
|
||||
FROM menu
|
||||
),
|
||||
|
||||
-- Call shell_dynamic if this script is processed directly by SQLPage.
|
||||
-- After copy-pasting adjust the input controls in the first CTE.
|
||||
|
||||
shell_dynamic AS (
|
||||
SELECT
|
||||
'dynamic' AS component,
|
||||
json_object(
|
||||
'component', 'shell',
|
||||
'title', 'SQLPage',
|
||||
'icon', 'database',
|
||||
'link', '/',
|
||||
'css', '/css/style.css',
|
||||
'menu_item', json(menu)
|
||||
) AS properties
|
||||
FROM menu
|
||||
)
|
||||
SELECT * FROM shell_dynamic_static;
|
||||
@@ -0,0 +1,116 @@
|
||||
select 'dynamic' as component,
|
||||
'[
|
||||
{
|
||||
"component": "shell",
|
||||
"title": "SQLPage",
|
||||
"icon": "database",
|
||||
"link": "/",
|
||||
"menu_item": [
|
||||
{
|
||||
"icon": "settings",
|
||||
"title": "Z",
|
||||
"button": true,
|
||||
"shape": "pill",
|
||||
"narrow": true,
|
||||
"outline": "warning",
|
||||
"submenu": [
|
||||
{
|
||||
"link": "/safety.sql",
|
||||
"icon": "user",
|
||||
"button": true,
|
||||
"shape": "pill",
|
||||
"size": "sm",
|
||||
"tooltip": "User",
|
||||
"color": "yellow"
|
||||
},
|
||||
{},
|
||||
{
|
||||
"link": "/performance.sql",
|
||||
"icon": "logout",
|
||||
"tooltip": "Logout",
|
||||
"button": true,
|
||||
"shape": "pill",
|
||||
"size": "sm",
|
||||
"outline": "warning"
|
||||
|
||||
},
|
||||
{
|
||||
"link": "/performance.sql",
|
||||
"icon": "",
|
||||
"tooltip": "Logout",
|
||||
"button": true,
|
||||
"shape": "pill",
|
||||
"size": "sm",
|
||||
"outline": "warning"
|
||||
|
||||
}
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
"icon": "database",
|
||||
"title": "Dummy",
|
||||
"button": true,
|
||||
"shape": "pill",
|
||||
"narrow": true,
|
||||
"color": "green"
|
||||
},
|
||||
{
|
||||
"icon": "",
|
||||
"title": "",
|
||||
"button": true,
|
||||
"shape": "pill",
|
||||
"narrow": true,
|
||||
"color": "blue"
|
||||
},
|
||||
{
|
||||
"title": "Examples",
|
||||
"icon": "trash",
|
||||
"submenu": [
|
||||
{
|
||||
"link": "/examples/tabs/",
|
||||
"icon": "device-floppy",
|
||||
"title": "Tabs"
|
||||
},
|
||||
{
|
||||
"link": "/examples/layouts.sql",
|
||||
"button": true,
|
||||
"tooltip": "Layouts",
|
||||
"title": "Layouts",
|
||||
"color": "primary"
|
||||
},
|
||||
{
|
||||
"link": "/examples/multistep-form",
|
||||
"title": "Forms"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Community",
|
||||
"submenu": [
|
||||
{
|
||||
"link": "blog.sql",
|
||||
"title": "Blog"
|
||||
},
|
||||
{
|
||||
"link": "//github.com/sqlpage/SQLPage/issues",
|
||||
"title": "Report a bug"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]' AS properties;
|
||||
|
||||
SELECT
|
||||
'button' AS component,
|
||||
'pill' AS shape,
|
||||
'' AS size,
|
||||
'center' AS justify;
|
||||
SELECT
|
||||
'' AS title,
|
||||
'browse_rec' AS id,
|
||||
'green' AS outline,
|
||||
TRUE AS narrow,
|
||||
'#' AS link,
|
||||
'/icons/earth-icon.svg' AS img;
|
||||