diff --git a/CHANGELOG.md b/CHANGELOG.md index d9ffed58..1aa62084 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ - MySQL's [`JSON_TABLE`](https://dev.mysql.com/doc/refman/8.0/en/json-table-functions.html) table-valued function, that allows easily iterating over json structures - MySQL's [`CALL`](https://dev.mysql.com/doc/refman/8.0/en/call.html) statements, to call stored procedures. - PostgreSQL `^@` starts-with operator + - New [carousel](https://sql.ophir.dev/documentation.sql?component=carousel#component) component to display a carousel of images. + - For those who write [custom components](https://sql.ophir.dev/custom_components.sql), a new `@component_index` variable is available in templates to get the index of the current component in the page. This makes it easy to generate unique ids for components. ## 0.18.2 (2024-01-29) diff --git a/Cargo.lock b/Cargo.lock index c8c2b4be..202d72d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -179,7 +179,7 @@ dependencies = [ "tokio-util", "tracing", "webpki-roots 0.22.6", - "webpki-roots 0.25.3", + "webpki-roots 0.25.4", ] [[package]] @@ -592,7 +592,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 0.25.3", + "webpki-roots 0.25.4", ] [[package]] @@ -1552,9 +1552,9 @@ dependencies = [ [[package]] name = "handlebars" -version = "5.1.1" +version = "5.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c73166c591e67fb4bf9bc04011b4e35f12e89fe8d676193aa263df065955a379" +checksum = "ab283476b99e66691dee3f1640fea91487a8d81f50fb5ecc75538f8f8879a1e4" dependencies = [ "log", "pest", @@ -2661,9 +2661,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.30" +version = "0.38.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca" +checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" dependencies = [ "bitflags 2.4.2", "errno", @@ -2719,7 +2719,7 @@ dependencies = [ "serde", "serde_json", "thiserror", - "webpki-roots 0.25.3", + "webpki-roots 0.25.4", "x509-parser", ] @@ -3070,7 +3070,7 @@ dependencies = [ "tokio-stream", "url", "uuid", - "webpki-roots 0.25.3", + "webpki-roots 0.25.4", "whoami", ] @@ -3203,7 +3203,7 @@ dependencies = [ "cfg-if", "fastrand 2.0.1", "redox_syscall", - "rustix 0.38.30", + "rustix 0.38.31", "windows-sys 0.52.0", ] @@ -3229,9 +3229,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe80ced77cbfb4cb91a94bf72b378b4b6791a0d9b7f09d0be747d1bdff4e68bd" +checksum = "00b24b79b7a07f10209f19e683ca1e289d80b1e76ffa8c2b779718566a083679" dependencies = [ "deranged", "itoa", @@ -3284,9 +3284,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.35.1" +version = "1.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104" +checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" dependencies = [ "backtrace", "bytes", @@ -3694,9 +3694,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.25.3" +version = "0.25.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1778a42e8b3b90bff8d0f5032bf22250792889a5cdc752aa0020c84abe3aaf10" +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" [[package]] name = "whoami" @@ -3873,9 +3873,9 @@ checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" [[package]] name = "winnow" -version = "0.5.36" +version = "0.5.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "818ce546a11a9986bc24f93d0cdf38a8a1a400f1473ea8c82e59f6e0ffab9249" +checksum = "a7cad8365489051ae9f054164e459304af2e7e9bb407c958076c8bf4aef52da5" dependencies = [ "memchr", ] diff --git a/examples/official-site/custom_components.sql b/examples/official-site/custom_components.sql index 68004fff..b71e0e06 100644 --- a/examples/official-site/custom_components.sql +++ b/examples/official-site/custom_components.sql @@ -111,6 +111,14 @@ and SQLPage adds a few more: - `each_row`: iterates over the rows of a query result - `typeof`: returns the type of a value (`string`, `number`, `boolean`, `object`, `array`, `null`) +### Attributes + +In addition to the parameters you pass to your components in your SQL queries, +SQLPage adds the following attributes to the context of your components: + + - `@component_index` : the index of the current component in the page. Useful to generate unique ids or classes. + - `@row_index` : the index of the current row in the current component. Useful to implement special behavior on the first row, for instance. + ## Overwriting the default components You can overwrite the default components, including the `shell` component, diff --git a/examples/official-site/sqlpage/migrations/34_carousel.sql b/examples/official-site/sqlpage/migrations/34_carousel.sql new file mode 100644 index 00000000..adb7b68a --- /dev/null +++ b/examples/official-site/sqlpage/migrations/34_carousel.sql @@ -0,0 +1,136 @@ +INSERT INTO component (name, description, icon, introduced_in_version) +VALUES ( + 'carousel', + 'A carousel is used to display multiple pieces of visual content without taking up too much space.', + 'carousel-horizontal', + '0.18.3' + ); +INSERT INTO parameter ( + component, + name, + description, + type, + top_level, + optional + ) +VALUES + ( + 'carousel', + 'title', + 'A name to display at the top of the carousel.', + 'TEXT', + TRUE, + TRUE + ), + ( + 'carousel', + 'indicators', + 'Style of image indicators (square or dot).', + 'TEXT', + TRUE, + TRUE + ), + ( + 'carousel', + 'vertical', + 'Whether to use the vertical image indicators.', + 'BOOLEAN', + TRUE, + TRUE + ), + ( + 'carousel', + 'controls', + 'Whether to show the control links to go previous or next item.', + 'BOOLEAN', + TRUE, + TRUE + ), + ( + 'carousel', + 'width', + 'Width of the component, between 1 and 12. Default is 12.', + 'NUMBER', + TRUE, + TRUE + ), + ( + 'carousel', + 'auto', + 'Whether to automatically cycle through the carousel items. Default is false.', + 'BOOLEAN', + TRUE, + TRUE + ), + ( + 'carousel', + 'center', + 'Whether to center the carousel.', + 'BOOLEAN', + TRUE, + TRUE + ), + ( + 'carousel', + 'fade', + 'Whether to apply the fading effect.', + 'BOOLEAN', + TRUE, + TRUE + ), + ( + 'carousel', + 'image', + 'The URL (absolute or relative) of an image to display in the carousel.', + 'URL', + FALSE, + FALSE + ), + ( + 'carousel', + 'title', + 'Add caption to the slide.', + 'TEXT', + FALSE, + TRUE + ), + ( + 'carousel', + 'description', + 'A short paragraph.', + 'TEXT', + FALSE, + TRUE + ), + ( + 'carousel', + 'description_md', + 'A short paragraph formatted using markdown.', + 'TEXT', + FALSE, + TRUE + ); +-- Insert example(s) for the component +INSERT INTO example(component, description, properties) +VALUES ( + 'carousel', + 'A basic example of carousel', + JSON( + '[ + {"component":"carousel","name":"cats1","title":"Famous Database Animals"}, + {"image":"https://upload.wikimedia.org/wikipedia/commons/thumb/d/d7/Elefantes_africanos_de_sabana_%28Loxodonta_africana%29%2C_Elephant_Sands%2C_Botsuana%2C_2018-07-28%2C_DD_114-117_PAN.jpg/2560px-Elefantes_africanos_de_sabana_%28Loxodonta_africana%29%2C_Elephant_Sands%2C_Botsuana%2C_2018-07-28%2C_DD_114-117_PAN.jpg"}, + {"image":"https://upload.wikimedia.org/wikipedia/commons/thumb/9/99/Penguin_Island_panorama_with_ferry_and_dolphins_in_foreground%2C_March_2023_06.jpg/1280px-Penguin_Island_panorama_with_ferry_and_dolphins_in_foreground%2C_March_2023_06.jpg"} + ]' + ) + ), + ( + 'carousel', + 'An advanced example of carousel with controls', + JSON( + '[ + {"component":"carousel","name":"cats2","title":"Cats","width":6,"center":true,"controls":true,"auto":true}, + {"image":"https://upload.wikimedia.org/wikipedia/commons/thumb/2/29/Cat_Sphynx._Kittens._img_11.jpg/1024px-Cat_Sphynx._Kittens._img_11.jpg","title":"A first cat","description":"The cat (Felis catus), commonly referred to as the domestic cat or house cat, is the only domesticated species in the family Felidae."}, + {"image":"https://upload.wikimedia.org/wikipedia/commons/thumb/e/ec/Cat_close-up_2004_b.jpg/1280px-Cat_close-up_2004_b.jpg","title":"Another cat"} + ]' + ) + ); \ No newline at end of file diff --git a/examples/official-site/sqlpage/migrations/32_shared_id_class_attributes.sql b/examples/official-site/sqlpage/migrations/99_shared_id_class_attributes.sql similarity index 94% rename from examples/official-site/sqlpage/migrations/32_shared_id_class_attributes.sql rename to examples/official-site/sqlpage/migrations/99_shared_id_class_attributes.sql index dcba2a49..ebe9610c 100644 --- a/examples/official-site/sqlpage/migrations/32_shared_id_class_attributes.sql +++ b/examples/official-site/sqlpage/migrations/99_shared_id_class_attributes.sql @@ -17,7 +17,8 @@ FROM (VALUES ('timeline', FALSE), -- ('title', TRUE), ('tracking', TRUE), - ('text', TRUE) + ('text', TRUE), + ('carousel', TRUE) ); INSERT INTO parameter(component, top_level, name, description, type, optional) @@ -47,6 +48,7 @@ FROM (VALUES ('timeline', TRUE), ('timeline', FALSE), -- ('title', TRUE), - ('tracking', TRUE) + ('tracking', TRUE), + ('carousel', TRUE) ); diff --git a/sqlpage/templates/carousel.handlebars b/sqlpage/templates/carousel.handlebars new file mode 100644 index 00000000..059a51aa --- /dev/null +++ b/sqlpage/templates/carousel.handlebars @@ -0,0 +1,40 @@ +
+
+ {{#if title}} +
+
{{title}}
+
+ {{/if}} + +
+ {{#if controls}} + + + Previous + + + + Next + + {{/if}} +
diff --git a/src/render.rs b/src/render.rs index 35d0f77e..b94e25b8 100644 --- a/src/render.rs +++ b/src/render.rs @@ -295,7 +295,7 @@ impl RenderContext { } else { PAGE_SHELL_COMPONENT }; - let mut shell_renderer = Self::create_renderer(shell_component, Arc::clone(&app_state)) + let mut shell_renderer = Self::create_renderer(shell_component, Arc::clone(&app_state), 0) .await .with_context(|| "The shell component should always exist")?; @@ -484,12 +484,17 @@ impl RenderContext { async fn create_renderer( component: &str, app_state: Arc, + component_index: usize, ) -> anyhow::Result { let split_template = app_state .all_templates .get_template(&app_state, component) .await?; - Ok(SplitTemplateRenderer::new(split_template, app_state)) + Ok(SplitTemplateRenderer::new( + split_template, + app_state, + component_index, + )) } /// Set a new current component and return the old one @@ -497,7 +502,16 @@ impl RenderContext { &mut self, component: &str, ) -> anyhow::Result> { - let new_component = Self::create_renderer(component, Arc::clone(&self.app_state)).await?; + let current_component_index = self + .current_component + .as_ref() + .map_or(1, |c| c.component_index); + let new_component = Self::create_renderer( + component, + Arc::clone(&self.app_state), + current_component_index + 1, + ) + .await?; Ok(self.current_component.replace(new_component)) } @@ -552,16 +566,22 @@ pub struct SplitTemplateRenderer { ctx: Context, app_state: Arc, row_index: usize, + component_index: usize, } impl SplitTemplateRenderer { - fn new(split_template: Arc, app_state: Arc) -> Self { + fn new( + split_template: Arc, + app_state: Arc, + component_index: usize, + ) -> Self { Self { split_template, local_vars: None, app_state, row_index: 0, ctx: Context::null(), + component_index, } } fn name(&self) -> &str { @@ -585,6 +605,13 @@ impl SplitTemplateRenderer { .unwrap_or_default(), ); let mut render_context = handlebars::RenderContext::new(None); + render_context + .block_mut() + .expect("context created without block") + .set_local_var( + "component_index", + JsonValue::Number(self.component_index.into()), + ); *self.ctx.data_mut() = data; let mut output = HandlebarWriterOutput(writer); self.split_template.before_list.render( @@ -678,7 +705,7 @@ mod tests { let mut output = Vec::new(); let config = app_config::tests::test_config(); let app_state = Arc::new(AppState::init(&config).await.unwrap()); - let mut rdr = SplitTemplateRenderer::new(Arc::new(split), app_state); + let mut rdr = SplitTemplateRenderer::new(Arc::new(split), app_state, 0); rdr.render_start(&mut output, json!({"name": "SQL"}))?; rdr.render_item(&mut output, json!({"x": 1}))?; rdr.render_item(&mut output, json!({"x": 2}))?; @@ -699,7 +726,7 @@ mod tests { let mut output = Vec::new(); let config = app_config::tests::test_config(); let app_state = Arc::new(AppState::init(&config).await.unwrap()); - let mut rdr = SplitTemplateRenderer::new(Arc::new(split), app_state); + let mut rdr = SplitTemplateRenderer::new(Arc::new(split), app_state, 0); rdr.render_start(&mut output, json!(null))?; rdr.render_item(&mut output, json!({"x": 1}))?; rdr.render_item(&mut output, json!({"x": 2}))?;