@@ -573,10 +573,6 @@ SlideForm.propTypes = {
selectTemplate: PropTypes.func.isRequired,
selectedTemplate: PropTypes.shape({
"@id": PropTypes.string,
- resources: PropTypes.shape({
- admin: PropTypes.string.isRequired,
- component: PropTypes.string.isRequired,
- }).isRequired,
}),
isLoading: PropTypes.bool,
loadingMessage: PropTypes.string,
diff --git a/assets/client/components/region.jsx b/assets/client/components/region.jsx
index 4d2dba5e..a06d2b7b 100644
--- a/assets/client/components/region.jsx
+++ b/assets/client/components/region.jsx
@@ -3,10 +3,10 @@ import PropTypes from "prop-types";
import "./region.scss";
import { createGridArea } from "../../shared/grid-generator/grid-generator";
import { TransitionGroup, CSSTransition } from "react-transition-group";
-import Slide from "./render-slide.jsx";
import ErrorBoundary from "./error-boundary.jsx";
import idFromPath from "../util/id-from-path";
import logger from "../logger/logger";
+import Slide from "./slide.jsx";
/**
* Region component.
diff --git a/assets/client/components/touch-region.jsx b/assets/client/components/touch-region.jsx
index 8ff6c145..05d3b556 100644
--- a/assets/client/components/touch-region.jsx
+++ b/assets/client/components/touch-region.jsx
@@ -2,11 +2,11 @@ import { React, useEffect, useState, createRef } from "react";
import PropTypes from "prop-types";
import "./touch-region.scss";
import { createGridArea } from "../../shared/grid-generator/grid-generator";
-import Slide from "./render-slide.jsx";
import ErrorBoundary from "./error-boundary.jsx";
import idFromPath from "../util/id-from-path";
import IconClose from "../assets/icon-close.svg";
import IconPointer from "../assets/icon-pointer.svg";
+import Slide from "./slide.jsx";
/**
* Region component.
diff --git a/assets/shared/custom-templates-example/custom-template-example.json b/assets/shared/custom-templates-example/custom-template-example.json
new file mode 100644
index 00000000..547022df
--- /dev/null
+++ b/assets/shared/custom-templates-example/custom-template-example.json
@@ -0,0 +1,22 @@
+{
+ "title": "Test",
+ "id": "01K2PREY1Q0XCTR9EK1YT5R4XK",
+ "options": {},
+ "adminForm": [
+ {
+ "key": "test-form-1",
+ "input": "header",
+ "text": "Skabelon: Test",
+ "formGroupClasses": "h4 mb-3"
+ },
+ {
+ "key": "test-form-title",
+ "input": "input",
+ "name": "title",
+ "type": "text",
+ "label": "Overskrift",
+ "helpText": "Her kan du skrive overskrift.",
+ "formGroupClasses": "mb-3"
+ }
+ ]
+}
diff --git a/assets/shared/custom-templates-example/custom-template-example.jsx b/assets/shared/custom-templates-example/custom-template-example.jsx
new file mode 100644
index 00000000..ed53a646
--- /dev/null
+++ b/assets/shared/custom-templates-example/custom-template-example.jsx
@@ -0,0 +1,58 @@
+import React, { useEffect } from "react";
+import templateConfig from "./custom-template-example.json";
+import BaseSlideExecution from "../slide-utils/base-slide-execution.js";
+import { ThemeStyles } from "../slide-utils/slide-util.jsx";
+
+function id() {
+ return templateConfig.id;
+}
+
+function config() {
+ return templateConfig;
+}
+
+function renderSlide(slide, run, slideDone) {
+ return
;
+}
+
+/**
+ * @param {object} props Props.
+ * @param {object} props.slide The slide.
+ * @param {object} props.content The slide content.
+ * @param {boolean} props.run Whether or not the slide should start running.
+ * @param {Function} props.slideDone Function to invoke when the slide is done playing.
+ * @param {string} props.executionId Unique id for the instance.
+ * @returns {JSX.Element} The component.
+ */
+function CustomTemplateExample({ slide, content, run, slideDone, executionId }) {
+ const { duration = 15000 } = content;
+ const { title = "Default title" } = content;
+
+ const slideExecution = new BaseSlideExecution(slide, slideDone);
+
+ useEffect(() => {
+ if (run) {
+ slideExecution.start(duration);
+ }
+
+ return function cleanup() {
+ slideExecution.stop();
+ };
+ }, [run]);
+
+ return (<>
+
+
{title}
+
+
+
+ >);
+}
+
+export default { id, config, renderSlide };
diff --git a/assets/shared/slide-utils/slide-config.js b/assets/shared/slide-utils/slide-config.js
deleted file mode 100644
index 1552172d..00000000
--- a/assets/shared/slide-utils/slide-config.js
+++ /dev/null
@@ -1,65 +0,0 @@
-import bookReviewConfig from "../templates/book-review.json";
-import calendarConfig from "../templates/calendar.json";
-import contactsConfig from "../templates/contacts.json";
-import iframeConfig from "../templates/iframe.json";
-import imageTextConfig from "../templates/image-text.json";
-import posterConfig from "../templates/poster.json";
-import rssConfig from "../templates/rss.json";
-import slideshowConfig from "../templates/slideshow.json";
-import instagramFeedConfig from "../templates/instagram-feed.json";
-import newsFeedConfig from "../templates/news-feed.json";
-import tableConfig from "../templates/table.json";
-import travelConfig from "../templates/travel.json";
-import videoConfig from "../templates/video.json";
-import vimeoPlayerConfig from "../templates/vimeo-player.json";
-
-function getSlideConfig(templateUlid) {
- switch (templateUlid) {
- // BookReview
- case "01FP2SME0ENTXWF362XHM6Z1B4":
- return bookReviewConfig;
- // Calendar
- case "01FRJPF4XATRN8PBZ35XN84PS6":
- return calendarConfig;
- // Contacts
- case "01FPZ19YEHX7MQ5Q6ZS0WK0VEA":
- return contactsConfig;
- // IFrame
- case "01FQBJQ2M3544ZKAADPWBXHY71":
- return iframeConfig;
- // ImageText
- case "01FP2SNGFN0BZQH03KCBXHKYHG":
- return imageTextConfig;
- // Poster
- case "01FWJZQ25A1868V63CWYYHQFKQ":
- return posterConfig;
- // RSS
- case "01FQC300GGWCA7A8H0SXY6P9FG":
- return rssConfig;
- // Slideshow
- case "01FP2SNSC9VXD10ZKXQR819NS9":
- return slideshowConfig;
- // InstagramFeed
- case "01FTZC0RKJYHG4JVZG5K709G46":
- return instagramFeedConfig;
- // NewsFeed
- case "01JEWPAFF93YSF418TH72W1SBA":
- return newsFeedConfig;
- // Table
- case "01FQBJFKM0YFX1VW5K94VBSNCP":
- return tableConfig;
- // Travel
- case "01FZD7K807VAKZ99BGSSCHRJM6":
- return travelConfig;
- // Video
- case "01FQBJFKM0YFX1VW5K94VBSNCC":
- return videoConfig;
- // Vimeo
- case "01FQBJQ2M3544ZKAADPWBXHY17":
- return vimeoPlayerConfig;
- default:
- return [];
- }
-}
-
-export default getSlideConfig;
diff --git a/assets/shared/slide-utils/templates.js b/assets/shared/slide-utils/templates.js
index 2d556528..e45427fc 100644
--- a/assets/shared/slide-utils/templates.js
+++ b/assets/shared/slide-utils/templates.js
@@ -1,21 +1,39 @@
-// Load all templates
+// Load all templates.
+// @see https://vite.dev/guide/features.html#glob-import
const templateModules = import.meta.glob('../templates/*.jsx', { eager: true })
const customTemplatesModules = import.meta.glob('../custom-templates/*.jsx', { eager: true })
-const idToModule = {};
+function findModule(modules, templateUlid) {
+ for (const key of Object.keys(modules)) {
+ const module = modules[key].default;
-Object.keys(templateModules).map((path) => {
- const module = templateModules[path].default;
- idToModule[module.id()] = module;
-});
+ if (typeof(module.id) === "function" &&
+ typeof(module.config) == "function" &&
+ typeof(module.renderSlide) == "function") {
+ if (module.id() === templateUlid) {
+ return module;
+ }
+ } else {
+ throw new Error("Template should have functions id(), config(), and renderSlide()");
+ }
+ }
-Object.keys(customTemplatesModules).map((path) => {
- const module = customTemplatesModules[path].default;
- idToModule[module.id()] = module;
-});
+ return null;
+}
function getTemplateModule(templateUlid) {
- return idToModule[templateUlid];
+ if (!templateUlid) {
+ return null;
+ }
+
+ const module = findModule(templateModules, templateUlid) ??
+ findModule(customTemplatesModules, templateUlid) ?? null;
+
+ if (module === null) {
+ throw new Error(`Cannot find module '${templateUlid}'`);
+ }
+
+ return module;
}
function getSlideConfig(templateUlid) {
@@ -24,7 +42,13 @@ function getSlideConfig(templateUlid) {
function renderSlide(slide, run, slideDone) {
const templateUlid = slide?.templateData?.id;
- return getTemplateModule(templateUlid).renderSlide(slide, run, slideDone);
+ const module = getTemplateModule(templateUlid);
+
+ if (!module) {
+ return '';
+ }
+
+ return module.renderSlide(slide, run, slideDone);
}
export {
diff --git a/assets/shared/templates/contacts.json b/assets/shared/templates/contacts.json
index 937318d5..aa4771ae 100644
--- a/assets/shared/templates/contacts.json
+++ b/assets/shared/templates/contacts.json
@@ -1,8 +1,6 @@
{
"title": "Kontakter",
- "icon": "",
"id": "01FPZ19YEHX7MQ5Q6ZS0WK0VEA",
- "description": "Skabelon til kontakter.",
"adminForm": [
{
"key": "contacts-form-1",
diff --git a/assets/template/fixtures/slide-fixtures.js b/assets/template/fixtures/slide-fixtures.js
index cdfd9643..abe134b7 100644
--- a/assets/template/fixtures/slide-fixtures.js
+++ b/assets/template/fixtures/slide-fixtures.js
@@ -1655,17 +1655,6 @@ const slideFixtures = [
mediaContain: true,
},
},
- {
- id: "test-0",
- templateData: {
- id: "01FP2SNGFN0BZQH03KCBXHKYH1",
- },
- content: {
- duration: 5000,
- vimeoid: "882393277",
- mediaContain: true,
- },
- },
];
export default slideFixtures;
diff --git a/composer.json b/composer.json
index 90708ca7..f2eb1e47 100644
--- a/composer.json
+++ b/composer.json
@@ -31,6 +31,7 @@
"symfony/dom-crawler": "~6.4.0",
"symfony/dotenv": "~6.4.0",
"symfony/expression-language": "~6.4.0",
+ "symfony/finder": "~6.4.0",
"symfony/flex": "^2.0",
"symfony/framework-bundle": "~6.4.0",
"symfony/http-client": "~6.4.0",
diff --git a/composer.lock b/composer.lock
index 3888ba8d..edaae548 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "e17b8074ba5e1eee3d1fd7014d14036c",
+ "content-hash": "1bd6fd456477d1e03a03bb4359ad3e77",
"packages": [
{
"name": "api-platform/core",
@@ -6890,16 +6890,16 @@
},
{
"name": "symfony/finder",
- "version": "v6.4.17",
+ "version": "v6.4.24",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
- "reference": "1d0e8266248c5d9ab6a87e3789e6dc482af3c9c7"
+ "reference": "73089124388c8510efb8d2d1689285d285937b08"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/finder/zipball/1d0e8266248c5d9ab6a87e3789e6dc482af3c9c7",
- "reference": "1d0e8266248c5d9ab6a87e3789e6dc482af3c9c7",
+ "url": "https://api.github.com/repos/symfony/finder/zipball/73089124388c8510efb8d2d1689285d285937b08",
+ "reference": "73089124388c8510efb8d2d1689285d285937b08",
"shasum": ""
},
"require": {
@@ -6934,7 +6934,7 @@
"description": "Finds files and directories via an intuitive fluent interface",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/finder/tree/v6.4.17"
+ "source": "https://github.com/symfony/finder/tree/v6.4.24"
},
"funding": [
{
@@ -6945,12 +6945,16 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
- "time": "2024-12-29T13:51:37+00:00"
+ "time": "2025-07-15T12:02:45+00:00"
},
{
"name": "symfony/flex",
diff --git a/docs/custom-templates.md b/docs/custom-templates.md
new file mode 100644
index 00000000..cd1ed409
--- /dev/null
+++ b/docs/custom-templates.md
@@ -0,0 +1,46 @@
+# Custom Templates
+
+It is possible to include your own templates in your installation.
+
+## Location
+
+Custom templates should be placed in the folder assets/shared/custom-templates/.
+This folder is in .gitignore so the contents will not be added to the git repository.
+
+How you populate this folder with your custom templates is up to you:
+
+* A git repository with root in the `assets/shared/custom-templates/` folder.
+* A symlink from another folder.
+* Maintaining a fork of the display repository.
+* ...
+
+## Files
+
+The following files are required for a custom template:
+
+* custom-template-name.jsx - A javascript module for the template.
+* custom-template-name.json - A configuration file for the template.
+
+Replace `custom-template-name` with a unique name for the template.
+
+### custom-template-name.jsx
+
+The `.jsx` should expose the following functions:
+
+* id() - The ULID of the template. Generate a ULID for your custom template.
+* config() - Should contain the following keys: id (as above), title (the titel displayed in the admin), options,
+ adminForm.
+* renderSlide(slide, run, slideDone) - Should return the JSX for the template.
+
+For an example of a custom template see `assets/shared/custom-templates-example/`.
+
+## Contributing template
+
+If you think the template could be used by other, consider contributing the template to the project as a Pull Request.
+
+### Guide for contributing template
+
+* Fork the `os2display/display` repository.
+* Move your custom template files (the .json and .jsx files and other required files) from the
+ `assets/shared/custom-templates/` folder to the `assets/shared/templates/` folder.
+* Create a PR to `os2display/display` repository.
diff --git a/fixtures/slide.yaml b/fixtures/slide.yaml
index a2f8a70d..782e8d2a 100644
--- a/fixtures/slide.yaml
+++ b/fixtures/slide.yaml
@@ -30,7 +30,7 @@ App\Entity\Tenant\Slide:
tenant: "@tenant_xyz"
slide_abc_notified (extends slide):
title: "slide_abc_notified"
- template: "@template_notified"
+ template: "@template_instagram_feed"
content:
maxEntries: 6
tenant: "@tenant_abc"
diff --git a/fixtures/template.yaml b/fixtures/template.yaml
index d8671f62..f0c6cad8 100644
--- a/fixtures/template.yaml
+++ b/fixtures/template.yaml
@@ -1,16 +1,79 @@
---
App\Entity\Template:
- template_image_text:
- title: "template_image_text"
- description: A template with different formats of image and text
- resources:
+ template_book_review:
+ id: ''
+ title: "Anmeldelse"
+ createdAt (unique): ''
+ modifiedAt: ''
+
+ template_calendar:
+ id: ''
+ title: "Kalender"
+ createdAt (unique): ''
+ modifiedAt: ''
+
+ template_contacts:
+ id: ''
+ title: "Kontakter"
+ createdAt (unique): ''
+ modifiedAt: ''
+
+ template_iframe:
+ id: ''
+ title: "IFrame"
createdAt (unique): ''
modifiedAt: ''
+
+ template_image_text:
id: ''
- template_notified:
- title: "template_notified"
- description: A template with different that serves notified data
- resources:
+ title: "Billede og tekst"
createdAt (unique): ''
modifiedAt: ''
+
+ template_instagram_feed:
id: ''
+ title: "Instagram feed"
+ createdAt (unique): ''
+ modifiedAt: ''
+
+ template_news_feed:
+ id: ''
+ title: "Nyheder"
+ createdAt (unique): ''
+ modifiedAt: ''
+
+ template_poster:
+ id: ''
+ title: "Plakat"
+ createdAt (unique): ''
+ modifiedAt: ''
+
+ template_rss:
+ id: ''
+ title: "Slideshow"
+ createdAt (unique): ''
+ modifiedAt: ''
+
+ template_table:
+ id: ''
+ title: "Tabel"
+ createdAt (unique): ''
+ modifiedAt: ''
+
+ template_travel:
+ id: ''
+ title: "Rejseplanen"
+ createdAt (unique): ''
+ modifiedAt: ''
+
+ template_video:
+ id: ''
+ title: "Video"
+ createdAt (unique): ''
+ modifiedAt: ''
+
+ template_vimeo_player:
+ id: ''
+ title: "Vimeo Player"
+ createdAt (unique): ''
+ modifiedAt: ''
diff --git a/migrations/Version20250815092648.php b/migrations/Version20250815092648.php
new file mode 100644
index 00000000..e6833738
--- /dev/null
+++ b/migrations/Version20250815092648.php
@@ -0,0 +1,28 @@
+addSql('ALTER TABLE template DROP icon, DROP resources, DROP description');
+ }
+
+ public function down(Schema $schema): void
+ {
+ // this down() migration is auto-generated, please modify it to your needs
+ $this->addSql('ALTER TABLE template ADD icon VARCHAR(255) DEFAULT \'\' NOT NULL, ADD resources JSON NOT NULL COMMENT \'(DC2Type:json)\', ADD description VARCHAR(255) DEFAULT \'\' NOT NULL');
+ }
+}
diff --git a/src/Command/LoadTemplateCommand.php b/src/Command/LoadTemplateCommand.php
index 48a3c7a2..0ef4bbdd 100644
--- a/src/Command/LoadTemplateCommand.php
+++ b/src/Command/LoadTemplateCommand.php
@@ -33,15 +33,13 @@ public function __construct(
protected function configure(): void
{
- $this->addArgument('filename', InputArgument::REQUIRED, 'json file to load. Can be a local file or a URL');
- $this->addOption('path-from-filename', 'p', InputOption::VALUE_NONE, 'Set path to component and admin from filename. Assumes that the config file loaded has the naming format: [templateName]-config[.*].json.', null);
- $this->addOption('timestamp', 't', InputOption::VALUE_NONE, 'Add a timestamp to the component and admin urls: ?ts=. Only applies if path-from-filename option is active.', null);
+ $this->addArgument('templateUlid', InputArgument::OPTIONAL, 'templateUlid to load');
}
final protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
- $successMessage = 'Template updated';
+
try {
/** @var string $filename */
@@ -106,7 +104,6 @@ final protected function execute(InputInterface $input, OutputInterface $output)
}
}
- $template->setResources($resources);
$template->setTitle($content->title);
$template->setDescription($content->description);
diff --git a/src/Command/UpdateCommand.php b/src/Command/UpdateCommand.php
index 440310df..0f6514d3 100644
--- a/src/Command/UpdateCommand.php
+++ b/src/Command/UpdateCommand.php
@@ -1,5 +1,7 @@
''])]
- private string $icon = '';
-
- #[ORM\Column(type: Types::JSON)]
- private array $resources = [];
+ private string $title = '';
/**
* @var Collection
@@ -43,30 +37,8 @@ public function __construct()
{
$this->slides = new ArrayCollection();
$this->tenants = new ArrayCollection();
- }
-
- public function getIcon(): string
- {
- return $this->icon;
- }
- public function setIcon(string $icon): self
- {
- $this->icon = $icon;
-
- return $this;
- }
-
- public function getResources(): array
- {
- return $this->resources;
- }
-
- public function setResources(array $resources): self
- {
- $this->resources = $resources;
-
- return $this;
+ parent::__construct();
}
/**
@@ -112,4 +84,14 @@ public function removeAllSlides(): self
return $this;
}
+
+ public function getTitle(): string
+ {
+ return $this->title;
+ }
+
+ public function setTitle(string $title): void
+ {
+ $this->title = $title;
+ }
}
diff --git a/src/State/TemplateProvider.php b/src/State/TemplateProvider.php
index 6a517877..c124e0c8 100644
--- a/src/State/TemplateProvider.php
+++ b/src/State/TemplateProvider.php
@@ -24,12 +24,10 @@ public function toOutput(object $object): TemplateDTO
$output = new TemplateDTO();
$output->id = $object->getId();
$output->title = $object->getTitle();
- $output->description = $object->getDescription();
$output->modified = $object->getModifiedAt();
$output->created = $object->getCreatedAt();
$output->modifiedBy = $object->getModifiedBy();
$output->createdBy = $object->getCreatedBy();
- $output->resources = $object->getResources();
return $output;
}
From b59d034c7da20b0b719e82a5259ff1fd3dca80b6 Mon Sep 17 00:00:00 2001
From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com>
Date: Fri, 15 Aug 2025 14:46:52 +0200
Subject: [PATCH 08/21] 4565: Improved documentation
---
assets/admin/components/slide/slide-form.jsx | 4 +--
assets/shared/slide-utils/templates.js | 33 +++++++++++++++-----
2 files changed, 28 insertions(+), 9 deletions(-)
diff --git a/assets/admin/components/slide/slide-form.jsx b/assets/admin/components/slide/slide-form.jsx
index f70d591e..e9226103 100644
--- a/assets/admin/components/slide/slide-form.jsx
+++ b/assets/admin/components/slide/slide-form.jsx
@@ -26,7 +26,7 @@ import "./slide-form.scss";
import Preview from "../preview/preview";
import StickyFooter from "../util/sticky-footer";
import Select from "../util/forms/select";
-import { getSlideConfig } from "../../../shared/slide-utils/templates";
+import { getConfig } from "../../../shared/slide-utils/templates";
/**
* The slide form component.
@@ -159,7 +159,7 @@ function SlideForm({
const newSelectedTemplates = [];
if (selectedTemplate) {
- const slideConfig = getSlideConfig(selectedTemplate['id']);
+ const slideConfig = getConfig(selectedTemplate['id']);
setContentFormElements(slideConfig.adminForm ?? []);
newSelectedTemplates.push(selectedTemplate);
}
diff --git a/assets/shared/slide-utils/templates.js b/assets/shared/slide-utils/templates.js
index e45427fc..7241594a 100644
--- a/assets/shared/slide-utils/templates.js
+++ b/assets/shared/slide-utils/templates.js
@@ -1,20 +1,25 @@
-// Load all templates.
+// Load templates.
// @see https://vite.dev/guide/features.html#glob-import
+// @see docs/custom-templates.md
const templateModules = import.meta.glob('../templates/*.jsx', { eager: true })
const customTemplatesModules = import.meta.glob('../custom-templates/*.jsx', { eager: true })
+function duckTypingTemplateModule(module) {
+ return typeof(module.id) === "function" &&
+ typeof(module.config) === "function" &&
+ typeof(module.renderSlide) === "function";
+}
+
function findModule(modules, templateUlid) {
for (const key of Object.keys(modules)) {
const module = modules[key].default;
- if (typeof(module.id) === "function" &&
- typeof(module.config) == "function" &&
- typeof(module.renderSlide) == "function") {
+ if (duckTypingTemplateModule(module)) {
if (module.id() === templateUlid) {
return module;
}
} else {
- throw new Error("Template should have functions id(), config(), and renderSlide()");
+ throw new Error("Template should implement functions: id(), config(), renderSlide(slide, run, slideDone)");
}
}
@@ -36,10 +41,24 @@ function getTemplateModule(templateUlid) {
return module;
}
-function getSlideConfig(templateUlid) {
+/**
+ * Get the config of the template.
+ *
+ * @param templateUlid The ULID of the template.
+ * @return object
+ */
+function getConfig(templateUlid) {
return getTemplateModule(templateUlid).config();
}
+/**
+ * Render slide.
+ *
+ * @param {object} slide The slide object.
+ * @param {string} run The run id.
+ * @param {Function} slideDone The function to invoke when the slide is done.
+ * @return {JSXElement|string}
+ */
function renderSlide(slide, run, slideDone) {
const templateUlid = slide?.templateData?.id;
const module = getTemplateModule(templateUlid);
@@ -52,6 +71,6 @@ function renderSlide(slide, run, slideDone) {
}
export {
- getSlideConfig,
+ getConfig,
renderSlide
}
From c49b09426b128a7a208690580c1a6721c3cc41d5 Mon Sep 17 00:00:00 2001
From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com>
Date: Mon, 18 Aug 2025 09:29:51 +0200
Subject: [PATCH 09/21] 4565: Added templates list command
---
fixtures/template.yaml | 8 +-
src/Command/LoadTemplateCommand.php | 203 ---------------------------
src/Command/TemplatesListCommand.php | 56 ++++++++
src/Service/TemplateService.php | 138 ++++++++++++++++++
4 files changed, 201 insertions(+), 204 deletions(-)
delete mode 100644 src/Command/LoadTemplateCommand.php
create mode 100644 src/Command/TemplatesListCommand.php
create mode 100644 src/Service/TemplateService.php
diff --git a/fixtures/template.yaml b/fixtures/template.yaml
index f0c6cad8..edc17fb8 100644
--- a/fixtures/template.yaml
+++ b/fixtures/template.yaml
@@ -48,12 +48,18 @@ App\Entity\Template:
createdAt (unique): ''
modifiedAt: ''
- template_rss:
+ template_slideshow:
id: ''
title: "Slideshow"
createdAt (unique): ''
modifiedAt: ''
+ template_rss:
+ id: ''
+ title: "RSS"
+ createdAt (unique): ''
+ modifiedAt: ''
+
template_table:
id: ''
title: "Tabel"
diff --git a/src/Command/LoadTemplateCommand.php b/src/Command/LoadTemplateCommand.php
deleted file mode 100644
index 0ef4bbdd..00000000
--- a/src/Command/LoadTemplateCommand.php
+++ /dev/null
@@ -1,203 +0,0 @@
-addArgument('templateUlid', InputArgument::OPTIONAL, 'templateUlid to load');
- }
-
- final protected function execute(InputInterface $input, OutputInterface $output): int
- {
- $io = new SymfonyStyle($input, $output);
-
-
- try {
- /** @var string $filename */
- $filename = $input->getArgument('filename');
-
- $content = json_decode(file_get_contents($filename), false, 512, JSON_THROW_ON_ERROR);
-
- // Validate template json.
- $schemaStorage = new SchemaStorage();
- $jsonSchemaObject = $this->getSchema();
- $schemaStorage->addSchema('file://contentSchema', $jsonSchemaObject);
- $validator = new Validator(new Factory($schemaStorage));
- $validator->validate($content, $jsonSchemaObject);
-
- if ($validator->isValid()) {
- $io->info('The supplied JSON validates against the schema.');
- } else {
- $message = "JSON does not validate. Violations:\n";
- foreach ($validator->getErrors() as $error) {
- $message .= sprintf("\n[%s] %s", $error['property'], $error['message']);
- }
-
- $io->error($message);
-
- return Command::INVALID;
- }
-
- if (!Ulid::isValid($content->id)) {
- $io->error('The Ulid is not valid');
-
- return Command::INVALID;
- }
-
- $repository = $this->entityManager->getRepository(Template::class);
- $template = $repository->findOneBy(['id' => Ulid::fromString($content->id)]);
-
- if (!$template) {
- $template = new Template();
- $metadata = $this->entityManager->getClassMetaData($template::class);
- $metadata->setIdGenerator(new AssignedGenerator());
-
- $ulid = Ulid::fromString($content->id);
-
- $template->setId($ulid);
-
- $this->entityManager->persist($template);
- $successMessage = 'Template added';
- }
-
- $template->setIcon($content->icon);
-
- $resources = get_object_vars($content->resources);
-
- if ($input->getOption('path-from-filename')) {
- // Set paths to component and admin from filename.
- $resources['component'] = preg_replace("/-config.*\.json$/", '.js', $filename);
- $resources['admin'] = preg_replace("/-config.*\.json$/", '-admin.json', $filename);
-
- if ($input->getOption('timestamp')) {
- $resources['component'] = $resources['component'].'?ts='.time();
- $resources['admin'] = $resources['admin'].'?ts='.time();
- }
- }
-
- $template->setTitle($content->title);
- $template->setDescription($content->description);
-
- $this->entityManager->flush();
-
- $io->success($successMessage);
-
- return Command::SUCCESS;
- } catch (\JsonException) {
- $io->error('Invalid json');
-
- return Command::INVALID;
- }
- }
-
- /**
- * Supplies json schema for validation.
- *
- * @return mixed
- * Json schema
- *
- * @throws \JsonException
- */
- private function getSchema(): mixed
- {
- $jsonSchema = <<<'JSON'
- {
- "$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://os2display.dk/config-schema.json",
- "title": "Config file schema",
- "description": "Schema for defining config files for templates",
- "type": "object",
- "properties": {
- "id": {
- "description": "Ulid",
- "type": "string"
- },
- "title": {
- "description": "The title of the template",
- "type": "string"
- },
- "description": {
- "description": "A description of the template",
- "type": "string"
- },
- "icon": {
- "description": "An icon for the template",
- "type": "string"
- },
- "resources": {
- "type": "object",
- "properties": {
- "schema": {
- "description": "Path to the json schema for the content",
- "type": "string"
- },
- "component": {
- "description": "Path to the react remote component that renders the content",
- "type": "string"
- },
- "admin": {
- "description": "Path to the json array describing the content form in the administration interface",
- "type": "string"
- },
- "assets": {
- "description": "Assets that are needed by the template",
- "type": "array",
- "items": {
- "type": "object",
- "description": "Asset item",
- "properties": {
- "type": {
- "type": "string",
- "url": "string"
- }
- }
- }
- },
- "options": {
- "description": "Default option values for the template",
- "type": "object"
- },
- "content": {
- "description": "Default content values for the template",
- "type": "object"
- }
- },
- "required": ["schema", "component", "admin", "assets", "options", "content"]
- }
- },
- "required": ["id", "icon", "description", "resources", "title"]
- }
- JSON;
-
- return json_decode($jsonSchema, false, 512, JSON_THROW_ON_ERROR);
- }
-}
diff --git a/src/Command/TemplatesListCommand.php b/src/Command/TemplatesListCommand.php
new file mode 100644
index 00000000..132222b3
--- /dev/null
+++ b/src/Command/TemplatesListCommand.php
@@ -0,0 +1,56 @@
+templateService->getCoreTemplates();
+
+ if (count($templates) === 0) {
+ $io->error("No core templates found.");
+
+ return Command::INVALID;
+ }
+
+ $customTemplates = $this->templateService->getCustomTemplates();
+
+ $io->table(['ID', 'Title', 'Status', 'Type'], array_map(fn (array $templateArray) => [
+ $templateArray['id'],
+ $templateArray['title'],
+ $templateArray['installed'] ? 'Installed' : 'Not Installed',
+ $templateArray['type'],
+ ], array_merge($templates, $customTemplates)));
+
+ return Command::SUCCESS;
+ } catch (\Exception $e) {
+ $io->error($e->getMessage());
+
+ return Command::INVALID;
+ }
+ }
+
+}
diff --git a/src/Service/TemplateService.php b/src/Service/TemplateService.php
new file mode 100644
index 00000000..d51e16f3
--- /dev/null
+++ b/src/Service/TemplateService.php
@@ -0,0 +1,138 @@
+files()->followLinks()->ignoreUnreadableDirs()->in('assets/shared/templates')->depth('== 0')->name('*.json');
+
+ if ($finder->hasResults()) {
+ return $this->getTemplates($finder);
+ }
+ }
+
+ return [];
+ }
+
+ public function getCustomTemplates(): array
+ {
+ $finder = new Finder();
+
+ if (is_dir('assets/shared/custom-templates')) {
+ $finder->files()->followLinks()->ignoreUnreadableDirs()->in('assets/shared/custom-templates')->depth('== 0')->name('*.json');
+
+ if ($finder->hasResults()) {
+ return $this->getTemplates($finder, true);
+ }
+ }
+
+ return [];
+ }
+
+ public function getTemplates(iterable $finder, bool $customTemplates = false): array
+ {
+ $templates = [];
+
+ // Validate template json.
+ $schemaStorage = new SchemaStorage();
+ $jsonSchemaObject = $this->getSchema();
+ $schemaStorage->addSchema('file://contentSchema', $jsonSchemaObject);
+ $validator = new Validator(new Factory($schemaStorage));
+
+ foreach ($finder as $file) {
+ $content = json_decode($file->getContents());
+ $validator->validate($content, $jsonSchemaObject);
+
+ if (!$validator->isValid()) {
+ $message = "JSON file " . $file->getFilename() . " does not validate. Violations:\n";
+ foreach ($validator->getErrors() as $error) {
+ $message .= sprintf("\n[%s] %s", $error['property'], $error['message']);
+ }
+
+ throw new Exception($message);
+ }
+
+ if (!Ulid::isValid($content->id)) {
+ throw new Exception('The Ulid is not valid');
+ }
+
+ $repository = $this->entityManager->getRepository(Template::class);
+ $template = $repository->findOneBy(['id' => Ulid::fromString($content->id)]);
+
+ $templates[] = [
+ 'id' => $content->id,
+ 'title' => $content->title,
+ 'templateEntity' => $template,
+ 'installed' => $template !== null,
+ 'type' => $customTemplates ? 'Custom' : 'Core',
+ ];
+ }
+
+ return $templates;
+ }
+
+ /**
+ * Supplies json schema for validation.
+ *
+ * @return mixed
+ * Json schema
+ *
+ * @throws \JsonException
+ */
+ public function getSchema(): object
+ {
+ $jsonSchema = <<<'JSON'
+ {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://os2display.dk/config-schema.json",
+ "title": "Config file schema",
+ "description": "Schema for defining config files for templates",
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "Ulid",
+ "type": "string"
+ },
+ "title": {
+ "description": "The title of the template",
+ "type": "string"
+ },
+ "options": {
+ "description": "Template options",
+ "type": "object"
+ },
+ "adminForm": {
+ "description": "The admin form description",
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "Form element"
+ }
+ }
+ },
+ "required": ["id", "title", "options", "adminForm"]
+ }
+ JSON;
+
+ return json_decode($jsonSchema, false, 512, JSON_THROW_ON_ERROR);
+ }
+}
From 53142434840eca9f58f90857745514078b0f8d8f Mon Sep 17 00:00:00 2001
From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com>
Date: Mon, 18 Aug 2025 11:07:55 +0200
Subject: [PATCH 10/21] 4565: Added command to install template
---
src/Command/TemplatesInstallCommand.php | 84 +++++++++++++++++++++++++
src/Service/TemplateService.php | 23 +++++++
2 files changed, 107 insertions(+)
create mode 100644 src/Command/TemplatesInstallCommand.php
diff --git a/src/Command/TemplatesInstallCommand.php b/src/Command/TemplatesInstallCommand.php
new file mode 100644
index 00000000..b6e01db9
--- /dev/null
+++ b/src/Command/TemplatesInstallCommand.php
@@ -0,0 +1,84 @@
+addOption('all', 'a', InputOption::VALUE_NONE, "Install all available templates");
+ $this->addArgument('templateUlid', InputArgument::OPTIONAL, "Install the template with the given ULID");
+ }
+
+ final protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $io = new SymfonyStyle($input, $output);
+
+ $all = $input->getOption('all');
+
+ $templates = $this->templateService->getAllTemplates();
+
+ if ($all) {
+ foreach ($templates as $templateArray) {
+ if (!$templateArray['installed']) {
+ $this->templateService->installTemplate($templateArray);
+ }
+ }
+
+ $io->success("Installed all available templates");
+ return Command::SUCCESS;
+ }
+
+ $templateUlid = $input->getArgument('templateUlid');
+
+ if (!$templateUlid) {
+ $io->warning("Template ULID not supplied.");
+ return Command::INVALID;
+ }
+
+ $templateToInstall = array_find($templates, function (array $template) use ($templateUlid): bool {
+ return $template['id'] === $templateUlid;
+ });
+
+ if ($templateToInstall !== null) {
+ if ($templateToInstall['installed']) {
+ $io->warning("Template ULID already installed");
+ return Command::INVALID;
+ } else {
+ $this->templateService->installTemplate($templateToInstall);
+ $io->success("Template " .$templateToInstall['title'] . " installed");
+ }
+ } else {
+ $io->warning("Template files not found.");
+ return Command::INVALID;
+ }
+
+ return Command::SUCCESS;
+ }
+
+}
diff --git a/src/Service/TemplateService.php b/src/Service/TemplateService.php
index d51e16f3..ef4b1db2 100644
--- a/src/Service/TemplateService.php
+++ b/src/Service/TemplateService.php
@@ -4,6 +4,7 @@
use App\Entity\Template;
use Doctrine\ORM\EntityManagerInterface;
+use Doctrine\ORM\Id\AssignedGenerator;
use JsonSchema\Constraints\Factory;
use JsonSchema\SchemaStorage;
use JsonSchema\Validator;
@@ -18,6 +19,26 @@ public function __construct(
) {
}
+ public function installTemplate($templateData): void
+ {
+ $template = new Template();
+ $template->setTitle($templateData['title']);
+
+ $metadata = $this->entityManager->getClassMetaData($template::class);
+ $metadata->setIdGenerator(new AssignedGenerator());
+
+ $ulid = Ulid::fromString($templateData['id']);
+ $template->setId($ulid);
+
+ $this->entityManager->persist($template);
+ $this->entityManager->flush();
+ }
+
+ public function getAllTemplates(): array
+ {
+ return array_merge($this->getCoreTemplates(), $this->getCustomTemplates());
+ }
+
public function getCoreTemplates(): array
{
$finder = new Finder();
@@ -81,6 +102,8 @@ public function getTemplates(iterable $finder, bool $customTemplates = false): a
$templates[] = [
'id' => $content->id,
'title' => $content->title,
+ 'adminForm' => $content->adminForm,
+ 'options' => $content->options,
'templateEntity' => $template,
'installed' => $template !== null,
'type' => $customTemplates ? 'Custom' : 'Core',
From 633b22727bcc61822e8daa141a1edc5f771d55ee Mon Sep 17 00:00:00 2001
From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com>
Date: Mon, 18 Aug 2025 12:58:05 +0200
Subject: [PATCH 11/21] 4565: Cleanup
---
src/Command/TemplatesInstallCommand.php | 5 +----
1 file changed, 1 insertion(+), 4 deletions(-)
diff --git a/src/Command/TemplatesInstallCommand.php b/src/Command/TemplatesInstallCommand.php
index b6e01db9..1a4b4373 100644
--- a/src/Command/TemplatesInstallCommand.php
+++ b/src/Command/TemplatesInstallCommand.php
@@ -4,10 +4,8 @@
namespace App\Command;
-use App\Entity\Template;
use App\Service\TemplateService;
use Doctrine\ORM\EntityManagerInterface;
-use Doctrine\ORM\Id\AssignedGenerator;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
@@ -15,7 +13,6 @@
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
-use Symfony\Component\Uid\Ulid;
#[AsCommand(
name: 'app:templates:install',
@@ -24,7 +21,7 @@
class TemplatesInstallCommand extends Command
{
public function __construct(
- private readonly TemplateService $templateService, private readonly EntityManagerInterface $entityManager,
+ private readonly TemplateService $templateService,
) {
parent::__construct();
}
From 080c6c4927fb48ca3e7b3d336578915b2acc7319 Mon Sep 17 00:00:00 2001
From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com>
Date: Mon, 18 Aug 2025 13:41:59 +0200
Subject: [PATCH 12/21] 4565: Fixed template commands
---
src/Command/TemplatesInstallCommand.php | 31 ++++++++-----------
src/Command/TemplatesListCommand.php | 11 ++++---
src/Model/TemplateData.php | 18 +++++++++++
src/Service/TemplateService.php | 41 +++++++++++++++----------
4 files changed, 61 insertions(+), 40 deletions(-)
create mode 100644 src/Model/TemplateData.php
diff --git a/src/Command/TemplatesInstallCommand.php b/src/Command/TemplatesInstallCommand.php
index 1a4b4373..7591a87e 100644
--- a/src/Command/TemplatesInstallCommand.php
+++ b/src/Command/TemplatesInstallCommand.php
@@ -4,6 +4,7 @@
namespace App\Command;
+use App\Model\TemplateData;
use App\Service\TemplateService;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
@@ -41,10 +42,8 @@ final protected function execute(InputInterface $input, OutputInterface $output)
$templates = $this->templateService->getAllTemplates();
if ($all) {
- foreach ($templates as $templateArray) {
- if (!$templateArray['installed']) {
- $this->templateService->installTemplate($templateArray);
- }
+ foreach ($templates as $templateToInstall) {
+ $this->templateService->installTemplate($templateToInstall);
}
$io->success("Installed all available templates");
@@ -58,24 +57,20 @@ final protected function execute(InputInterface $input, OutputInterface $output)
return Command::INVALID;
}
- $templateToInstall = array_find($templates, function (array $template) use ($templateUlid): bool {
- return $template['id'] === $templateUlid;
+ $templatesFound = array_find($templates, function (TemplateData $templateData) use ($templateUlid): bool {
+ return $templateData->id === $templateUlid;
});
- if ($templateToInstall !== null) {
- if ($templateToInstall['installed']) {
- $io->warning("Template ULID already installed");
- return Command::INVALID;
- } else {
- $this->templateService->installTemplate($templateToInstall);
- $io->success("Template " .$templateToInstall['title'] . " installed");
- }
- } else {
- $io->warning("Template files not found.");
- return Command::INVALID;
+ if (count($templatesFound) !== 1) {
+ $io->error("Template not found.");
+ return Command::FAILURE;
}
+ $templateToInstall = $templatesFound[0];
+
+ $this->templateService->installTemplate($templateToInstall);
+ $io->success("Template " .$templateToInstall->title . " installed");
+
return Command::SUCCESS;
}
-
}
diff --git a/src/Command/TemplatesListCommand.php b/src/Command/TemplatesListCommand.php
index 132222b3..2a589af6 100644
--- a/src/Command/TemplatesListCommand.php
+++ b/src/Command/TemplatesListCommand.php
@@ -4,6 +4,7 @@
namespace App\Command;
+use App\Model\TemplateData;
use App\Service\TemplateService;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
@@ -38,11 +39,11 @@ final protected function execute(InputInterface $input, OutputInterface $output)
$customTemplates = $this->templateService->getCustomTemplates();
- $io->table(['ID', 'Title', 'Status', 'Type'], array_map(fn (array $templateArray) => [
- $templateArray['id'],
- $templateArray['title'],
- $templateArray['installed'] ? 'Installed' : 'Not Installed',
- $templateArray['type'],
+ $io->table(['ID', 'Title', 'Status', 'Type'], array_map(fn (TemplateData $templateData) => [
+ $templateData->id,
+ $templateData->title,
+ $templateData->installed ? 'Installed' : 'Not Installed',
+ $templateData->type,
], array_merge($templates, $customTemplates)));
return Command::SUCCESS;
diff --git a/src/Model/TemplateData.php b/src/Model/TemplateData.php
new file mode 100644
index 00000000..76de25ca
--- /dev/null
+++ b/src/Model/TemplateData.php
@@ -0,0 +1,18 @@
+setTitle($templateData['title']);
+ $template = $templateData->templateEntity;
- $metadata = $this->entityManager->getClassMetaData($template::class);
- $metadata->setIdGenerator(new AssignedGenerator());
+ if ($template === null) {
+ $template = new Template();
- $ulid = Ulid::fromString($templateData['id']);
- $template->setId($ulid);
+ $metadata = $this->entityManager->getClassMetaData($template::class);
+ $metadata->setIdGenerator(new AssignedGenerator());
+
+ $ulid = Ulid::fromString($templateData->id);
+ $template->setId($ulid);
+
+ $this->entityManager->persist($template);
+ }
+
+ $template->setTitle($templateData->title);
- $this->entityManager->persist($template);
$this->entityManager->flush();
}
@@ -99,15 +106,15 @@ public function getTemplates(iterable $finder, bool $customTemplates = false): a
$repository = $this->entityManager->getRepository(Template::class);
$template = $repository->findOneBy(['id' => Ulid::fromString($content->id)]);
- $templates[] = [
- 'id' => $content->id,
- 'title' => $content->title,
- 'adminForm' => $content->adminForm,
- 'options' => $content->options,
- 'templateEntity' => $template,
- 'installed' => $template !== null,
- 'type' => $customTemplates ? 'Custom' : 'Core',
- ];
+ $templates[] = new TemplateData(
+ $content->id,
+ $content->title,
+ $content->adminForm,
+ $content->options,
+ $template,
+ $template !== null,
+ $customTemplates ? 'Custom' : 'Core',
+ );
}
return $templates;
From 4a733275df17ebadabe3a65ee53fe31e1b5cd1ef Mon Sep 17 00:00:00 2001
From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com>
Date: Fri, 22 Aug 2025 13:32:43 +0200
Subject: [PATCH 13/21] 4565: Fixed template related tests
---
.gitignore | 6 ++--
src/Command/TemplatesInstallCommand.php | 28 ++++++++++---------
src/Command/TemplatesListCommand.php | 5 ++--
src/Command/UpdateCommand.php | 10 ++-----
src/Model/TemplateData.php | 2 ++
src/Service/TemplateService.php | 24 ++++++++--------
tests/Api/TemplatesTest.php | 6 ++--
.../RelationsChecksumListenerTest.php | 2 +-
8 files changed, 41 insertions(+), 42 deletions(-)
diff --git a/.gitignore b/.gitignore
index 27168616..21674ccc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,9 +1,7 @@
+# Ignore custom templates folder.
/assets/shared/custom-templates/*
-/repos
-
-/public/admin
-/public/client
+# Ignore release json file.
/public/release.json
###> symfony/framework-bundle ###
diff --git a/src/Command/TemplatesInstallCommand.php b/src/Command/TemplatesInstallCommand.php
index 7591a87e..c25fe129 100644
--- a/src/Command/TemplatesInstallCommand.php
+++ b/src/Command/TemplatesInstallCommand.php
@@ -6,7 +6,6 @@
use App\Model\TemplateData;
use App\Service\TemplateService;
-use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
@@ -29,8 +28,9 @@ public function __construct(
protected function configure(): void
{
- $this->addOption('all', 'a', InputOption::VALUE_NONE, "Install all available templates");
- $this->addArgument('templateUlid', InputArgument::OPTIONAL, "Install the template with the given ULID");
+ $this->addOption('all', 'a', InputOption::VALUE_NONE, 'Install all available templates');
+ $this->addOption('update', 'u', InputOption::VALUE_NONE, 'Update already installed templates');
+ $this->addArgument('templateUlid', InputArgument::OPTIONAL, 'Install the template with the given ULID');
}
final protected function execute(InputInterface $input, OutputInterface $output): int
@@ -38,38 +38,40 @@ final protected function execute(InputInterface $input, OutputInterface $output)
$io = new SymfonyStyle($input, $output);
$all = $input->getOption('all');
+ $update = $input->getOption('update');
$templates = $this->templateService->getAllTemplates();
if ($all) {
foreach ($templates as $templateToInstall) {
- $this->templateService->installTemplate($templateToInstall);
+ $this->templateService->installTemplate($templateToInstall, $update);
}
- $io->success("Installed all available templates");
+ $io->success('Installed all available templates');
+
return Command::SUCCESS;
}
$templateUlid = $input->getArgument('templateUlid');
- if (!$templateUlid) {
- $io->warning("Template ULID not supplied.");
+ if (null === $templateUlid) {
+ $io->warning('Template ULID not supplied.');
+
return Command::INVALID;
}
- $templatesFound = array_find($templates, function (TemplateData $templateData) use ($templateUlid): bool {
- return $templateData->id === $templateUlid;
- });
+ $templatesFound = array_find($templates, fn (TemplateData $templateData): bool => $templateData->id === $templateUlid);
+
+ if (1 !== count($templatesFound)) {
+ $io->error('Template not found.');
- if (count($templatesFound) !== 1) {
- $io->error("Template not found.");
return Command::FAILURE;
}
$templateToInstall = $templatesFound[0];
$this->templateService->installTemplate($templateToInstall);
- $io->success("Template " .$templateToInstall->title . " installed");
+ $io->success('Template '.$templateToInstall->title.' installed');
return Command::SUCCESS;
}
diff --git a/src/Command/TemplatesListCommand.php b/src/Command/TemplatesListCommand.php
index 2a589af6..4466c9d2 100644
--- a/src/Command/TemplatesListCommand.php
+++ b/src/Command/TemplatesListCommand.php
@@ -31,8 +31,8 @@ final protected function execute(InputInterface $input, OutputInterface $output)
try {
$templates = $this->templateService->getCoreTemplates();
- if (count($templates) === 0) {
- $io->error("No core templates found.");
+ if (0 === count($templates)) {
+ $io->error('No core templates found.');
return Command::INVALID;
}
@@ -53,5 +53,4 @@ final protected function execute(InputInterface $input, OutputInterface $output)
return Command::INVALID;
}
}
-
}
diff --git a/src/Command/UpdateCommand.php b/src/Command/UpdateCommand.php
index 0f6514d3..9e85424c 100644
--- a/src/Command/UpdateCommand.php
+++ b/src/Command/UpdateCommand.php
@@ -6,11 +6,7 @@
class UpdateCommand
{
- // Sanity checks? Er migrations kørt?
- // Kør denne efter hver update.
- // Loop json files
- // On not installed link to install command.
- // Ingen templates = clean install.
- // Vil du installere alle?
- // --force
+ // TODO: Test that migrations have been run.
+ // TODO: Run test of status for templates. No templates = clean install. Install all?
+ // TODO: Update existing templates.
}
diff --git a/src/Model/TemplateData.php b/src/Model/TemplateData.php
index 76de25ca..6b01484f 100644
--- a/src/Model/TemplateData.php
+++ b/src/Model/TemplateData.php
@@ -1,5 +1,7 @@
templateEntity;
- if ($template === null) {
+ if (null === $template) {
$template = new Template();
$metadata = $this->entityManager->getClassMetaData($template::class);
@@ -36,7 +36,9 @@ public function installTemplate(TemplateData $templateData): void
$this->entityManager->persist($template);
}
- $template->setTitle($templateData->title);
+ if ($update) {
+ $template->setTitle($templateData->title);
+ }
$this->entityManager->flush();
}
@@ -87,20 +89,20 @@ public function getTemplates(iterable $finder, bool $customTemplates = false): a
$validator = new Validator(new Factory($schemaStorage));
foreach ($finder as $file) {
- $content = json_decode($file->getContents());
+ $content = json_decode((string) $file->getContents());
$validator->validate($content, $jsonSchemaObject);
if (!$validator->isValid()) {
- $message = "JSON file " . $file->getFilename() . " does not validate. Violations:\n";
+ $message = 'JSON file '.$file->getFilename()." does not validate. Violations:\n";
foreach ($validator->getErrors() as $error) {
$message .= sprintf("\n[%s] %s", $error['property'], $error['message']);
}
- throw new Exception($message);
+ throw new \Exception($message);
}
if (!Ulid::isValid($content->id)) {
- throw new Exception('The Ulid is not valid');
+ throw new \Exception('The Ulid is not valid');
}
$repository = $this->entityManager->getRepository(Template::class);
@@ -112,7 +114,7 @@ public function getTemplates(iterable $finder, bool $customTemplates = false): a
$content->adminForm,
$content->options,
$template,
- $template !== null,
+ null !== $template,
$customTemplates ? 'Custom' : 'Core',
);
}
diff --git a/tests/Api/TemplatesTest.php b/tests/Api/TemplatesTest.php
index a1318c06..46a1cf34 100644
--- a/tests/Api/TemplatesTest.php
+++ b/tests/Api/TemplatesTest.php
@@ -19,14 +19,14 @@ public function testGetCollection(): void
'@context' => '/contexts/Template',
'@id' => '/v2/templates',
'@type' => 'hydra:Collection',
- 'hydra:totalItems' => 2,
+ 'hydra:totalItems' => 14,
'hydra:view' => [
- '@id' => '/v2/templates?itemsPerPage=5',
+ '@id' => '/v2/templates?itemsPerPage=5&page=1',
'@type' => 'hydra:PartialCollectionView',
],
]);
- $this->assertCount(2, $response->toArray()['hydra:member']);
+ $this->assertCount(5, $response->toArray()['hydra:member']);
// @TODO: resources: Object value found, but an array is required. In JSON it's an object but in the entity
// it's an key array? So this test will fail.
diff --git a/tests/EventListener/RelationsChecksumListenerTest.php b/tests/EventListener/RelationsChecksumListenerTest.php
index c94aa615..56da30fe 100644
--- a/tests/EventListener/RelationsChecksumListenerTest.php
+++ b/tests/EventListener/RelationsChecksumListenerTest.php
@@ -175,7 +175,7 @@ public function testPersistSlide(): void
$media = $this->em->getRepository(Tenant\Media::class)->findOneBy(['tenant' => $tenant]);
$feedSource = $this->em->getRepository(Tenant\FeedSource::class)->findOneBy(['tenant' => $tenant]);
$theme = $this->em->getRepository(Tenant\Theme::class)->findOneBy(['tenant' => $tenant]);
- $template = $this->em->getRepository(Template::class)->findOneBy(['title' => 'template_image_text']);
+ $template = $this->em->getRepository(Template::class)->findOneBy(['title' => 'Billede og tekst']);
$feed = new Tenant\Feed();
$feed->setTenant($tenant);
From 318dceb8123714cfd7039ab7208a5e2864f497f3 Mon Sep 17 00:00:00 2001
From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com>
Date: Fri, 22 Aug 2025 14:02:00 +0200
Subject: [PATCH 14/21] 4565: Updated API spec
---
CHANGELOG.md | 1 +
assets/shared/slide-utils/templates.js | 31 +++++++++-------
public/api-spec-v2.json | 50 --------------------------
public/api-spec-v2.yaml | 36 -------------------
4 files changed, 19 insertions(+), 99 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3e9902fb..31a70fb9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ All notable changes to this project will be documented in this file.
* Cleaned up Github Actions workflows.
* Updated PHP dependencies.
* Added Playwright github action.
+* Changed how templates are imported.
### NB! Prior to 3.x the project was split into separate repositories
diff --git a/assets/shared/slide-utils/templates.js b/assets/shared/slide-utils/templates.js
index 7241594a..2a52c9fb 100644
--- a/assets/shared/slide-utils/templates.js
+++ b/assets/shared/slide-utils/templates.js
@@ -1,13 +1,17 @@
// Load templates.
// @see https://vite.dev/guide/features.html#glob-import
// @see docs/custom-templates.md
-const templateModules = import.meta.glob('../templates/*.jsx', { eager: true })
-const customTemplatesModules = import.meta.glob('../custom-templates/*.jsx', { eager: true })
+const templateModules = import.meta.glob("../templates/*.jsx", { eager: true });
+const customTemplatesModules = import.meta.glob("../custom-templates/*.jsx", {
+ eager: true,
+});
function duckTypingTemplateModule(module) {
- return typeof(module.id) === "function" &&
- typeof(module.config) === "function" &&
- typeof(module.renderSlide) === "function";
+ return (
+ typeof module.id === "function" &&
+ typeof module.config === "function" &&
+ typeof module.renderSlide === "function"
+ );
}
function findModule(modules, templateUlid) {
@@ -19,7 +23,9 @@ function findModule(modules, templateUlid) {
return module;
}
} else {
- throw new Error("Template should implement functions: id(), config(), renderSlide(slide, run, slideDone)");
+ throw new Error(
+ "Template should implement functions: id(), config(), renderSlide(slide, run, slideDone)",
+ );
}
}
@@ -31,8 +37,10 @@ function getTemplateModule(templateUlid) {
return null;
}
- const module = findModule(templateModules, templateUlid) ??
- findModule(customTemplatesModules, templateUlid) ?? null;
+ const module =
+ findModule(templateModules, templateUlid) ??
+ findModule(customTemplatesModules, templateUlid) ??
+ null;
if (module === null) {
throw new Error(`Cannot find module '${templateUlid}'`);
@@ -64,13 +72,10 @@ function renderSlide(slide, run, slideDone) {
const module = getTemplateModule(templateUlid);
if (!module) {
- return '';
+ return "";
}
return module.renderSlide(slide, run, slideDone);
}
-export {
- getConfig,
- renderSlide
-}
+export { getConfig, renderSlide };
diff --git a/public/api-spec-v2.json b/public/api-spec-v2.json
index 9439d206..0a05b36e 100644
--- a/public/api-spec-v2.json
+++ b/public/api-spec-v2.json
@@ -5878,20 +5878,6 @@
"explode": false,
"allowReserved": false
},
- {
- "name": "description",
- "in": "query",
- "description": "",
- "required": false,
- "deprecated": false,
- "allowEmptyValue": false,
- "schema": {
- "type": "string"
- },
- "style": "form",
- "explode": false,
- "allowReserved": false
- },
{
"name": "createdBy",
"in": "query",
@@ -14649,15 +14635,6 @@
"title": {
"type": "string"
},
- "description": {
- "type": "string"
- },
- "resources": {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
"modifiedBy": {
"type": "string"
},
@@ -14686,15 +14663,6 @@
"title": {
"type": "string"
},
- "description": {
- "type": "string"
- },
- "resources": {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
"modifiedBy": {
"type": "string"
},
@@ -14758,15 +14726,6 @@
"title": {
"type": "string"
},
- "description": {
- "type": "string"
- },
- "resources": {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
"modifiedBy": {
"type": "string"
},
@@ -14830,15 +14789,6 @@
"title": {
"type": "string"
},
- "description": {
- "type": "string"
- },
- "resources": {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
"modifiedBy": {
"type": "string"
},
diff --git a/public/api-spec-v2.yaml b/public/api-spec-v2.yaml
index 0391c09e..687d694a 100644
--- a/public/api-spec-v2.yaml
+++ b/public/api-spec-v2.yaml
@@ -4164,18 +4164,6 @@ paths:
style: form
explode: false
allowReserved: false
- -
- name: description
- in: query
- description: ''
- required: false
- deprecated: false
- allowEmptyValue: false
- schema:
- type: string
- style: form
- explode: false
- allowReserved: false
-
name: createdBy
in: query
@@ -10214,12 +10202,6 @@ components:
properties:
title:
type: string
- description:
- type: string
- resources:
- type: array
- items:
- type: string
modifiedBy:
type: string
createdBy:
@@ -10240,12 +10222,6 @@ components:
properties:
title:
type: string
- description:
- type: string
- resources:
- type: array
- items:
- type: string
modifiedBy:
type: string
createdBy:
@@ -10289,12 +10265,6 @@ components:
additionalProperties: true
title:
type: string
- description:
- type: string
- resources:
- type: array
- items:
- type: string
modifiedBy:
type: string
createdBy:
@@ -10338,12 +10308,6 @@ components:
type: string
title:
type: string
- description:
- type: string
- resources:
- type: array
- items:
- type: string
modifiedBy:
type: string
createdBy:
From d4f279920051f5a0d9614fd0dc94777d935e9424 Mon Sep 17 00:00:00 2001
From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com>
Date: Mon, 25 Aug 2025 12:22:01 +0200
Subject: [PATCH 15/21] Update docs/custom-templates.md
Co-authored-by: Sine Jespersen
---
docs/custom-templates.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/custom-templates.md b/docs/custom-templates.md
index cd1ed409..39019fd2 100644
--- a/docs/custom-templates.md
+++ b/docs/custom-templates.md
@@ -4,7 +4,7 @@ It is possible to include your own templates in your installation.
## Location
-Custom templates should be placed in the folder assets/shared/custom-templates/.
+Custom templates should be placed in the folder `assets/shared/custom-templates/`.
This folder is in .gitignore so the contents will not be added to the git repository.
How you populate this folder with your custom templates is up to you:
From 2b13acc759203a8c4530c932d52d8ff2aa73ee1b Mon Sep 17 00:00:00 2001
From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com>
Date: Mon, 25 Aug 2025 12:22:13 +0200
Subject: [PATCH 16/21] Update docs/custom-templates.md
Co-authored-by: Sine Jespersen
---
docs/custom-templates.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/custom-templates.md b/docs/custom-templates.md
index 39019fd2..8575e031 100644
--- a/docs/custom-templates.md
+++ b/docs/custom-templates.md
@@ -5,7 +5,7 @@ It is possible to include your own templates in your installation.
## Location
Custom templates should be placed in the folder `assets/shared/custom-templates/`.
-This folder is in .gitignore so the contents will not be added to the git repository.
+This folder is in `.gitignore` so the contents will not be added to the git repository.
How you populate this folder with your custom templates is up to you:
From 604227f85728ed069c028ff40b6a6d91910cc50e Mon Sep 17 00:00:00 2001
From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com>
Date: Mon, 25 Aug 2025 12:22:24 +0200
Subject: [PATCH 17/21] Update docs/custom-templates.md
Co-authored-by: Sine Jespersen
---
docs/custom-templates.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/custom-templates.md b/docs/custom-templates.md
index 8575e031..2883b1dd 100644
--- a/docs/custom-templates.md
+++ b/docs/custom-templates.md
@@ -18,7 +18,7 @@ How you populate this folder with your custom templates is up to you:
The following files are required for a custom template:
-* custom-template-name.jsx - A javascript module for the template.
+* `custom-template-name.jsx` - A javascript module for the template.
* custom-template-name.json - A configuration file for the template.
Replace `custom-template-name` with a unique name for the template.
From d16d637c079329966f0a693233947efed7b0264a Mon Sep 17 00:00:00 2001
From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com>
Date: Mon, 25 Aug 2025 12:26:14 +0200
Subject: [PATCH 18/21] Update docs/custom-templates.md
Co-authored-by: Sine Jespersen
---
docs/custom-templates.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/custom-templates.md b/docs/custom-templates.md
index 2883b1dd..7e2d8c1f 100644
--- a/docs/custom-templates.md
+++ b/docs/custom-templates.md
@@ -19,7 +19,7 @@ How you populate this folder with your custom templates is up to you:
The following files are required for a custom template:
* `custom-template-name.jsx` - A javascript module for the template.
-* custom-template-name.json - A configuration file for the template.
+* `custom-template-name.json` - A configuration file for the template.
Replace `custom-template-name` with a unique name for the template.
From e2ffe83f35c818ab51f7fbd8957585f01544d8b4 Mon Sep 17 00:00:00 2001
From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com>
Date: Mon, 25 Aug 2025 12:26:36 +0200
Subject: [PATCH 19/21] Update docs/custom-templates.md
Co-authored-by: Sine Jespersen
---
docs/custom-templates.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/custom-templates.md b/docs/custom-templates.md
index 7e2d8c1f..5d5fbf03 100644
--- a/docs/custom-templates.md
+++ b/docs/custom-templates.md
@@ -36,7 +36,7 @@ For an example of a custom template see `assets/shared/custom-templates-example/
## Contributing template
-If you think the template could be used by other, consider contributing the template to the project as a Pull Request.
+If you think the template could be used by other, consider contributing the template to the project as a pull request.
### Guide for contributing template
From f993104602b4cac5f111db4f6dd47219e404426b Mon Sep 17 00:00:00 2001
From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com>
Date: Tue, 26 Aug 2025 07:11:08 +0200
Subject: [PATCH 20/21] 4565: Fixed issues raised in review
---
assets/admin/app.jsx | 4 ++--
assets/admin/components/auth-handler.jsx | 4 ++--
assets/admin/components/restricted-route.jsx | 4 ++--
assets/admin/components/slide/preview/slide-preview.jsx | 2 +-
assets/admin/components/slide/slide-form.jsx | 4 +++-
5 files changed, 10 insertions(+), 8 deletions(-)
diff --git a/assets/admin/app.jsx b/assets/admin/app.jsx
index ad96b49d..bca4ee6a 100644
--- a/assets/admin/app.jsx
+++ b/assets/admin/app.jsx
@@ -7,7 +7,7 @@ import Container from "react-bootstrap/Container";
import Row from "react-bootstrap/Row";
import Col from "react-bootstrap/Col";
import localStorageKeys from "./components/util/local-storage-keys";
-import RestrictedRoute from "./components/restricted-route.jsx";
+import RestrictedRoute from "./components/restricted-route";
import Topbar from "./components/navigation/topbar/top-bar";
import SideBar from "./components/navigation/sidebar/sidebar";
import ScreenList from "./components/screen/screen-list";
@@ -32,7 +32,7 @@ import UserContext from "./context/user-context";
import ListContext from "./context/list-context";
import SharedPlaylists from "./components/playlist/shared-playlists";
import Logout from "./components/user/logout";
-import AuthHandler from "./components/auth-handler.jsx";
+import AuthHandler from "./components/auth-handler";
import LoadingComponent from "./components/util/loading-component/loading-component";
import ModalProvider from "./context/modal-context/modal-provider";
import UsersList from "./components/users/users-list";
diff --git a/assets/admin/components/auth-handler.jsx b/assets/admin/components/auth-handler.jsx
index cc3d86d3..85ac6027 100644
--- a/assets/admin/components/auth-handler.jsx
+++ b/assets/admin/components/auth-handler.jsx
@@ -1,7 +1,7 @@
import { React, useContext } from "react";
import PropTypes from "prop-types";
-import Login from "./user/login.jsx";
-import UserContext from "../context/user-context.jsx";
+import Login from "./user/login";
+import UserContext from "../context/user-context";
/**
* The auth handler wrapper.
diff --git a/assets/admin/components/restricted-route.jsx b/assets/admin/components/restricted-route.jsx
index f1e6b6bc..c6b19ec3 100644
--- a/assets/admin/components/restricted-route.jsx
+++ b/assets/admin/components/restricted-route.jsx
@@ -1,7 +1,7 @@
import { React, useContext } from "react";
import PropTypes from "prop-types";
-import UserContext from "../context/user-context.jsx";
-import NoAccess from "./no-access/no-access.jsx";
+import UserContext from "../context/user-context";
+import NoAccess from "./no-access/no-access";
/**
* The restricted route wrapper.
diff --git a/assets/admin/components/slide/preview/slide-preview.jsx b/assets/admin/components/slide/preview/slide-preview.jsx
index 5c67b9b3..afd65adc 100644
--- a/assets/admin/components/slide/preview/slide-preview.jsx
+++ b/assets/admin/components/slide/preview/slide-preview.jsx
@@ -2,7 +2,7 @@ import { React, useEffect, useState } from "react";
import { Button } from "react-bootstrap";
import PropTypes from "prop-types";
import { useTranslation } from "react-i18next";
-import ErrorBoundary from "../../error-boundary.jsx";
+import ErrorBoundary from "../../error-boundary";
import "./slide-preview.scss";
import { renderSlide } from "../../../../shared/slide-utils/templates";
diff --git a/assets/admin/components/slide/slide-form.jsx b/assets/admin/components/slide/slide-form.jsx
index e9226103..e226b706 100644
--- a/assets/admin/components/slide/slide-form.jsx
+++ b/assets/admin/components/slide/slide-form.jsx
@@ -16,7 +16,7 @@ import idFromUrl from "../util/helpers/id-from-url";
import FormInput from "../util/forms/form-input";
import ContentForm from "./content/content-form";
import LoadingComponent from "../util/loading-component/loading-component";
-import SlidePreview from "./preview/slide-preview.jsx";
+import SlidePreview from "./preview/slide-preview";
import FeedSelector from "./content/feed-selector";
import SelectPlaylistsTable from "../util/multi-and-table/select-playlists-table";
import localStorageKeys from "../util/local-storage-keys";
@@ -161,8 +161,10 @@ function SlideForm({
if (selectedTemplate) {
const slideConfig = getConfig(selectedTemplate['id']);
setContentFormElements(slideConfig.adminForm ?? []);
+ setDisableLivePreview(slideConfig?.options?.disableLivePreview ?? false);
newSelectedTemplates.push(selectedTemplate);
}
+
setSelectedTemplates(newSelectedTemplates);
}, [selectedTemplate]);
From 10c3b5834df03a636335871d23c3f33f96fee5da Mon Sep 17 00:00:00 2001
From: Troels Ugilt Jensen <6103205+tuj@users.noreply.github.com>
Date: Tue, 26 Aug 2025 08:10:56 +0200
Subject: [PATCH 21/21] 4565: Added comment
---
assets/shared/slide-utils/templates.js | 1 +
1 file changed, 1 insertion(+)
diff --git a/assets/shared/slide-utils/templates.js b/assets/shared/slide-utils/templates.js
index 2a52c9fb..793168ea 100644
--- a/assets/shared/slide-utils/templates.js
+++ b/assets/shared/slide-utils/templates.js
@@ -1,6 +1,7 @@
// Load templates.
// @see https://vite.dev/guide/features.html#glob-import
// @see docs/custom-templates.md
+// Eager loading because no other code piece imports the templates otherwise.
const templateModules = import.meta.glob("../templates/*.jsx", { eager: true });
const customTemplatesModules = import.meta.glob("../custom-templates/*.jsx", {
eager: true,