diff --git a/.github/workflows/playwright.yaml b/.github/workflows/playwright.yaml index 5b78012d..52be0281 100644 --- a/.github/workflows/playwright.yaml +++ b/.github/workflows/playwright.yaml @@ -33,7 +33,7 @@ jobs: - name: Run playwright env: - CI: "true" + CI: true run: | docker compose run --rm playwright npx playwright install --with-deps docker compose run --rm playwright npx playwright test diff --git a/README.md b/README.md index dcd91eb9..c8c05e19 100644 --- a/README.md +++ b/README.md @@ -36,8 +36,8 @@ At the core of OS2Display is an API that clients communicate with. All data runs It includes an Admin for creating content and a Client for displaying the content. The structure is that slides are the content element of the system. Each slide is based on a Template with content -added. The slides are gathered into playlists. Playlists are then added to screens. -A screen is the connection between a physical device and the content. +added. The slides are gathered into playlists. Playlists are then added to screens. A screen is the connection between a +physical device and the content. ```mermaid flowchart LR @@ -51,7 +51,7 @@ Further documentation can be found in the ## Content Structure | Component | Description | Accessible by | -|-----------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------------| +| --------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------ | | Slide | A slide is the visible content on a screen. | Admin, editor | | Media | Media is either images or videos used as content for slides. | Admin, editor | | Theme | A theme has css, that can override the slide css. | Admin | @@ -90,14 +90,13 @@ Architectural decisions are recorded in `docs/adr`. ## Versioning -We use [SemVer](http://semver.org/) for versioning. -For the versions available, see the -[tags on this repository](https://github.com/os2display/display-api-service/tags). +We use [SemVer](http://semver.org/) for versioning. For the versions available, see the [tags on this +repository](https://github.com/os2display/display-api-service/tags). ## Technologies -The API is written in PHP project, built with [Symfony](https://symfony.com/) and -[API Platform](https://api-platform.com/). +The API is written in PHP project, built with [Symfony](https://symfony.com/) and [API +Platform](https://api-platform.com/). The Admin and Client are written in javascript and [React](https://react.dev/) and built with [Vite](https://vite.dev/). @@ -186,13 +185,13 @@ The API is stateless except for the `/v2/authentication` routes. ## Authentication -Authentication is achieved through `/v2/authentication/token` for the `/admin` -and through `/v2/authentication/screen` for the `/client`. +Authentication is achieved through `/v2/authentication/token` for the `/admin` and through `/v2/authentication/screen` +for the `/client`. ## Tenants -Content is connected to a Tenant. A user is in x tenants. -This allows for maintaining multiple content silos in the same installation. +Content is connected to a Tenant. A user is in x tenants. This allows for maintaining multiple content silos in the same +installation. You can add a new tenant: @@ -211,16 +210,13 @@ It is also possible to configure if a tenants should support interactive slides. ## OIDC providers -At the present two possible oidc providers are implemented: 'internal' and 'external'. -These work differently. +At the present two possible oidc providers are implemented: 'internal' and 'external'. These work differently. -The internal provider is expected to handle both authentication and authorization. -Any users logging in through the internal will be granted access based on the -tenants/roles provided. +The internal provider is expected to handle both authentication and authorization. Any users logging in through the +internal will be granted access based on the tenants/roles provided. -The external provider only handles authentication. A user logging in through the -external provider will not be granted access automatically, but will be challenged -to enter an activation (invite) code to verify access. +The external provider only handles authentication. A user logging in through the external provider will not be granted +access automatically, but will be challenged to enter an activation (invite) code to verify access. See `docs/feed/openid-connect.md` for environment variables for OpenID Connect configuration. @@ -234,20 +230,17 @@ The claim keys needed are set in the env variables: - `INTERNAL_OIDC_CLAIM_EMAIL` - `INTERNAL_OIDC_CLAIM_GROUPS` -The value of the claim with the name that is defined in the env variable `INTERNAL_OIDC_CLAIM_GROUPS` is mapped to -the user's access to tenants in `App\Security\AzureOidcAuthenticator`. The claim field should consist of an array of -names that should follow the following structure ``. -`` can be `Admin` or `Redaktoer` (editor). -E.g. `Example1Admin` will map to the tenant with name `Example1` with `ROLE_ADMIN`. -If the tenant does not exist it will be created when the user logs in. +The value of the claim with the name that is defined in the env variable `INTERNAL_OIDC_CLAIM_GROUPS` is mapped to the +user's access to tenants in `App\Security\AzureOidcAuthenticator`. The claim field should consist of an array of names +that should follow the following structure ``. `` can be `Admin` or +`Redaktoer` (editor). E.g. `Example1Admin` will map to the tenant with name `Example1` with `ROLE_ADMIN`. If the tenant +does not exist it will be created when the user logs in. ### External -The external oidc provider takes only the claim defined in the env variable -OIDC_EXTERNAL_CLAIM_ID, hashes it and uses this hash as providerId for the user. -When a user logs in with this provider, it is initially not in any tenant. -To be added to a tenant the user has to use an activation code a -ROLE_EXTERNAL_USER_ADMIN has created. +The external oidc provider takes only the claim defined in the env variable OIDC_EXTERNAL_CLAIM_ID, hashes it and uses +this hash as providerId for the user. When a user logs in with this provider, it is initially not in any tenant. To be +added to a tenant the user has to use an activation code a ROLE_EXTERNAL_USER_ADMIN has created. ## JWT Auth @@ -263,8 +256,7 @@ Then create a local test user if needed: docker compose exec phpfpm bin/console app:user:add ``` -You can now obtain a token by sending a `POST` request to the -`/v2/authentication/token` endpoint: +You can now obtain a token by sending a `POST` request to the `/v2/authentication/token` endpoint: ```curl curl --location --request 'POST' \ @@ -316,9 +308,8 @@ See the `docker-compose.override.yml` playwright entry and the version imported #### Testing on the built files -This project includes a test script that handles building assets, running -Playwright tests, and stops and starts the node container. This script tests the -*built* files. This is the approach the GitHub Action uses. +This project includes a test script that handles building assets, running Playwright tests, and stops and starts the +node container. This script tests the _built_ files. This is the approach the GitHub Action uses. ```shell task test:frontend-built @@ -330,9 +321,8 @@ or ./scripts/test {TEST-PATH} ``` -TEST-PATH is optional, and is the specific test file or directory to run like -`admin`/`client`/`template` or a specific file, e.g. `admin-app.spec.js`. If -TEST-PATH is omitted, all tests will run. +TEST-PATH is optional, and is the specific test file or directory to run like `admin`/`client`/`template` or a specific +file, e.g. `admin-app.spec.js`. If TEST-PATH is omitted, all tests will run. #### Testing on local machine @@ -364,8 +354,8 @@ task generate:api-spec This will generate `public/api-spec-v2.json` and `public/api-spec-v2.yaml`. -This generated API specification is used to generate -[Redux Toolkit RTK Query](https://redux-toolkit.js.org/rtk-query/overview) code for interacting with the API. +This generated API specification is used to generate [Redux Toolkit RTK +Query](https://redux-toolkit.js.org/rtk-query/overview) code for interacting with the API. To generate the Redux Toolkit RTK Query code, run the following command: @@ -400,10 +390,11 @@ TRACK_SCREEN_INFO_UPDATE_INTERVAL_SECONDS=300 ###< App ### ``` -- APP_ACTIVATION_CODE_EXPIRE_INTERVAL: Specifies how long an external user activation code should live. - The format of the interval should follow . +- APP_ACTIVATION_CODE_EXPIRE_INTERVAL: Specifies how long an external user activation code should live. The format of + the interval should follow . **Default**: 2 days. + - APP_KEY_VAULT_SOURCE: Source of key-value pair for `src/Service/KeyVaultService`. Atm. "ENVIRONMENT" is the only option. - APP_KEY_VAULT_JSON: A json object formatted as a string. Contains key-value pairs that can be accessed by through @@ -427,27 +418,30 @@ ADMIN_ENHANCED_PREVIEW=false ###< Admin configuration ### ``` -- ADMIN_REJSEPLANEN_APIKEY: An API key accessing Rejseplanen API used for Travel template. - See [https://labs.rejseplanen.dk/](https://labs.rejseplanen.dk/) for information about acquiring an API key. +- ADMIN_REJSEPLANEN_APIKEY: An API key accessing Rejseplanen API used for Travel template. See + [https://labs.rejseplanen.dk/](https://labs.rejseplanen.dk/) for information about acquiring an API key. **Default**: Not set. + - ADMIN_SHOW_SCREEN_STATUS: Should the status of the screen be shown in the Admin (true|false)? **Default**: Disabled. -- ADMIN_TOUCH_BUTTON_REGIONS: Should the option of setting a button name for a slide be enabled in the Admin? - This option is used by the Client if a region is configured to be a "touch-buttons" region. + +- ADMIN_TOUCH_BUTTON_REGIONS: Should the option of setting a button name for a slide be enabled in the Admin? This + option is used by the Client if a region is configured to be a "touch-buttons" region. **Default**: Disabled. + - ADMIN_LOGIN_METHODS: Which login methods should be displayed in the admin (array of objects as json string)? Available types: "oidc" | "username-password". ```json { - "type": "oidc", - "provider": "internal", - "label": "Button text", - "icon": "faCity" + "type": "oidc", + "provider": "internal", + "label": "Button text", + "icon": "faCity" } ``` @@ -456,21 +450,22 @@ ADMIN_ENHANCED_PREVIEW=false - icon: Name of the fontawesome icon to use for the button or "mitID" for MitID logo. ```json - { - "type": "username-password", - "provider": "username-password", - "label": "" - } + { + "type": "username-password", + "provider": "username-password", + "label": "" + } ``` - provider: "username-password" - label: Label for the username password login section **Default**: Username and password login option is enabled. + - ADMIN_ENHANCED_PREVIEW: Should the enhanced preview mode be active (true|false)? When enabled, previews will be - handled by iFraming in the Client app. This will allow the option of previewing playlists and screens. - If disabled, only slides can be previewed. This will be with the "live" method. This preview is not as precise. - See [Preview mode in the Client](#preview-mode-in-the-client). + handled by iFraming in the Client app. This will allow the option of previewing playlists and screens. If disabled, + only slides can be previewed. This will be with the "live" method. This preview is not as precise. See [Preview mode + in the Client](#preview-mode-in-the-client). **Default**: Disabled. @@ -494,26 +489,31 @@ CLIENT_DEBUG=false waiting for being activated in the administration. **Default**: 20 s. + - CLIENT_REFRESH_TOKEN_TIMEOUT: How often (milliseconds) should it be checked whether the token needs to be refreshed? **Default**: 30 s. + - CLIENT_REFRESH_TOKEN_TIMEOUT: How often (milliseconds) should it be checked whether the token needs to be refreshed? **Default**: 60 s. + - CLIENT_SCHEDULING_INTERVAL: How often (milliseconds) should the scheduling be run for the logged in screen? **Default**: 60 s. + - CLIENT_PULL_STRATEGY_INTERVAL: How often (milliseconds) should data be pulled from the API? **Default**: 1 m. and 30 s. -- CLIENT_COLOR_SCHEME: Which colour scheme should be enabled? Should be a json object as string. - This is used to signal how changes to darkmode are handled. - Options are: + +- CLIENT_COLOR_SCHEME: Which colour scheme should be enabled? Should be a json object as string. This is used to signal + how changes to darkmode are handled. Options are: - Not set - will use the browsers prefers-color-scheme setting. - '{"type":"library","lat":56.0,"lng":10.0}' - In this case the change to darkmode is handled with a library that activates darkmode according to sunrise/sunset of the location given by the longitude/latitude (lat/lng). **Default**: Library mode with a lat/lng set in Denmark. + - CLIENT_DEBUG: Should the Client be in debug mode (true|false). When not in debug mode the mouse pointer is hidden. **Default**: Disabled. @@ -591,10 +591,10 @@ classDiagram ## Online check for Client -If the client does not have internet when starting, it cannot load the assets needed for the Client. -The `public/client/online-check` has been added to handle this. -The folder contains an `index.html`, that checks connectivity before redirecting to `/client`. -If this index.html is cached in the browser the online check page can load without internet. +If the client does not have internet when starting, it cannot load the assets needed for the Client. The +`public/client/online-check` has been added to handle this. The folder contains an `index.html`, that checks +connectivity before redirecting to `/client`. If this index.html is cached in the browser the online check page can load +without internet. To use this, set the starting path of the Client to `/client/online-check`. @@ -627,8 +627,8 @@ This feature is used in the Admin for displaying previews of slides, playlists a ## Screen status -Screen status consists of 2 elements. Tracking latest request from a screen client. -This data is collected and exposed through the API. +Screen status consists of 2 elements. Tracking latest request from a screen client. This data is collected and exposed +through the API. The other part is in the admin where the data can be exposed to the user. @@ -648,20 +648,20 @@ ADMIN_SHOW_SCREEN_STATUS=true In the list view of screens, there is a column called "Status". -This column shows the status of the connection of a "screen" in the administration and an -actual "machine" running the screen data. +This column shows the status of the connection of a "screen" in the administration and an actual "machine" running the +screen data. This status can be: - "+ Tilkobl": The screen is not connected to a machine. -- ✓ (green): The machine is connected and running the latest code. +- ✓ (green): The machine is connected and running the latest code. - i (yellow circle): The machine is not running the newest released code. - ! (red triangle): The machine has not called the API within the last hour or the access token is expired. ### Screen edit view -In the screen edit view, the "Tilkobling" section shows the status of the connection between the -screen entity and a machine running the screen data. +In the screen edit view, the "Tilkobling" section shows the status of the connection between the screen entity and a +machine running the screen data. The status can be: @@ -677,8 +677,8 @@ Furthermore, the section "Tilkobling" will show the following data: * Kodeudgivelsestidspunkt: 17/6 2024 17:26 ``` -This shows when the latest communication has occured, what client version the machine is running, -and the time of client code release. +This shows when the latest communication has occured, what client version the machine is running, and the time of client +code release. ## Feeds @@ -791,8 +791,8 @@ It is possible to include custom 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. +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: @@ -824,11 +824,37 @@ The `.jsx` should expose the following functions: For an example of a custom template see `assets/shared/custom-templates-example/`. -The slide is responsible for signaling that it is done executing. -This is done by calling the slideDone() function. If the slide should just run for X milliseconds then you can use the -BaseSlideExecution class to handle this. See the example for this approach. +The slide is responsible for signaling that it is done executing. This is done by calling the slideDone() function. If +the slide should just run for X milliseconds then you can use the BaseSlideExecution class to handle this. See the +example for this approach. + +#### custom-template-name.json + +The `.json` should include: + +```json +{ + "title": "Billede og tekst", + "id": "01FP2SNGFN0BZQH03KCBXHKYHG", // ULID, https://ulidgenerator.com/ + "options": {} // Optional, can contain extra options such as `disableLivePreview` that disables live preview in the admin UI + "adminForm": {} // Optional, described below +} +``` + +##### Admin Form not set in json + +It is possible to create an interactive admin form in a `.jsx`-file. As described above with renderSlide, when a +template has a custom `.jsx` admin form, it needs to implement a `renderAdminForm` function in the base file. + +- renderAdminForm(formStateObject, onChange, handleMedia, mediaData) - Should return the JSX for the admin form. + - slideContent: The slide data object from the admin, could e.g. contain a title `slideContent["title"]` + - onSlideContentChange: A callback for changes on the slide content. + - handleMedia: A function that handles saving media (only necessary if the slide has media) + - mediaData: An object that can contain already saved media (only necessary if the slide has media) + +For an example of a custom template see `assets/shared/custom-templates-example/`. -##### Admin Form +##### Admin Form set in json To get content into the slide the config.adminForm field should be set. This should be an array of objects with the following attributes: @@ -874,8 +900,7 @@ If you think the template could be used by other, consider contributing the temp ## Screen Layouts -A screen layout is a setting that defines how a screen is divided into different regions. -A layout consists of a grid. +A screen layout is a setting that defines how a screen is divided into different regions. A layout consists of a grid. The grid regions are created from the number of rows and columns selected for the given layout. The regions are named diff --git a/assets/admin/components/activation-code/activation-code-activate.jsx b/assets/admin/components/activation-code/activation-code-activate.jsx index 711aabbb..6b822c31 100644 --- a/assets/admin/components/activation-code/activation-code-activate.jsx +++ b/assets/admin/components/activation-code/activation-code-activate.jsx @@ -10,7 +10,7 @@ import { } from "../util/list/toast-component/display-toast"; import LoadingComponent from "../util/loading-component/loading-component"; import ContentBody from "../util/content-body/content-body"; -import FormInput from "../util/forms/form-input"; +import FormInput from "../../../shared/forms/form-input.jsx"; import ContentFooter from "../util/content-footer/content-footer"; /** diff --git a/assets/admin/components/activation-code/activation-code-form.jsx b/assets/admin/components/activation-code/activation-code-form.jsx index 6dab9bfa..92f66e7e 100644 --- a/assets/admin/components/activation-code/activation-code-form.jsx +++ b/assets/admin/components/activation-code/activation-code-form.jsx @@ -4,8 +4,8 @@ import { useTranslation } from "react-i18next"; import Form from "react-bootstrap/Form"; import LoadingComponent from "../util/loading-component/loading-component"; import ContentBody from "../util/content-body/content-body"; -import FormInput from "../util/forms/form-input"; -import RadioButtons from "../util/forms/radio-buttons"; +import FormInput from "../../../shared/forms/form-input"; +import RadioButtons from "../../../shared/forms/radio-buttons"; import StickyFooter from "../util/sticky-footer"; /** diff --git a/assets/admin/components/feed-sources/feed-source-form.jsx b/assets/admin/components/feed-sources/feed-source-form.jsx index bf9c0db2..6b876fdd 100644 --- a/assets/admin/components/feed-sources/feed-source-form.jsx +++ b/assets/admin/components/feed-sources/feed-source-form.jsx @@ -3,10 +3,10 @@ import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import Form from "react-bootstrap/Form"; import LoadingComponent from "../util/loading-component/loading-component"; -import FormInputArea from "../util/forms/form-input-area"; -import FormSelect from "../util/forms/select"; +import FormInputArea from "../../../shared/forms/form-input-area"; +import FormSelect from "../../../shared/forms/select"; import ContentBody from "../util/content-body/content-body"; -import FormInput from "../util/forms/form-input"; +import FormInput from "../../../shared/forms/form-input"; import CalendarApiFeedType from "./templates/calendar-api-feed-type"; import NotifiedFeedType from "./templates/notified-feed-type"; import EventDatabaseApiFeedType from "./templates/event-database-feed-type"; diff --git a/assets/admin/components/feed-sources/templates/colibo-feed-type.jsx b/assets/admin/components/feed-sources/templates/colibo-feed-type.jsx index 36abcf91..8ec81611 100644 --- a/assets/admin/components/feed-sources/templates/colibo-feed-type.jsx +++ b/assets/admin/components/feed-sources/templates/colibo-feed-type.jsx @@ -2,7 +2,7 @@ import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { Alert } from "react-bootstrap"; import MultiselectFromEndpoint from "../../slide/content/multiselect-from-endpoint"; -import FormInput from "../../util/forms/form-input"; +import FormInput from "../../../../shared/forms/form-input"; const ColiboFeedType = ({ feedSourceId, diff --git a/assets/admin/components/feed-sources/templates/event-database-feed-type.jsx b/assets/admin/components/feed-sources/templates/event-database-feed-type.jsx index 56834272..1572062c 100644 --- a/assets/admin/components/feed-sources/templates/event-database-feed-type.jsx +++ b/assets/admin/components/feed-sources/templates/event-database-feed-type.jsx @@ -1,5 +1,5 @@ import { useTranslation } from "react-i18next"; -import FormInput from "../../util/forms/form-input"; +import FormInput from "../../../../shared/forms/form-input"; const EventDatabaseApiTemplate = ({ handleInput, formStateObject, mode }) => { const { t } = useTranslation("common", { diff --git a/assets/admin/components/feed-sources/templates/event-database-v2-feed-type.jsx b/assets/admin/components/feed-sources/templates/event-database-v2-feed-type.jsx index 10e09dc6..865bfeb0 100644 --- a/assets/admin/components/feed-sources/templates/event-database-v2-feed-type.jsx +++ b/assets/admin/components/feed-sources/templates/event-database-v2-feed-type.jsx @@ -1,5 +1,5 @@ import { useTranslation } from "react-i18next"; -import FormInput from "../../util/forms/form-input"; +import FormInput from "../../../../shared/forms/form-input"; const EventDatabaseApiV2FeedType = ({ handleInput, formStateObject, mode }) => { const { t } = useTranslation("common", { diff --git a/assets/admin/components/feed-sources/templates/notified-feed-type.jsx b/assets/admin/components/feed-sources/templates/notified-feed-type.jsx index c1abec00..688a53f8 100644 --- a/assets/admin/components/feed-sources/templates/notified-feed-type.jsx +++ b/assets/admin/components/feed-sources/templates/notified-feed-type.jsx @@ -1,5 +1,5 @@ import { useTranslation } from "react-i18next"; -import FormInput from "../../util/forms/form-input"; +import FormInput from "../../../../shared/forms/form-input"; const NotifiedFeedType = ({ handleInput, formStateObject, mode }) => { const { t } = useTranslation("common", { diff --git a/assets/admin/components/groups/group-form.jsx b/assets/admin/components/groups/group-form.jsx index b1889c5b..c49bf211 100644 --- a/assets/admin/components/groups/group-form.jsx +++ b/assets/admin/components/groups/group-form.jsx @@ -5,7 +5,7 @@ import Form from "react-bootstrap/Form"; import LoadingComponent from "../util/loading-component/loading-component"; import ContentBody from "../util/content-body/content-body"; import ContentFooter from "../util/content-footer/content-footer"; -import FormInput from "../util/forms/form-input"; +import FormInput from "../../../shared/forms/form-input"; /** * The group form component. diff --git a/assets/admin/components/media/media-list.jsx b/assets/admin/components/media/media-list.jsx index 5cd8ed32..058f3099 100644 --- a/assets/admin/components/media/media-list.jsx +++ b/assets/admin/components/media/media-list.jsx @@ -17,7 +17,7 @@ import { useGetV2MediaQuery, useDeleteV2MediaByIdMutation, } from "../../../shared/redux/enhanced-api.ts"; -import FormCheckbox from "../util/forms/form-checkbox"; +import FormCheckbox from "../../../shared/forms/form-checkbox"; import "./media-list.scss"; /** diff --git a/assets/admin/components/playlist-drag-and-drop/playlist-drag-and-drop.jsx b/assets/admin/components/playlist-drag-and-drop/playlist-drag-and-drop.jsx index 1735e166..18b1379b 100644 --- a/assets/admin/components/playlist-drag-and-drop/playlist-drag-and-drop.jsx +++ b/assets/admin/components/playlist-drag-and-drop/playlist-drag-and-drop.jsx @@ -1,9 +1,9 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; import { SelectPlaylistColumns } from "../playlist/playlists-columns"; -import PlaylistsDropdown from "../util/forms/multiselect-dropdown/playlists/playlists-dropdown"; +import PlaylistsDropdown from "../util/multiselect-dropdown/playlists/playlists-dropdown"; import DragAndDropTable from "../util/drag-and-drop-table/drag-and-drop-table"; -import FormCheckbox from "../util/forms/form-checkbox"; +import FormCheckbox from "../../../shared/forms/form-checkbox"; import { useGetV2PlaylistsByIdSlidesQuery, useGetV2PlaylistsQuery, diff --git a/assets/admin/components/playlist/playlist-campaign-form.jsx b/assets/admin/components/playlist/playlist-campaign-form.jsx index c9618613..52d96a1b 100644 --- a/assets/admin/components/playlist/playlist-campaign-form.jsx +++ b/assets/admin/components/playlist/playlist-campaign-form.jsx @@ -5,15 +5,15 @@ import { useNavigate } from "react-router-dom"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faExpand } from "@fortawesome/free-solid-svg-icons"; import ContentBody from "../util/content-body/content-body"; -import FormInput from "../util/forms/form-input"; -import FormInputArea from "../util/forms/form-input-area"; +import FormInput from "../../../shared/forms/form-input"; +import FormInputArea from "../../../shared/forms/form-input-area"; import SelectSlidesTable from "../util/multi-and-table/select-slides-table"; import LoadingComponent from "../util/loading-component/loading-component"; import Preview from "../preview/preview"; import idFromUrl from "../util/helpers/id-from-url"; import StickyFooter from "../util/sticky-footer"; import localStorageKeys from "../util/local-storage-keys"; -import Select from "../util/forms/select"; +import Select from "../../../shared/forms/select"; import userContext from "../../context/user-context"; /** diff --git a/assets/admin/components/playlist/playlist-form.jsx b/assets/admin/components/playlist/playlist-form.jsx index fd894738..6d3bd7b7 100644 --- a/assets/admin/components/playlist/playlist-form.jsx +++ b/assets/admin/components/playlist/playlist-form.jsx @@ -5,7 +5,7 @@ import UserContext from "../../context/user-context"; import Schedule from "../util/schedule/schedule"; import { useGetV2TenantsQuery } from "../../../shared/redux/enhanced-api.ts"; import ContentBody from "../util/content-body/content-body"; -import TenantsDropdown from "../util/forms/multiselect-dropdown/tenants/tenants-dropdown"; +import TenantsDropdown from "../util/multiselect-dropdown/tenants/tenants-dropdown"; /** * The playlist form component. diff --git a/assets/admin/components/playlist/playlist-gantt-chart.jsx b/assets/admin/components/playlist/playlist-gantt-chart.jsx index f7bd6e03..67880861 100644 --- a/assets/admin/components/playlist/playlist-gantt-chart.jsx +++ b/assets/admin/components/playlist/playlist-gantt-chart.jsx @@ -2,7 +2,7 @@ import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import GanttChart from "../util/gantt-chart"; import localStorageKeys from "../util/local-storage-keys"; -import FormCheckbox from "../util/forms/form-checkbox"; +import FormCheckbox from "../../../shared/forms/form-checkbox"; /** * @param {object} props The props. diff --git a/assets/admin/components/screen/screen-form.jsx b/assets/admin/components/screen/screen-form.jsx index 8f64516c..26df02ae 100644 --- a/assets/admin/components/screen/screen-form.jsx +++ b/assets/admin/components/screen/screen-form.jsx @@ -5,20 +5,20 @@ import { useNavigate } from "react-router-dom"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faExpand } from "@fortawesome/free-solid-svg-icons"; import ContentBody from "../util/content-body/content-body"; -import FormInput from "../util/forms/form-input"; -import FormInputArea from "../util/forms/form-input-area"; +import FormInput from "../../../shared/forms/form-input"; +import FormInputArea from "../../../shared/forms/form-input-area"; import SelectGroupsTable from "../util/multi-and-table/select-groups-table"; import GridGenerationAndSelect from "./util/grid-generation-and-select"; -import MultiSelectComponent from "../util/forms/multiselect-dropdown/multi-dropdown"; +import MultiSelectComponent from "../util/multiselect-dropdown/multi-dropdown"; import idFromUrl from "../util/helpers/id-from-url"; import { useGetV2LayoutsQuery, enhancedApi, } from "../../../shared/redux/enhanced-api.ts"; -import FormCheckbox from "../util/forms/form-checkbox"; +import FormCheckbox from "../../../shared/forms/form-checkbox"; import Preview from "../preview/preview"; import StickyFooter from "../util/sticky-footer"; -import Select from "../util/forms/select"; +import Select from "../../../shared/forms/select"; import userContext from "../../context/user-context"; import ScreenStatus from "./screen-status"; import { displayError } from "../util/list/toast-component/display-toast"; diff --git a/assets/admin/components/screen/screen-status.jsx b/assets/admin/components/screen/screen-status.jsx index 972c39cf..b9a96bb6 100644 --- a/assets/admin/components/screen/screen-status.jsx +++ b/assets/admin/components/screen/screen-status.jsx @@ -16,7 +16,7 @@ import { useDispatch } from "react-redux"; import idFromUrl from "../util/helpers/id-from-url"; import { enhancedApi } from "../../../shared/redux/enhanced-api.ts"; import { displayError } from "../util/list/toast-component/display-toast"; -import FormInput from "../util/forms/form-input"; +import FormInput from "../../../shared/forms/form-input"; import AdminConfigLoader from "../util/admin-config-loader.js"; /** diff --git a/assets/admin/components/screen/util/screen-gantt-chart.jsx b/assets/admin/components/screen/util/screen-gantt-chart.jsx index e59cb50e..e907f355 100644 --- a/assets/admin/components/screen/util/screen-gantt-chart.jsx +++ b/assets/admin/components/screen/util/screen-gantt-chart.jsx @@ -3,7 +3,7 @@ import { RRule } from "rrule"; import { useTranslation } from "react-i18next"; import GanttChart from "../../util/gantt-chart"; import localStorageKeys from "../../util/local-storage-keys"; -import FormCheckbox from "../../util/forms/form-checkbox"; +import FormCheckbox from "../../../../shared/forms/form-checkbox"; import UserContext from "../../../context/user-context"; /** diff --git a/assets/admin/components/slide/content/contacts/contact-form.jsx b/assets/admin/components/slide/content/contacts/contact-form.jsx index 2e1cff04..f84fedd7 100644 --- a/assets/admin/components/slide/content/contacts/contact-form.jsx +++ b/assets/admin/components/slide/content/contacts/contact-form.jsx @@ -2,7 +2,7 @@ import set from "lodash.set"; import { Col, Row } from "react-bootstrap"; import { useTranslation } from "react-i18next"; import FileSelector from "../file-selector"; -import FormInput from "../../../util/forms/form-input"; +import FormInput from "../../../../../shared/forms/form-input"; /** * Contact form. diff --git a/assets/admin/components/slide/content/content-form.jsx b/assets/admin/components/slide/content/content-form.jsx index 77dbb3dd..a3f91504 100644 --- a/assets/admin/components/slide/content/content-form.jsx +++ b/assets/admin/components/slide/content/content-form.jsx @@ -1,13 +1,14 @@ -import FormCheckbox from "../../util/forms/form-checkbox"; -import FormInput from "../../util/forms/form-input"; -import Select from "../../util/forms/select"; +import FormCheckbox from "../../../../shared/forms/form-checkbox"; +import FormInput from "../../../../shared/forms/form-input"; +import Select from "../../../../shared/forms/select"; import Contacts from "./contacts/contacts"; -import RichText from "../../util/forms/rich-text/rich-text"; -import FormTable from "../../util/forms/form-table/form-table"; +import RichText from "../../../../shared/forms/rich-text/rich-text"; +import FormTable from "../../../../shared/forms/form-table/form-table"; import FileSelector from "./file-selector"; import StationSelector from "./station/station-selector"; -import RadioButtons from "../../util/forms/radio-buttons"; -import CheckboxOptions from "../../util/forms/checkbox-options"; +import RadioButtons from "../../../../shared/forms/radio-buttons"; +import CheckboxOptions from "../../../../shared/forms/checkbox-options"; +import getInputFiles from "../../../../shared/admin-util/helper"; /** * Render form elements for content form. @@ -29,20 +30,6 @@ function ContentForm({ onChange = null, mediaData = {}, }) { - const getInputFiles = (field) => { - const inputFiles = []; - - if (Array.isArray(field)) { - field.forEach((mediaId) => { - if (Object.prototype.hasOwnProperty.call(mediaData, mediaId)) { - inputFiles.push(mediaData[mediaId]); - } - }); - } - - return inputFiles; - }; - /** * @param {object} formData - The data for form input. * @returns {object | string} - Returns a rendered jsx object. @@ -78,7 +65,7 @@ function ContentForm({ )} {}} + onFileChange={() => {}} // Todo perhaps an error instead of an empty default /> ); }; diff --git a/assets/admin/components/slide/content/file-dropzone.jsx b/assets/admin/components/slide/content/file-dropzone.jsx index 277c3ca3..b8df6e5a 100644 --- a/assets/admin/components/slide/content/file-dropzone.jsx +++ b/assets/admin/components/slide/content/file-dropzone.jsx @@ -40,7 +40,7 @@ function FileDropzone({ onFilesAdded, acceptedMimetypes = null }) { <> {/* TODO: Fix styling for dropzone: https://react-dropzone.js.org/#section-styling-dropzone */} {/* eslint-disable react/jsx-props-no-spreading */} -
+
{t("file-dropzone.drag-and-drop-text")} diff --git a/assets/admin/components/slide/content/file-form-element.jsx b/assets/admin/components/slide/content/file-form-element.jsx index 78a4a339..ff138507 100644 --- a/assets/admin/components/slide/content/file-form-element.jsx +++ b/assets/admin/components/slide/content/file-form-element.jsx @@ -1,6 +1,6 @@ import { Button, Row, Col } from "react-bootstrap"; import { useTranslation } from "react-i18next"; -import FormInput from "../../util/forms/form-input"; +import FormInput from "../../../../shared/forms/form-input"; import FilePreview from "./file-preview"; /** diff --git a/assets/admin/components/slide/content/file-selector.jsx b/assets/admin/components/slide/content/file-selector.jsx index fa2042ac..8e8267d3 100644 --- a/assets/admin/components/slide/content/file-selector.jsx +++ b/assets/admin/components/slide/content/file-selector.jsx @@ -107,19 +107,21 @@ function FileSelector({ /> {enableMediaLibrary && ( <> - - {/* +
+ + {/* TODO: Make this configurable. It should always align with sizes in https://github.com/os2display/display-api-service/blob/develop/src/Entity/Tenant/Media.php */} -
- {t("file-selector.max-size")}: 200 MB +
+ {t("file-selector.max-size")}: 200 MB +
+ {renderAdminForm( + idFromUrl(selectedTemplate.id), + slide.content, + handleContent, + handleMedia, + mediaData, + )} {contentFormElements.map((formElement) => ( {formElement.input === "feed" && ( diff --git a/assets/admin/components/themes/theme-form.jsx b/assets/admin/components/themes/theme-form.jsx index 03b75693..aadef68a 100644 --- a/assets/admin/components/themes/theme-form.jsx +++ b/assets/admin/components/themes/theme-form.jsx @@ -3,9 +3,9 @@ import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import Form from "react-bootstrap/Form"; import LoadingComponent from "../util/loading-component/loading-component"; -import FormInputArea from "../util/forms/form-input-area"; +import FormInputArea from "../../../shared/forms/form-input-area"; import ContentBody from "../util/content-body/content-body"; -import FormInput from "../util/forms/form-input"; +import FormInput from "../../../shared/forms/form-input"; import ImageUploader from "../util/image-uploader/image-uploader"; import StickyFooter from "../util/sticky-footer"; diff --git a/assets/admin/components/user/login.jsx b/assets/admin/components/user/login.jsx index 26b802c7..4528d3e1 100644 --- a/assets/admin/components/user/login.jsx +++ b/assets/admin/components/user/login.jsx @@ -7,7 +7,7 @@ import queryString from "query-string"; import Col from "react-bootstrap/Col"; import { MultiSelect } from "react-multi-select-component"; import UserContext from "../../context/user-context"; -import FormInput from "../util/forms/form-input"; +import FormInput from "../../../shared/forms/form-input"; import { enhancedApi } from "../../../shared/redux/enhanced-api.ts"; import AdminConfigLoader from "../util/admin-config-loader.js"; import { displayError } from "../util/list/toast-component/display-toast"; diff --git a/assets/admin/components/util/image-uploader/image.jsx b/assets/admin/components/util/image-uploader/image.jsx index e3710b43..dcb6c522 100644 --- a/assets/admin/components/util/image-uploader/image.jsx +++ b/assets/admin/components/util/image-uploader/image.jsx @@ -1,7 +1,7 @@ import { useState } from "react"; import { Button, Row, Col } from "react-bootstrap"; import { useTranslation } from "react-i18next"; -import FormInput from "../forms/form-input"; +import FormInput from "../../../../shared/forms/form-input"; import "./image-uploader.scss"; /** diff --git a/assets/admin/components/util/list/list.jsx b/assets/admin/components/util/list/list.jsx index 9a0f127c..78558427 100644 --- a/assets/admin/components/util/list/list.jsx +++ b/assets/admin/components/util/list/list.jsx @@ -10,9 +10,9 @@ import useModal from "../../../context/modal-context/modal-context-hook"; import Pagination from "../paginate/pagination"; import ListLoading from "../loading-component/list-loading"; import localStorageKeys from "../local-storage-keys"; -import FormCheckbox from "../forms/form-checkbox"; +import FormCheckbox from "../../../../shared/forms/form-checkbox"; import ListContext from "../../../context/list-context"; -import Select from "../forms/select"; +import Select from "../../../../shared/forms/select"; /** * @param {object} props - The props. diff --git a/assets/admin/components/util/multi-and-table/select-groups-table.jsx b/assets/admin/components/util/multi-and-table/select-groups-table.jsx index 316c591a..03b3aa99 100644 --- a/assets/admin/components/util/multi-and-table/select-groups-table.jsx +++ b/assets/admin/components/util/multi-and-table/select-groups-table.jsx @@ -6,7 +6,7 @@ import { useGetV2ScreenGroupsQuery, useGetV2ScreenGroupsByIdScreensQuery, } from "../../../../shared/redux/enhanced-api.ts"; -import GroupsDropdown from "../forms/multiselect-dropdown/groups/groups-dropdown"; +import GroupsDropdown from "../multiselect-dropdown/groups/groups-dropdown"; import useFetchDataHook from "../fetch-data-hook.js"; /** diff --git a/assets/admin/components/util/multi-and-table/select-playlists-table.jsx b/assets/admin/components/util/multi-and-table/select-playlists-table.jsx index 89e9235f..a01ad19e 100644 --- a/assets/admin/components/util/multi-and-table/select-playlists-table.jsx +++ b/assets/admin/components/util/multi-and-table/select-playlists-table.jsx @@ -6,7 +6,7 @@ import { useGetV2PlaylistsQuery, useGetV2PlaylistsByIdSlidesQuery, } from "../../../../shared/redux/enhanced-api.ts"; -import PlaylistsDropdown from "../forms/multiselect-dropdown/playlists/playlists-dropdown"; +import PlaylistsDropdown from "../multiselect-dropdown/playlists/playlists-dropdown"; import { SelectPlaylistColumns } from "../../playlist/playlists-columns"; import useFetchDataHook from "../fetch-data-hook.js"; diff --git a/assets/admin/components/util/multi-and-table/select-screens-table.jsx b/assets/admin/components/util/multi-and-table/select-screens-table.jsx index 1b743c58..1e3555ee 100644 --- a/assets/admin/components/util/multi-and-table/select-screens-table.jsx +++ b/assets/admin/components/util/multi-and-table/select-screens-table.jsx @@ -1,7 +1,7 @@ import { useState, useEffect } from "react"; import { useTranslation } from "react-i18next"; import Table from "../table/table"; -import ScreensDropdown from "../forms/multiselect-dropdown/screens/screens-dropdown"; +import ScreensDropdown from "../multiselect-dropdown/screens/screens-dropdown"; import { SelectScreenColumns } from "../../screen/util/screen-columns"; import { enhancedApi, diff --git a/assets/admin/components/util/multi-and-table/select-slides-table.jsx b/assets/admin/components/util/multi-and-table/select-slides-table.jsx index 43e1f894..6fab0228 100644 --- a/assets/admin/components/util/multi-and-table/select-slides-table.jsx +++ b/assets/admin/components/util/multi-and-table/select-slides-table.jsx @@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next"; import dayjs from "dayjs"; import { SelectSlideColumns } from "../../slide/slides-columns"; import DragAndDropTable from "../drag-and-drop-table/drag-and-drop-table"; -import SlidesDropdown from "../forms/multiselect-dropdown/slides/slides-dropdown"; +import SlidesDropdown from "../multiselect-dropdown/slides/slides-dropdown"; import { useGetV2SlidesQuery, useGetV2PlaylistsByIdQuery, diff --git a/assets/admin/components/util/forms/multiselect-dropdown/groups/groups-dropdown.jsx b/assets/admin/components/util/multiselect-dropdown/groups/groups-dropdown.jsx similarity index 100% rename from assets/admin/components/util/forms/multiselect-dropdown/groups/groups-dropdown.jsx rename to assets/admin/components/util/multiselect-dropdown/groups/groups-dropdown.jsx diff --git a/assets/admin/components/util/forms/multiselect-dropdown/multi-dropdown.jsx b/assets/admin/components/util/multiselect-dropdown/multi-dropdown.jsx similarity index 99% rename from assets/admin/components/util/forms/multiselect-dropdown/multi-dropdown.jsx rename to assets/admin/components/util/multiselect-dropdown/multi-dropdown.jsx index 0ae2cd44..5d378934 100644 --- a/assets/admin/components/util/forms/multiselect-dropdown/multi-dropdown.jsx +++ b/assets/admin/components/util/multiselect-dropdown/multi-dropdown.jsx @@ -2,7 +2,7 @@ import { useEffect, useState } from "react"; import { MultiSelect } from "react-multi-select-component"; import Form from "react-bootstrap/Form"; import { useTranslation } from "react-i18next"; -import contentString from "../../helpers/content-string"; +import contentString from "../helpers/content-string"; import "./multi-dropdown.scss"; /** diff --git a/assets/admin/components/util/forms/multiselect-dropdown/multi-dropdown.scss b/assets/admin/components/util/multiselect-dropdown/multi-dropdown.scss similarity index 100% rename from assets/admin/components/util/forms/multiselect-dropdown/multi-dropdown.scss rename to assets/admin/components/util/multiselect-dropdown/multi-dropdown.scss diff --git a/assets/admin/components/util/forms/multiselect-dropdown/playlists/playlists-dropdown.jsx b/assets/admin/components/util/multiselect-dropdown/playlists/playlists-dropdown.jsx similarity index 100% rename from assets/admin/components/util/forms/multiselect-dropdown/playlists/playlists-dropdown.jsx rename to assets/admin/components/util/multiselect-dropdown/playlists/playlists-dropdown.jsx diff --git a/assets/admin/components/util/forms/multiselect-dropdown/screens/screens-dropdown.jsx b/assets/admin/components/util/multiselect-dropdown/screens/screens-dropdown.jsx similarity index 100% rename from assets/admin/components/util/forms/multiselect-dropdown/screens/screens-dropdown.jsx rename to assets/admin/components/util/multiselect-dropdown/screens/screens-dropdown.jsx diff --git a/assets/admin/components/util/forms/multiselect-dropdown/slides/slides-dropdown.jsx b/assets/admin/components/util/multiselect-dropdown/slides/slides-dropdown.jsx similarity index 100% rename from assets/admin/components/util/forms/multiselect-dropdown/slides/slides-dropdown.jsx rename to assets/admin/components/util/multiselect-dropdown/slides/slides-dropdown.jsx diff --git a/assets/admin/components/util/forms/multiselect-dropdown/tenants/tenants-dropdown.jsx b/assets/admin/components/util/multiselect-dropdown/tenants/tenants-dropdown.jsx similarity index 100% rename from assets/admin/components/util/forms/multiselect-dropdown/tenants/tenants-dropdown.jsx rename to assets/admin/components/util/multiselect-dropdown/tenants/tenants-dropdown.jsx diff --git a/assets/admin/components/util/schedule/schedule.jsx b/assets/admin/components/util/schedule/schedule.jsx index 7c8a7a28..eb425233 100644 --- a/assets/admin/components/util/schedule/schedule.jsx +++ b/assets/admin/components/util/schedule/schedule.jsx @@ -5,8 +5,8 @@ import { Button, FormGroup } from "react-bootstrap"; import dayjs from "dayjs"; import { RRule } from "rrule"; import utc from "dayjs/plugin/utc"; -import FormInput from "../forms/form-input"; -import Select from "../forms/select"; +import FormInput from "../../../../shared/forms/form-input"; +import Select from "../../../../shared/forms/select"; import { createNewSchedule, createScheduleFromRRule, @@ -16,7 +16,7 @@ import { getNextOccurrences, getRruleString, } from "./schedule-util"; -import FormCheckbox from "../forms/form-checkbox"; +import FormCheckbox from "../../../../shared/forms/form-checkbox"; import Tooltip from "../tooltip"; dayjs.extend(utc); diff --git a/assets/client/components/slide.jsx b/assets/client/components/slide.jsx index bf5b76d5..fe2a75f7 100644 --- a/assets/client/components/slide.jsx +++ b/assets/client/components/slide.jsx @@ -1,6 +1,6 @@ import ErrorBoundary from "./error-boundary.jsx"; import logger from "../logger/logger"; -import { renderSlide } from "../../shared/slide-utils/templates.js"; +import { renderSlide } from "../../shared/slide-utils/templates-slide.js"; import "./slide.scss"; /** diff --git a/assets/shared/admin-util/helper.js b/assets/shared/admin-util/helper.js new file mode 100644 index 00000000..781482ea --- /dev/null +++ b/assets/shared/admin-util/helper.js @@ -0,0 +1,14 @@ +const getInputFiles = (field, mediaData) => { + const inputFiles = []; + if (Array.isArray(field)) { + field.forEach((mediaId) => { + if (Object.prototype.hasOwnProperty.call(mediaData, mediaId)) { + inputFiles.push(mediaData[mediaId]); + } + }); + } + + return inputFiles; +}; + +export default getInputFiles; diff --git a/assets/shared/custom-templates-example/custom-template-example.jsx b/assets/shared/custom-templates-example/custom-template-example.jsx index 0ea8a45f..088ad86e 100644 --- a/assets/shared/custom-templates-example/custom-template-example.jsx +++ b/assets/shared/custom-templates-example/custom-template-example.jsx @@ -2,6 +2,10 @@ import { 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"; +import i18next from "i18next"; +import adminTranslations from "./translations.json"; +import { useTranslation } from "react-i18next"; +import getInputFiles from "../admin-util/helper.js"; /** * Get the ULID of the template. @@ -38,6 +42,17 @@ function renderSlide(slide, run, slideDone) { ); } +function renderAdminForm(formStateObject, onChange, handleMedia, mediaData) { + return ( + + ); +} + /** * @param {object} props Props. * @param {object} props.slide The slide. @@ -82,4 +97,69 @@ function CustomTemplateExample({ ); } -export default { id, config, renderSlide }; +/** + * @param {object} props Props. + * @param {object} props.slideContent The slide content. + * @param {Function} props.onSlideContentChange on slide content change. + * @param {Function} props.handleMedia on slide media change. + * @param {object} props.mediaData The media object. + * @returns {JSX.Element} The component. + */ +function CustomTemplateAdminExample({ + slideContent, + onSlideContentChange, + handleMedia = () => {}, + mediaData = {}, +}) { + const { t } = useTranslation("custom-template-admin-example"); + + useEffect(() => { + const currentLang = i18next.language; + if ( + !i18next.hasResourceBundle(currentLang, "custom-template-admin-example") + ) { + i18next.addResourceBundle( + currentLang, + "custom-template-admin-example", + adminTranslations["custom-template-admin-example"], + true, + true, + ); + } + }, []); + + return ( + <> +

{t("header")}

+
+ {t("content-sub-header")} +