diff --git a/Taskfile.yml b/Taskfile.yml
index 0dbc4dc90..97639dd7c 100644
--- a/Taskfile.yml
+++ b/Taskfile.yml
@@ -202,7 +202,7 @@ tasks:
test:frontend-local:
desc: "Runs frontend tests from the local machine."
cmds:
- - BASE_URL="https://display.local.itkdev.dk" npx playwright test
+ - BASE_URL="https://display.local.itkdev.dk" npx playwright test {{.CLI_ARGS}}
test:frontend-local-ui:
desc: "Runs frontend tests from the local machine in UI mode."
@@ -215,9 +215,10 @@ tasks:
- task compose -- exec phpfpm composer update-api-spec
generate:redux-toolkit-api:
- desc: "Generate the RTK Query api slices (in assets/shared/redux/)."
+ desc: "Generate the RTK Query api slices."
cmds:
- - task compose -- exec node npx @rtk-query/codegen-openapi /app/assets/shared/redux/openapi-config.js
+ - task compose -- exec node npx @rtk-query/codegen-openapi /app/assets/admin/redux/openapi-config.js
+ - task compose -- exec node npx @rtk-query/codegen-openapi /app/assets/client/redux/openapi-config.js
app:update:
desc: "Migrate to latest database schema and update installed templates"
diff --git a/assets/admin/components/activation-code/activation-code-activate.jsx b/assets/admin/components/activation-code/activation-code-activate.jsx
index 711aabbbc..f6589b533 100644
--- a/assets/admin/components/activation-code/activation-code-activate.jsx
+++ b/assets/admin/components/activation-code/activation-code-activate.jsx
@@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import Form from "react-bootstrap/Form";
import { Button } from "react-bootstrap";
-import { usePostV2UserActivationCodesActivateMutation } from "../../../shared/redux/enhanced-api.ts";
+import { usePostV2UserActivationCodesActivateMutation } from "../../redux/enhanced-api.ts";
import {
displaySuccess,
displayError,
diff --git a/assets/admin/components/activation-code/activation-code-create.jsx b/assets/admin/components/activation-code/activation-code-create.jsx
index 5a8faf35b..adb822cfe 100644
--- a/assets/admin/components/activation-code/activation-code-create.jsx
+++ b/assets/admin/components/activation-code/activation-code-create.jsx
@@ -1,7 +1,7 @@
import { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
-import { usePostV2UserActivationCodesMutation } from "../../../shared/redux/enhanced-api.ts";
+import { usePostV2UserActivationCodesMutation } from "../../redux/enhanced-api.ts";
import ActivationCodeForm from "./activation-code-form";
import {
displaySuccess,
diff --git a/assets/admin/components/activation-code/activation-code-list.jsx b/assets/admin/components/activation-code/activation-code-list.jsx
index 8925119d1..5e9fd833f 100644
--- a/assets/admin/components/activation-code/activation-code-list.jsx
+++ b/assets/admin/components/activation-code/activation-code-list.jsx
@@ -18,7 +18,7 @@ import {
enhancedApi,
useDeleteV2UserActivationCodesByIdMutation,
useGetV2UserActivationCodesQuery,
-} from "../../../shared/redux/enhanced-api.ts";
+} from "../../redux/enhanced-api.ts";
/**
* The Activation Code list component.
diff --git a/assets/admin/components/feed-sources/feed-source-edit.jsx b/assets/admin/components/feed-sources/feed-source-edit.jsx
index 602af8bcd..b57d74696 100644
--- a/assets/admin/components/feed-sources/feed-source-edit.jsx
+++ b/assets/admin/components/feed-sources/feed-source-edit.jsx
@@ -1,5 +1,5 @@
import { useParams } from "react-router-dom";
-import { useGetV2FeedSourcesByIdQuery } from "../../../shared/redux/enhanced-api.ts";
+import { useGetV2FeedSourcesByIdQuery } from "../../redux/enhanced-api.ts";
import FeedSourceManager from "./feed-source-manager";
/**
diff --git a/assets/admin/components/feed-sources/feed-source-manager.jsx b/assets/admin/components/feed-sources/feed-source-manager.jsx
index 6dcfe0e7b..e42eafcc9 100644
--- a/assets/admin/components/feed-sources/feed-source-manager.jsx
+++ b/assets/admin/components/feed-sources/feed-source-manager.jsx
@@ -5,7 +5,7 @@ import FeedSourceForm from "./feed-source-form";
import {
usePostV2FeedSourcesMutation,
usePutV2FeedSourcesByIdMutation,
-} from "../../../shared/redux/enhanced-api.ts";
+} from "../../redux/enhanced-api.ts";
import {
displayError,
displaySuccess,
diff --git a/assets/admin/components/feed-sources/feed-sources-list.jsx b/assets/admin/components/feed-sources/feed-sources-list.jsx
index 90d4146de..054dd0fcb 100644
--- a/assets/admin/components/feed-sources/feed-sources-list.jsx
+++ b/assets/admin/components/feed-sources/feed-sources-list.jsx
@@ -5,7 +5,7 @@ import {
useGetV2FeedSourcesQuery,
useDeleteV2FeedSourcesByIdMutation,
useGetV2FeedSourcesByIdSlidesQuery,
-} from "../../../shared/redux/enhanced-api.ts";
+} from "../../redux/enhanced-api.ts";
import ListContext from "../../context/list-context";
import ContentBody from "../util/content-body/content-body";
import List from "../util/list/list";
diff --git a/assets/admin/components/groups/group-create.jsx b/assets/admin/components/groups/group-create.jsx
index bc64a70bc..9830608b6 100644
--- a/assets/admin/components/groups/group-create.jsx
+++ b/assets/admin/components/groups/group-create.jsx
@@ -1,7 +1,7 @@
import { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
-import { usePostV2ScreenGroupsMutation } from "../../../shared/redux/enhanced-api.ts";
+import { usePostV2ScreenGroupsMutation } from "../../redux/enhanced-api.ts";
import GroupForm from "./group-form";
import {
displaySuccess,
diff --git a/assets/admin/components/groups/group-edit.jsx b/assets/admin/components/groups/group-edit.jsx
index fca125590..a5d6c3466 100644
--- a/assets/admin/components/groups/group-edit.jsx
+++ b/assets/admin/components/groups/group-edit.jsx
@@ -4,7 +4,7 @@ import { useTranslation } from "react-i18next";
import {
useGetV2ScreenGroupsByIdQuery,
usePutV2ScreenGroupsByIdMutation,
-} from "../../../shared/redux/enhanced-api.ts";
+} from "../../redux/enhanced-api.ts";
import {
displaySuccess,
displayError,
diff --git a/assets/admin/components/groups/groups-columns.jsx b/assets/admin/components/groups/groups-columns.jsx
index 191df9414..0b38faa61 100644
--- a/assets/admin/components/groups/groups-columns.jsx
+++ b/assets/admin/components/groups/groups-columns.jsx
@@ -3,11 +3,11 @@ import ColumnHoc from "../util/column-hoc";
import SelectColumnHoc from "../util/select-column-hoc";
import useModal from "../../context/modal-context/modal-context-hook.jsx";
import { useDispatch } from "react-redux";
-import { enhancedApi } from "../../../shared/redux/enhanced-api.ts";
import idFromUrl from "../util/helpers/id-from-url.jsx";
import getAllPages from "../util/helpers/get-all-pages.js";
import { Link } from "react-router-dom";
import { Button } from "react-bootstrap";
+import { enhancedApi } from "../../redux/enhanced-api.ts";
function ScreensButton({ group }) {
const { t } = useTranslation("common", { keyPrefix: "groups-columns" });
diff --git a/assets/admin/components/groups/groups-list.jsx b/assets/admin/components/groups/groups-list.jsx
index 2b75c358a..a92d9cdfc 100644
--- a/assets/admin/components/groups/groups-list.jsx
+++ b/assets/admin/components/groups/groups-list.jsx
@@ -16,7 +16,7 @@ import {
useGetV2ScreenGroupsQuery,
useGetV2ScreenGroupsByIdScreensQuery,
useDeleteV2ScreenGroupsByIdMutation,
-} from "../../../shared/redux/enhanced-api.ts";
+} from "../../redux/enhanced-api.ts";
/**
* The groups list component.
diff --git a/assets/admin/components/media/media-create.jsx b/assets/admin/components/media/media-create.jsx
index 051274acf..0db61a579 100644
--- a/assets/admin/components/media/media-create.jsx
+++ b/assets/admin/components/media/media-create.jsx
@@ -1,6 +1,6 @@
import { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
-import { usePostMediaCollectionMutation } from "../../../shared/redux/enhanced-api.ts";
+import { usePostMediaCollectionMutation } from "../../redux/enhanced-api.ts";
import MediaForm from "./media-form";
import {
displayError,
diff --git a/assets/admin/components/media/media-list.jsx b/assets/admin/components/media/media-list.jsx
index 5cd8ed320..3897a71c1 100644
--- a/assets/admin/components/media/media-list.jsx
+++ b/assets/admin/components/media/media-list.jsx
@@ -16,7 +16,7 @@ import {
import {
useGetV2MediaQuery,
useDeleteV2MediaByIdMutation,
-} from "../../../shared/redux/enhanced-api.ts";
+} from "../../redux/enhanced-api.ts";
import FormCheckbox from "../util/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 1735e1661..5dd5d7849 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
@@ -7,7 +7,7 @@ import FormCheckbox from "../util/forms/form-checkbox";
import {
useGetV2PlaylistsByIdSlidesQuery,
useGetV2PlaylistsQuery,
-} from "../../../shared/redux/enhanced-api.ts";
+} from "../../redux/enhanced-api.ts";
import ScreenGanttChart from "../screen/util/screen-gantt-chart";
/**
diff --git a/assets/admin/components/playlist/campaign-form.jsx b/assets/admin/components/playlist/campaign-form.jsx
index 4e2fe5fdc..ee993c694 100644
--- a/assets/admin/components/playlist/campaign-form.jsx
+++ b/assets/admin/components/playlist/campaign-form.jsx
@@ -1,6 +1,6 @@
import { useTranslation } from "react-i18next";
import idFromUrl from "../util/helpers/id-from-url";
-import { enhancedApi } from "../../../shared/redux/enhanced-api.ts";
+import { enhancedApi } from "../../redux/enhanced-api.ts";
import ContentBody from "../util/content-body/content-body";
import SelectScreensTable from "../util/multi-and-table/select-screens-table";
import SelectGroupsTable from "../util/multi-and-table/select-groups-table";
diff --git a/assets/admin/components/playlist/playlist-campaign-edit.jsx b/assets/admin/components/playlist/playlist-campaign-edit.jsx
index 8a7af904e..7ffd955f9 100644
--- a/assets/admin/components/playlist/playlist-campaign-edit.jsx
+++ b/assets/admin/components/playlist/playlist-campaign-edit.jsx
@@ -2,7 +2,7 @@ import { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import PlaylistCampaignManager from "./playlist-campaign-manager";
import idFromUrl from "../util/helpers/id-from-url";
-import { useGetV2PlaylistsByIdQuery } from "../../../shared/redux/enhanced-api.ts";
+import { useGetV2PlaylistsByIdQuery } from "../../redux/enhanced-api.ts";
/**
* The playlist/campaign edit component.
diff --git a/assets/admin/components/playlist/playlist-campaign-list.jsx b/assets/admin/components/playlist/playlist-campaign-list.jsx
index ddbcfddc0..59b593111 100644
--- a/assets/admin/components/playlist/playlist-campaign-list.jsx
+++ b/assets/admin/components/playlist/playlist-campaign-list.jsx
@@ -16,7 +16,7 @@ import {
useDeleteV2PlaylistsByIdMutation,
useGetV2PlaylistsByIdSlidesQuery,
useGetV2PlaylistsQuery,
-} from "../../../shared/redux/enhanced-api.ts";
+} from "../../redux/enhanced-api.ts";
/**
* The shared list component.
diff --git a/assets/admin/components/playlist/playlist-campaign-manager.jsx b/assets/admin/components/playlist/playlist-campaign-manager.jsx
index b8cb387a3..2f65182ef 100644
--- a/assets/admin/components/playlist/playlist-campaign-manager.jsx
+++ b/assets/admin/components/playlist/playlist-campaign-manager.jsx
@@ -15,7 +15,7 @@ import {
enhancedApi,
usePutV2PlaylistsByIdMutation,
usePostV2PlaylistsMutation,
-} from "../../../shared/redux/enhanced-api.ts";
+} from "../../redux/enhanced-api.ts";
import { set } from "lodash/object";
/**
diff --git a/assets/admin/components/playlist/playlist-form.jsx b/assets/admin/components/playlist/playlist-form.jsx
index fd894738b..b58c663d2 100644
--- a/assets/admin/components/playlist/playlist-form.jsx
+++ b/assets/admin/components/playlist/playlist-form.jsx
@@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next";
import { Alert } from "react-bootstrap";
import UserContext from "../../context/user-context";
import Schedule from "../util/schedule/schedule";
-import { useGetV2TenantsQuery } from "../../../shared/redux/enhanced-api.ts";
+import { useGetV2TenantsQuery } from "../../redux/enhanced-api.ts";
import ContentBody from "../util/content-body/content-body";
import TenantsDropdown from "../util/forms/multiselect-dropdown/tenants/tenants-dropdown";
diff --git a/assets/admin/components/playlist/playlists-columns.jsx b/assets/admin/components/playlist/playlists-columns.jsx
index d7c93be26..814838c80 100644
--- a/assets/admin/components/playlist/playlists-columns.jsx
+++ b/assets/admin/components/playlist/playlists-columns.jsx
@@ -5,11 +5,11 @@ import DateValue from "../util/date-value";
import PublishingStatus from "../util/publishingStatus";
import useModal from "../../context/modal-context/modal-context-hook.jsx";
import { useDispatch } from "react-redux";
-import { enhancedApi } from "../../../shared/redux/enhanced-api.ts";
import idFromUrl from "../util/helpers/id-from-url.jsx";
import getAllPages from "../util/helpers/get-all-pages.js";
import { Link } from "react-router-dom";
import { Button } from "react-bootstrap";
+import { enhancedApi } from "../../redux/enhanced-api.ts";
function SlidesButton({ playlist }) {
const { t } = useTranslation("common", { keyPrefix: "playlists-columns" });
diff --git a/assets/admin/components/playlist/shared-playlists.jsx b/assets/admin/components/playlist/shared-playlists.jsx
index 7b89e0fec..8523ca840 100644
--- a/assets/admin/components/playlist/shared-playlists.jsx
+++ b/assets/admin/components/playlist/shared-playlists.jsx
@@ -7,7 +7,7 @@ import ListContext from "../../context/list-context";
import ContentBody from "../util/content-body/content-body";
import { displayError } from "../util/list/toast-component/display-toast";
import getSharedPlaylistColumns from "./shared-playlists-column";
-import { useGetV2PlaylistsQuery } from "../../../shared/redux/enhanced-api.ts";
+import { useGetV2PlaylistsQuery } from "../../redux/enhanced-api.ts";
/**
* The list component for shared playlists.
diff --git a/assets/admin/components/screen/screen-edit.jsx b/assets/admin/components/screen/screen-edit.jsx
index 7fd26e5a5..fd533fd62 100644
--- a/assets/admin/components/screen/screen-edit.jsx
+++ b/assets/admin/components/screen/screen-edit.jsx
@@ -2,7 +2,7 @@ import { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import idFromUrl from "../util/helpers/id-from-url";
import ScreenManager from "./screen-manager";
-import { useGetV2ScreensByIdQuery } from "../../../shared/redux/enhanced-api.ts";
+import { useGetV2ScreensByIdQuery } from "../../redux/enhanced-api.ts";
/**
* The screen edit component.
diff --git a/assets/admin/components/screen/screen-form.jsx b/assets/admin/components/screen/screen-form.jsx
index 0f141cf33..4d2aa5457 100644
--- a/assets/admin/components/screen/screen-form.jsx
+++ b/assets/admin/components/screen/screen-form.jsx
@@ -14,7 +14,7 @@ import idFromUrl from "../util/helpers/id-from-url";
import {
useGetV2LayoutsQuery,
enhancedApi,
-} from "../../../shared/redux/enhanced-api.ts";
+} from "../../redux/enhanced-api.ts";
import FormCheckbox from "../util/forms/form-checkbox";
import Preview from "../preview/preview";
import StickyFooter from "../util/sticky-footer";
diff --git a/assets/admin/components/screen/screen-list.jsx b/assets/admin/components/screen/screen-list.jsx
index 9c5c33dd2..5f6a4dc41 100644
--- a/assets/admin/components/screen/screen-list.jsx
+++ b/assets/admin/components/screen/screen-list.jsx
@@ -11,7 +11,7 @@ import useModal from "../../context/modal-context/modal-context-hook";
import {
useGetV2ScreensQuery,
useDeleteV2ScreensByIdMutation,
-} from "../../../shared/redux/enhanced-api.ts";
+} from "../../redux/enhanced-api.ts";
import {
displaySuccess,
displayError,
diff --git a/assets/admin/components/screen/screen-manager.jsx b/assets/admin/components/screen/screen-manager.jsx
index 0c141b493..930b8ad7f 100644
--- a/assets/admin/components/screen/screen-manager.jsx
+++ b/assets/admin/components/screen/screen-manager.jsx
@@ -4,7 +4,7 @@ import { useNavigate } from "react-router-dom";
import {
usePostV2ScreensMutation,
usePutV2ScreensByIdMutation,
-} from "../../../shared/redux/enhanced-api.ts";
+} from "../../redux/enhanced-api.ts";
import ScreenForm from "./screen-form";
import {
displaySuccess,
diff --git a/assets/admin/components/screen/screen-status.jsx b/assets/admin/components/screen/screen-status.jsx
index b6ee26645..543d28b1e 100644
--- a/assets/admin/components/screen/screen-status.jsx
+++ b/assets/admin/components/screen/screen-status.jsx
@@ -14,7 +14,7 @@ import {
import { useNavigate } from "react-router-dom";
import { useDispatch } from "react-redux";
import idFromUrl from "../util/helpers/id-from-url";
-import { enhancedApi } from "../../../shared/redux/enhanced-api.ts";
+import { enhancedApi } from "../../redux/enhanced-api.ts";
import { displayError } from "../util/list/toast-component/display-toast";
import FormInput from "../util/forms/form-input";
import AdminConfigLoader from "../util/admin-config-loader.js";
diff --git a/assets/admin/components/screen/util/campaign-icon.jsx b/assets/admin/components/screen/util/campaign-icon.jsx
new file mode 100644
index 000000000..8003d2a26
--- /dev/null
+++ b/assets/admin/components/screen/util/campaign-icon.jsx
@@ -0,0 +1,105 @@
+import { useEffect, useState } from "react";
+import { useDispatch } from "react-redux";
+import { useTranslation } from "react-i18next";
+import Spinner from "react-bootstrap/Spinner";
+import idFromUrl from "../../util/helpers/id-from-url";
+import calculateIsPublished from "../../util/helpers/calculate-is-published";
+import {
+ enhancedApi,
+ useGetV2ScreensByIdCampaignsQuery,
+ useGetV2ScreensByIdScreenGroupsQuery,
+} from "../../../redux/enhanced-api.ts";
+
+/**
+ * An icon to show if the screen has an active campaign.
+ *
+ * @param {object} props - The props.
+ * @param {string} props.id The id of the screen.
+ * @param {number} props.delay Delay the fetch.
+ * @returns {object} The campaign icon.
+ */
+function CampaignIcon({ id, delay = 1000 }) {
+ const { t } = useTranslation("common", { keyPrefix: "campaign-icon" });
+ const dispatch = useDispatch();
+ const [isOverriddenByCampaign, setIsOverriddenByCampaign] = useState(null);
+ const [screenCampaignsChecked, setScreenCampaignsChecked] = useState(false);
+ const [allCampaigns, setAllCampaigns] = useState([]);
+ const [getData, setGetData] = useState(false);
+
+ const { data: campaigns, isLoading } = useGetV2ScreensByIdCampaignsQuery(
+ { id },
+ { skip: !getData || !id },
+ );
+ const { data: groups, isLoading: isLoadingScreenGroups } =
+ useGetV2ScreensByIdScreenGroupsQuery({ id }, { skip: !getData || !id });
+
+ useEffect(() => {
+ if (campaigns) {
+ setAllCampaigns(
+ campaigns["hydra:member"].map(({ campaign }) => campaign),
+ );
+ setScreenCampaignsChecked(true);
+ }
+ }, [campaigns]);
+
+ useEffect(() => {
+ if (groups && !isOverriddenByCampaign && screenCampaignsChecked) {
+ groups["hydra:member"].forEach((group) => {
+ dispatch(
+ enhancedApi.endpoints.getV2ScreenGroupsByIdCampaigns.initiate({
+ id: idFromUrl(group["@id"]),
+ }),
+ ).then((result) => {
+ let allCampaignsCopy = [...allCampaigns];
+ if (allCampaignsCopy.length > 0 && result.data) {
+ allCampaignsCopy = allCampaignsCopy.concat(
+ result.data["hydra:member"].map(({ campaign }) => campaign),
+ );
+ }
+ setAllCampaigns(allCampaignsCopy);
+ });
+ });
+ }
+ }, [groups, screenCampaignsChecked]);
+
+ useEffect(() => {
+ if (allCampaigns.length > 0 && !isOverriddenByCampaign) {
+ allCampaigns.forEach(({ published }) => {
+ if (calculateIsPublished(published)) {
+ setIsOverriddenByCampaign(true);
+ }
+ });
+ }
+ }, [allCampaigns]);
+
+ useEffect(() => {
+ const timeout = setTimeout(() => {
+ setGetData(true);
+ }, delay);
+
+ return () => {
+ clearTimeout(timeout);
+ };
+ }, []);
+
+ if (!getData || isLoading || isLoadingScreenGroups) {
+ return (
+
+
+
+ );
+ }
+
+ return isOverriddenByCampaign
+ ? t("overridden-by-campaign")
+ : t("not-overridden-by-campaign");
+}
+
+export default CampaignIcon;
diff --git a/assets/admin/components/screen/util/campaigns-button.jsx b/assets/admin/components/screen/util/campaigns-button.jsx
index edb5657c3..c1b5e3dd3 100644
--- a/assets/admin/components/screen/util/campaigns-button.jsx
+++ b/assets/admin/components/screen/util/campaigns-button.jsx
@@ -6,8 +6,8 @@ import idFromUrl from "../../util/helpers/id-from-url.jsx";
import getAllPages from "../../util/helpers/get-all-pages.js";
import { useDispatch } from "react-redux";
import useModal from "../../../context/modal-context/modal-context-hook.jsx";
-import { enhancedApi } from "../../../../shared/redux/enhanced-api.ts";
import { Button } from "react-bootstrap";
+import { enhancedApi } from "../../../redux/enhanced-api.ts";
function getAllScreenGroupCampaigns(dispatch, screenGroupIds = []) {
return screenGroupIds.reduce(
diff --git a/assets/admin/components/screen/util/grid-generation-and-select.jsx b/assets/admin/components/screen/util/grid-generation-and-select.jsx
index f9e9b6140..1faa64c40 100644
--- a/assets/admin/components/screen/util/grid-generation-and-select.jsx
+++ b/assets/admin/components/screen/util/grid-generation-and-select.jsx
@@ -4,7 +4,7 @@ import Grid from "./grid";
import { useTranslation } from "react-i18next";
import idFromUrl from "../../util/helpers/id-from-url";
import PlaylistDragAndDrop from "../../playlist-drag-and-drop/playlist-drag-and-drop";
-import { enhancedApi } from "../../../../shared/redux/enhanced-api.ts";
+import { enhancedApi } from "../../../redux/enhanced-api.ts";
import useFetchDataHook from "../../util/fetch-data-hook.js";
import mapToIds from "../../util/helpers/map-to-ids.js";
import "./grid.scss";
diff --git a/assets/admin/components/screen/util/screen-groups-button.jsx b/assets/admin/components/screen/util/screen-groups-button.jsx
index dbd8d01b5..1fc696401 100644
--- a/assets/admin/components/screen/util/screen-groups-button.jsx
+++ b/assets/admin/components/screen/util/screen-groups-button.jsx
@@ -3,9 +3,9 @@ import idFromUrl from "../../util/helpers/id-from-url";
import getAllPages from "../../util/helpers/get-all-pages.js";
import { Button } from "react-bootstrap";
import useModal from "../../../context/modal-context/modal-context-hook.jsx";
-import { enhancedApi } from "../../../../shared/redux/enhanced-api.ts";
import { useDispatch } from "react-redux";
import { Link } from "react-router-dom";
+import { enhancedApi } from "../../../redux/enhanced-api.ts";
function ScreenGroupsButton({ screen }) {
const { t } = useTranslation("common", { keyPrefix: "screen-columns" });
diff --git a/assets/admin/components/slide/content/feed-selector.jsx b/assets/admin/components/slide/content/feed-selector.jsx
index 15ea1bfdc..2a05f419a 100644
--- a/assets/admin/components/slide/content/feed-selector.jsx
+++ b/assets/admin/components/slide/content/feed-selector.jsx
@@ -5,7 +5,7 @@ import { useDispatch } from "react-redux";
import {
enhancedApi,
useGetV2FeedSourcesQuery,
-} from "../../../../shared/redux/enhanced-api.ts";
+} from "../../../redux/enhanced-api.ts";
import MultiSelectComponent from "../../util/forms/multiselect-dropdown/multi-dropdown";
import idFromUrl from "../../util/helpers/id-from-url";
import ContentForm from "./content-form";
diff --git a/assets/admin/components/slide/content/media-selector-list.jsx b/assets/admin/components/slide/content/media-selector-list.jsx
index e247ab231..8e0c4d8ac 100644
--- a/assets/admin/components/slide/content/media-selector-list.jsx
+++ b/assets/admin/components/slide/content/media-selector-list.jsx
@@ -3,7 +3,7 @@ import { Col, Form, Row, Spinner } from "react-bootstrap";
import { useTranslation } from "react-i18next";
import SearchBox from "../../util/search-box/search-box";
import ContentBody from "../../util/content-body/content-body";
-import { useGetV2MediaQuery } from "../../../../shared/redux/enhanced-api.ts";
+import { useGetV2MediaQuery } from "../../../redux/enhanced-api.ts";
import "../../media/media-list.scss";
import Pagination from "../../util/paginate/pagination";
import FilePreview from "./file-preview";
diff --git a/assets/admin/components/slide/slide-edit.jsx b/assets/admin/components/slide/slide-edit.jsx
index bea68346c..0e20dfa0a 100644
--- a/assets/admin/components/slide/slide-edit.jsx
+++ b/assets/admin/components/slide/slide-edit.jsx
@@ -1,5 +1,5 @@
import { useParams } from "react-router-dom";
-import { useGetV2SlidesByIdQuery } from "../../../shared/redux/enhanced-api.ts";
+import { useGetV2SlidesByIdQuery } from "../../redux/enhanced-api.ts";
import SlideManager from "./slide-manager";
/**
diff --git a/assets/admin/components/slide/slide-form.jsx b/assets/admin/components/slide/slide-form.jsx
index 902d86c51..5be7068ba 100644
--- a/assets/admin/components/slide/slide-form.jsx
+++ b/assets/admin/components/slide/slide-form.jsx
@@ -10,7 +10,7 @@ import MultiSelectComponent from "../util/forms/multiselect-dropdown/multi-dropd
import {
useGetV2TemplatesQuery,
useGetV2ThemesQuery,
-} from "../../../shared/redux/enhanced-api.ts";
+} from "../../redux/enhanced-api.ts";
import idFromUrl from "../util/helpers/id-from-url";
import FormInput from "../util/forms/form-input";
import ContentForm from "./content/content-form";
diff --git a/assets/admin/components/slide/slide-manager.jsx b/assets/admin/components/slide/slide-manager.jsx
index eeee9320b..e1bb046a8 100644
--- a/assets/admin/components/slide/slide-manager.jsx
+++ b/assets/admin/components/slide/slide-manager.jsx
@@ -12,7 +12,7 @@ import {
usePostV2SlidesMutation,
usePutV2SlidesByIdPlaylistsMutation,
usePutV2SlidesByIdMutation,
-} from "../../../shared/redux/enhanced-api.ts";
+} from "../../redux/enhanced-api.ts";
import SlideForm from "./slide-form";
import {
displaySuccess,
@@ -292,7 +292,7 @@ function SlideManager({
localFormStateObject.media.forEach((media) => {
promises.push(
dispatch(
- enhancedApi.endpoints.getv2MediaById.initiate({
+ enhancedApi.endpoints.getV2MediaById.initiate({
id: idFromUrl(media),
}),
),
diff --git a/assets/admin/components/slide/slides-list.jsx b/assets/admin/components/slide/slides-list.jsx
index 56d385c52..f2908fb8e 100644
--- a/assets/admin/components/slide/slides-list.jsx
+++ b/assets/admin/components/slide/slides-list.jsx
@@ -16,7 +16,7 @@ import {
useGetV2SlidesQuery,
useDeleteV2SlidesByIdMutation,
useGetV2PlaylistsByIdQuery,
-} from "../../../shared/redux/enhanced-api.ts";
+} from "../../redux/enhanced-api.ts";
/**
* The slides list component.
diff --git a/assets/admin/components/themes/theme-edit.jsx b/assets/admin/components/themes/theme-edit.jsx
index 15870a906..ce533156d 100644
--- a/assets/admin/components/themes/theme-edit.jsx
+++ b/assets/admin/components/themes/theme-edit.jsx
@@ -1,5 +1,5 @@
import { useParams } from "react-router-dom";
-import { useGetV2ThemesByIdQuery } from "../../../shared/redux/enhanced-api.ts";
+import { useGetV2ThemesByIdQuery } from "../../redux/enhanced-api.ts";
import ThemeManager from "./theme-manager";
/**
diff --git a/assets/admin/components/themes/theme-manager.jsx b/assets/admin/components/themes/theme-manager.jsx
index 63ea6bef1..c2f19ea42 100644
--- a/assets/admin/components/themes/theme-manager.jsx
+++ b/assets/admin/components/themes/theme-manager.jsx
@@ -6,7 +6,7 @@ import {
usePostV2ThemesMutation,
usePutV2ThemesByIdMutation,
usePostMediaCollectionMutation,
-} from "../../../shared/redux/enhanced-api.ts";
+} from "../../redux/enhanced-api.ts";
import {
displaySuccess,
displayError,
diff --git a/assets/admin/components/themes/themes-list.jsx b/assets/admin/components/themes/themes-list.jsx
index b5a78e5d3..886cd7cd9 100644
--- a/assets/admin/components/themes/themes-list.jsx
+++ b/assets/admin/components/themes/themes-list.jsx
@@ -15,7 +15,7 @@ import {
import {
useGetV2ThemesQuery,
useDeleteV2ThemesByIdMutation,
-} from "../../../shared/redux/enhanced-api.ts";
+} from "../../redux/enhanced-api.ts";
/**
* The themes list component.
diff --git a/assets/admin/components/user/login.jsx b/assets/admin/components/user/login.jsx
index 26b802c78..25d04c87c 100644
--- a/assets/admin/components/user/login.jsx
+++ b/assets/admin/components/user/login.jsx
@@ -8,7 +8,7 @@ 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 { enhancedApi } from "../../../shared/redux/enhanced-api.ts";
+import { enhancedApi } from "../../redux/enhanced-api.ts";
import AdminConfigLoader from "../util/admin-config-loader.js";
import { displayError } from "../util/list/toast-component/display-toast";
import localStorageKeys from "../util/local-storage-keys";
diff --git a/assets/admin/components/users/users-list.jsx b/assets/admin/components/users/users-list.jsx
index 95ad37d99..40311dac2 100644
--- a/assets/admin/components/users/users-list.jsx
+++ b/assets/admin/components/users/users-list.jsx
@@ -16,7 +16,7 @@ import {
import {
useDeleteV2UsersByIdRemoveFromTenantMutation,
useGetV2UsersQuery,
-} from "../../../shared/redux/enhanced-api.ts";
+} from "../../redux/enhanced-api.ts";
/**
* The users list component.
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 316c591a7..472ea1d3f 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
@@ -5,7 +5,7 @@ import { SelectGroupColumns } from "../../groups/groups-columns";
import {
useGetV2ScreenGroupsQuery,
useGetV2ScreenGroupsByIdScreensQuery,
-} from "../../../../shared/redux/enhanced-api.ts";
+} from "../../../redux/enhanced-api.ts";
import GroupsDropdown from "../forms/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 89e9235fb..853a1e738 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
@@ -5,7 +5,7 @@ import {
enhancedApi,
useGetV2PlaylistsQuery,
useGetV2PlaylistsByIdSlidesQuery,
-} from "../../../../shared/redux/enhanced-api.ts";
+} from "../../../redux/enhanced-api.ts";
import PlaylistsDropdown from "../forms/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 1b743c588..9503ebcfd 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
@@ -7,7 +7,7 @@ import {
enhancedApi,
useGetV2ScreensQuery,
useGetV2ScreensByIdScreenGroupsQuery,
-} from "../../../../shared/redux/enhanced-api.ts";
+} from "../../../redux/enhanced-api.ts";
import useFetchDataHook from "../fetch-data-hook.js";
/**
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 43e1f894b..5e711a529 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
@@ -8,7 +8,7 @@ import {
useGetV2SlidesQuery,
useGetV2PlaylistsByIdQuery,
enhancedApi,
-} from "../../../../shared/redux/enhanced-api.ts";
+} from "../../../redux/enhanced-api.ts";
import PlaylistGanttChart from "../../playlist/playlist-gantt-chart";
import { displayWarning } from "../list/toast-component/display-toast";
import useFetchDataHook from "../fetch-data-hook.js";
diff --git a/assets/admin/components/util/template-label-in-list.jsx b/assets/admin/components/util/template-label-in-list.jsx
index a2975eb04..00602e91d 100644
--- a/assets/admin/components/util/template-label-in-list.jsx
+++ b/assets/admin/components/util/template-label-in-list.jsx
@@ -1,5 +1,5 @@
import Spinner from "react-bootstrap/Spinner";
-import { useGetV2TemplatesByIdQuery } from "../../../shared/redux/enhanced-api.ts";
+import { useGetV2TemplatesByIdQuery } from "../../redux/enhanced-api.ts";
import idFromUrl from "./helpers/id-from-url";
/**
* @param {object} props The props.
diff --git a/assets/admin/index.jsx b/assets/admin/index.jsx
index 824959ecf..397b42ead 100644
--- a/assets/admin/index.jsx
+++ b/assets/admin/index.jsx
@@ -1,7 +1,7 @@
import { BrowserRouter } from "react-router-dom";
import { createRoot } from "react-dom/client";
import { Provider } from "react-redux";
-import { store } from "../shared/redux/store.js";
+import { store } from "./redux/store.js";
import App from "./app";
const container = document.getElementById("root");
diff --git a/assets/shared/redux/extended-base-query.js b/assets/admin/redux/base-query.js
similarity index 97%
rename from assets/shared/redux/extended-base-query.js
rename to assets/admin/redux/base-query.js
index 999d3e3e8..f1fa1a77c 100644
--- a/assets/shared/redux/extended-base-query.js
+++ b/assets/admin/redux/base-query.js
@@ -1,5 +1,5 @@
import { fetchBaseQuery } from "@reduxjs/toolkit/query/react";
-import localStorageKeys from "../../admin/components/util/local-storage-keys.jsx";
+import localStorageKeys from "../components/util/local-storage-keys.jsx";
const extendedBaseQuery = async (args, api, extraOptions) => {
const baseUrl = "/";
diff --git a/assets/shared/redux/empty-api.ts b/assets/admin/redux/empty-api.ts
similarity index 74%
rename from assets/shared/redux/empty-api.ts
rename to assets/admin/redux/empty-api.ts
index 7e72803f7..f6076162f 100644
--- a/assets/shared/redux/empty-api.ts
+++ b/assets/admin/redux/empty-api.ts
@@ -1,5 +1,5 @@
import { createApi } from '@reduxjs/toolkit/query/react';
-import extendedBaseQuery from "./extended-base-query";
+import extendedBaseQuery from "./base-query";
export const emptySplitApi = createApi({
baseQuery: extendedBaseQuery,
diff --git a/assets/shared/redux/enhanced-api.ts b/assets/admin/redux/enhanced-api.ts
similarity index 100%
rename from assets/shared/redux/enhanced-api.ts
rename to assets/admin/redux/enhanced-api.ts
diff --git a/assets/shared/redux/generated-api.ts b/assets/admin/redux/generated-api.ts
similarity index 99%
rename from assets/shared/redux/generated-api.ts
rename to assets/admin/redux/generated-api.ts
index 63959ac3e..005a2151f 100644
--- a/assets/shared/redux/generated-api.ts
+++ b/assets/admin/redux/generated-api.ts
@@ -237,9 +237,9 @@ const injectedRtkApi = api
}),
invalidatesTags: ["Media"],
}),
- getv2MediaById: build.query<
- Getv2MediaByIdApiResponse,
- Getv2MediaByIdApiArg
+ getV2MediaById: build.query<
+ GetV2MediaByIdApiResponse,
+ GetV2MediaByIdApiArg
>({
query: (queryArg) => ({ url: `/v2/media/${queryArg.id}` }),
providesTags: ["Media"],
@@ -1144,8 +1144,8 @@ export type PostMediaCollectionApiArg = {
file: Blob;
};
};
-export type Getv2MediaByIdApiResponse = /** status 200 OK */ Blob;
-export type Getv2MediaByIdApiArg = {
+export type GetV2MediaByIdApiResponse = /** status 200 OK */ Blob;
+export type GetV2MediaByIdApiArg = {
id: string;
};
export type DeleteV2MediaByIdApiResponse = unknown;
@@ -2634,7 +2634,7 @@ export const {
useLoginCheckPostMutation,
useGetV2MediaQuery,
usePostMediaCollectionMutation,
- useGetv2MediaByIdQuery,
+ useGetV2MediaByIdQuery,
useDeleteV2MediaByIdMutation,
useGetV2CampaignsByIdScreenGroupsQuery,
useGetV2CampaignsByIdScreensQuery,
diff --git a/assets/shared/redux/openapi-config.js b/assets/admin/redux/openapi-config.js
similarity index 100%
rename from assets/shared/redux/openapi-config.js
rename to assets/admin/redux/openapi-config.js
diff --git a/assets/shared/redux/store.js b/assets/admin/redux/store.js
similarity index 100%
rename from assets/shared/redux/store.js
rename to assets/admin/redux/store.js
diff --git a/assets/client/app.css b/assets/client/app.css
deleted file mode 100644
index 837191701..000000000
--- a/assets/client/app.css
+++ /dev/null
@@ -1,3 +0,0 @@
-body {
- background-color: blue;
-}
diff --git a/assets/client/app.js b/assets/client/app.js
deleted file mode 100644
index 620d7555d..000000000
--- a/assets/client/app.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import "./app.css";
-
-console.log("Hello client");
diff --git a/assets/client/app.jsx b/assets/client/app.jsx
index 779f62bd3..4e02efa72 100644
--- a/assets/client/app.jsx
+++ b/assets/client/app.jsx
@@ -1,16 +1,18 @@
import { useEffect, useRef, useState } from "react";
import Screen from "./components/screen.jsx";
import ContentService from "./service/content-service";
-import ClientConfigLoader from "./util/client-config-loader.js";
-import logger from "./logger/logger";
+import ClientConfigLoader from "./core/client-config-loader.js";
+import logger from "./core/logger.js";
import fallback from "./assets/fallback.png";
-import appStorage from "./util/app-storage";
+import appStorage from "./core/app-storage";
import defaults from "./util/defaults";
import tokenService from "./service/token-service";
import releaseService from "./service/release-service";
import tenantService from "./service/tenant-service";
import statusService from "./service/status-service";
import constants from "./util/constants";
+import reauthenticateRef from "./redux/reauthenticate-ref";
+import { useClientState } from "./client-state-context.jsx";
import "./app.scss";
/**
@@ -24,19 +26,23 @@ import "./app.scss";
*/
function App({ preview, previewId }) {
const [running, setRunning] = useState(false);
- const [screen, setScreen] = useState("");
const [bindKey, setBindKey] = useState(null);
- const [displayFallback, setDisplayFallback] = useState(true);
const [debug, setDebug] = useState(false);
+ const { screen, isContentEmpty, callbacks } = useClientState();
+
const checkLoginTimeoutRef = useRef(null);
+ const loginTimeoutGenRef = useRef(0);
const contentServiceRef = useRef(null);
+ const runningRef = useRef(false);
+ const reauthenticatingRef = useRef(false);
const fallbackImageUrl = appStorage.getFallbackImageUrl();
+ const safeFallbackUrl = (
+ fallbackImageUrl !== null ? fallbackImageUrl : fallback
+ ).replace(/"/g, "");
const fallbackStyle = {
- backgroundImage: `url("${
- fallbackImageUrl !== null ? fallbackImageUrl : fallback
- }")`,
+ backgroundImage: `url("${safeFallbackUrl}")`,
};
const appStyle = {};
@@ -45,20 +51,6 @@ function App({ preview, previewId }) {
appStyle.cursor = "none";
}
- /**
- * Handles "screen" events.
- *
- * @param {CustomEvent} event
- * The event.
- */
- function screenHandler(event) {
- const screenData = event.detail?.screen;
-
- if (screenData !== null) {
- setScreen(screenData);
- }
- }
-
const startContent = (localScreenId) => {
logger.info("Starting content.");
@@ -70,22 +62,17 @@ function App({ preview, previewId }) {
}
setBindKey(null);
+ runningRef.current = true;
setRunning(true);
- contentServiceRef.current = new ContentService();
+ contentServiceRef.current = new ContentService(callbacks);
// Start the content service.
contentServiceRef.current.start();
const entrypoint = `/v2/screens/${localScreenId}`;
-
- document.dispatchEvent(
- new CustomEvent("startDataSync", {
- detail: {
- screenPath: entrypoint,
- },
- }),
- );
+ contentServiceRef.current.stopSync();
+ contentServiceRef.current.startSyncing(entrypoint);
tokenService.startRefreshing();
};
@@ -94,9 +81,14 @@ function App({ preview, previewId }) {
const restartLoginTimeout = () => {
if (checkLoginTimeoutRef.current !== null) {
clearTimeout(checkLoginTimeoutRef.current);
+ checkLoginTimeoutRef.current = null;
}
+ loginTimeoutGenRef.current += 1;
+ const gen = loginTimeoutGenRef.current;
+
ClientConfigLoader.loadConfig().then((config) => {
+ if (gen !== loginTimeoutGenRef.current) return;
checkLoginTimeoutRef.current = setTimeout(
checkLogin,
config.loginCheckTimeout ?? defaults.loginCheckTimeoutDefault,
@@ -111,7 +103,7 @@ function App({ preview, previewId }) {
const localStorageToken = appStorage.getToken();
const localScreenId = appStorage.getScreenId();
- if (!running && localStorageToken && localScreenId) {
+ if (!runningRef.current && localStorageToken && localScreenId) {
startContent(localScreenId);
} else {
statusService.setStatus(constants.STATUS_LOGIN);
@@ -130,12 +122,16 @@ function App({ preview, previewId }) {
}
})
.catch(() => {
+ logger.warn("Failed to check login.");
restartLoginTimeout();
});
}
};
const reauthenticateHandler = () => {
+ if (reauthenticatingRef.current) return;
+ reauthenticatingRef.current = true;
+
logger.info("Reauthenticate handler invoked. Trying to use refresh token.");
tokenService
@@ -148,7 +144,11 @@ function App({ preview, previewId }) {
statusService.setError(constants.ERROR_TOKEN_REFRESH_FAILED);
- document.dispatchEvent(new Event("stopDataSync"));
+ if (contentServiceRef.current !== null) {
+ contentServiceRef.current.stopSync();
+ contentServiceRef.current.stop();
+ contentServiceRef.current = null;
+ }
appStorage.clearToken();
appStorage.clearRefreshToken();
@@ -156,30 +156,19 @@ function App({ preview, previewId }) {
appStorage.clearTenant();
appStorage.clearFallbackImageUrl();
- if (contentServiceRef?.current !== null) {
- contentServiceRef.current.stop();
- contentServiceRef.current = null;
- }
-
- setScreen(null);
+ callbacks.current.setScreen(null);
+ runningRef.current = false;
setRunning(false);
tokenService.stopRefreshing();
checkLogin();
+ })
+ .finally(() => {
+ reauthenticatingRef.current = false;
});
};
- const contentEmpty = () => {
- logger.info("Content empty. Displaying fallback.");
- setDisplayFallback(true);
- };
-
- const contentNotEmpty = () => {
- logger.info("Content not empty. Displaying content.");
- setDisplayFallback(false);
- };
-
// ctrl/cmd i will log screen out and refresh
const handleKeyboard = ({ repeat, metaKey, ctrlKey, code }) => {
if (!repeat && (metaKey || ctrlKey) && code === "KeyI") {
@@ -191,40 +180,32 @@ function App({ preview, previewId }) {
useEffect(() => {
logger.info("Mounting App.");
if (preview !== null) {
- document.addEventListener("screen", screenHandler);
- document.addEventListener("contentEmpty", contentEmpty);
- document.addEventListener("contentNotEmpty", contentNotEmpty);
-
if (preview === "screen") {
startContent(previewId);
- return;
+ } else {
+ setRunning(true);
+ contentServiceRef.current = new ContentService(callbacks);
+ contentServiceRef.current.start();
+ contentServiceRef.current.startPreview(preview, previewId);
}
- setRunning(true);
- contentServiceRef.current = new ContentService();
- contentServiceRef.current.start();
- document.dispatchEvent(
- new CustomEvent("startPreview", {
- detail: {
- mode: preview,
- id: previewId,
- },
- }),
- );
} else {
- document.addEventListener("keypress", handleKeyboard);
- document.addEventListener("screen", screenHandler);
- document.addEventListener("reauthenticate", reauthenticateHandler);
- document.addEventListener("contentEmpty", contentEmpty);
- document.addEventListener("contentNotEmpty", contentNotEmpty);
+ document.addEventListener("keydown", handleKeyboard);
+
+ // Wire up reauthenticate callback for base-query (outside React tree).
+ reauthenticateRef.current = reauthenticateHandler;
tokenService.checkToken();
- ClientConfigLoader.loadConfig().then((config) => {
- setDebug(config.debug ?? false);
+ ClientConfigLoader.loadConfig()
+ .then((config) => {
+ setDebug(config.debug ?? false);
- const relationChecksumEnabled = config.relationsChecksumEnabled;
- logger.info(`Relation checksum enabled: ${relationChecksumEnabled}`);
- });
+ const relationChecksumEnabled = config.relationsChecksumEnabled;
+ logger.info(`Relation checksum enabled: ${relationChecksumEnabled}`);
+ })
+ .catch((err) => {
+ logger.error(`Failed to load config: ${err}`);
+ });
releaseService.checkForNewRelease().finally(() => {
releaseService.setPreviousBootInUrl();
@@ -238,23 +219,26 @@ function App({ preview, previewId }) {
statusService.setStatusInUrl();
}
- /* eslint-disable-next-line consistent-return */
return function cleanup() {
logger.info("Unmounting App.");
- document.removeEventListener("keypress", handleKeyboard);
- document.removeEventListener("screen", screenHandler);
- document.removeEventListener("reauthenticate", reauthenticateHandler);
- document.removeEventListener("contentEmpty", contentEmpty);
- document.removeEventListener("contentNotEmpty", contentNotEmpty);
-
- if (checkLoginTimeoutRef?.current) {
- clearTimeout(checkLoginTimeoutRef.current);
+ if (contentServiceRef.current !== null) {
+ contentServiceRef.current.stopSync();
+ contentServiceRef.current.stop();
+ contentServiceRef.current = null;
}
- tokenService.stopRefreshing();
+ if (preview === null) {
+ document.removeEventListener("keydown", handleKeyboard);
+ reauthenticateRef.current = () => {};
- releaseService.stopReleaseCheck();
+ if (checkLoginTimeoutRef.current) {
+ clearTimeout(checkLoginTimeoutRef.current);
+ }
+
+ tokenService.stopRefreshing();
+ releaseService.stopReleaseCheck();
+ }
};
}, []);
@@ -282,7 +266,7 @@ function App({ preview, previewId }) {
>
)}
- {displayFallback && !bindKey && (
+ {isContentEmpty && !bindKey && (
)}
diff --git a/assets/client/app.scss b/assets/client/app.scss
index 2f68c5a8e..162b7a6a3 100644
--- a/assets/client/app.scss
+++ b/assets/client/app.scss
@@ -52,10 +52,6 @@ body {
}
}
-.ql-editor {
- min-height: 200px;
-}
-
.fallback {
height: 100%;
width: 100%;
diff --git a/assets/client/client-state-context.jsx b/assets/client/client-state-context.jsx
new file mode 100644
index 000000000..b29e7e517
--- /dev/null
+++ b/assets/client/client-state-context.jsx
@@ -0,0 +1,47 @@
+import { createContext, useState, useRef, useContext, useCallback } from "react";
+
+const ClientStateContext = createContext();
+
+/**
+ * Provider that holds client state previously communicated via document events.
+ * Services receive the callbacks ref at construction and call them directly.
+ */
+function ClientStateProvider({ children }) {
+ const [screen, setScreen] = useState(null);
+ const [isContentEmpty, setIsContentEmpty] = useState(false);
+ const [regionSlides, setRegionSlides] = useState({});
+
+ const updateRegionSlides = useCallback((regionId, slides) => {
+ setRegionSlides((prev) => ({ ...prev, [regionId]: slides }));
+ }, []);
+
+ // Stable callbacks ref — services hold a reference to this object
+ // and call its methods instead of dispatching document events.
+ const callbacks = useRef({
+ setScreen,
+ setIsContentEmpty,
+ updateRegionSlides,
+ onRegionReady: () => {},
+ onRegionRemoved: () => {},
+ onReauthenticate: () => {},
+ });
+
+ const value = {
+ screen,
+ isContentEmpty,
+ regionSlides,
+ callbacks,
+ };
+
+ return (
+
+ {children}
+
+ );
+}
+
+function useClientState() {
+ return useContext(ClientStateContext);
+}
+
+export { ClientStateProvider, useClientState };
diff --git a/assets/client/components/error-boundary.jsx b/assets/client/components/error-boundary.jsx
index 98aeb0ac0..54259f111 100644
--- a/assets/client/components/error-boundary.jsx
+++ b/assets/client/components/error-boundary.jsx
@@ -1,5 +1,5 @@
import { Component } from "react";
-import logger from "../logger/logger";
+import logger from "../core/logger.js";
import fallback from "../assets/fallback.png";
import "./error-boundary.scss";
@@ -14,6 +14,12 @@ class ErrorBoundary extends Component {
return { hasError: true };
}
+ componentDidUpdate(prevProps) {
+ if (this.props.resetKey !== prevProps.resetKey && this.state.hasError) {
+ this.setState({ hasError: false, errorMessage: null, errorStackTrace: null });
+ }
+ }
+
componentDidCatch(error, errorInfo) {
logger.error(`ErrorBoundary caught error: ${error}`, errorInfo);
diff --git a/assets/client/components/region.jsx b/assets/client/components/region.jsx
index 878350ba7..2cb0605da 100644
--- a/assets/client/components/region.jsx
+++ b/assets/client/components/region.jsx
@@ -1,10 +1,12 @@
-import { useEffect, useState, createRef } from "react";
+import { useEffect, useState, createRef, useRef } from "react";
import { createGridArea } from "../../shared/grid-generator/grid-generator";
import { TransitionGroup, CSSTransition } from "react-transition-group";
import ErrorBoundary from "./error-boundary.jsx";
import idFromPath from "../util/id-from-path";
-import logger from "../logger/logger";
+import logger from "../core/logger.js";
import Slide from "./slide.jsx";
+import constants from "../util/constants";
+import { useClientState } from "../client-state-context.jsx";
import "./region.scss";
/**
@@ -24,8 +26,17 @@ function Region({ region }) {
const [nodeRefs, setNodeRefs] = useState({});
const [runId, setRunId] = useState(null);
+ // Refs to avoid stale closures in slideDone — templates capture slideDone
+ // in a useEffect([run]) that won't re-run when slides/newSlides change.
+ const slidesRef = useRef(null);
+ const newSlidesRef = useRef(null);
+ slidesRef.current = slides;
+ newSlidesRef.current = newSlides;
+
+ const { regionSlides, callbacks } = useClientState();
const rootStyle = {};
const regionId = idFromPath(region["@id"]);
+ const incomingSlides = regionSlides[regionId];
rootStyle.gridArea = createGridArea(region.gridArea);
@@ -38,14 +49,20 @@ function Region({ region }) {
* The slide.
*/
function findNextSlide(fromId) {
- const slideIndex = slides.findIndex(
+ const currentSlides = slidesRef.current;
+
+ if (!currentSlides || currentSlides.length === 0) {
+ return { nextSlide: null, nextIndex: 0 };
+ }
+
+ const slideIndex = currentSlides.findIndex(
(slideElement) => slideElement.executionId === fromId,
);
- const nextIndex = (slideIndex + 1) % slides.length;
+ const nextIndex = (slideIndex + 1) % currentSlides.length;
return {
- nextSlide: slides[nextIndex],
+ nextSlide: currentSlides[nextIndex],
nextIndex,
};
}
@@ -57,12 +74,13 @@ function Region({ region }) {
*/
const slideDone = (slide) => {
const nextSlideAndIndex = findNextSlide(slide.executionId);
+ const latestNewSlides = newSlidesRef.current;
- if (nextSlideAndIndex.nextIndex === 0 && Array.isArray(newSlides)) {
- const nextSlides = [...newSlides];
+ if (nextSlideAndIndex.nextIndex === 0 && Array.isArray(latestNewSlides)) {
+ const nextSlides = [...latestNewSlides];
setSlides(nextSlides);
setNewSlides(null);
- setCurrentSlide(nextSlides[0]);
+ setCurrentSlide(nextSlides.length > 0 ? nextSlides[0] : null);
} else {
setCurrentSlide(nextSlideAndIndex.nextSlide);
}
@@ -70,15 +88,6 @@ function Region({ region }) {
setRunId(new Date().toISOString());
logger.info(`Slide done with executionId: ${slide?.executionId}`);
-
- // Emit slideDone event.
- const slideDoneEvent = new CustomEvent("slideDone", {
- detail: {
- regionId,
- executionId: slide.executionId,
- },
- });
- document.dispatchEvent(slideDoneEvent);
};
/**
@@ -88,70 +97,42 @@ function Region({ region }) {
*/
const slideError = (slideWithError) => {
// Set error timestamp to force reload.
- const slide = slides.find(
- (slideElement) => slideElement.executionId === slideWithError.executionId,
+ setSlides((prev) =>
+ prev.map((s) =>
+ s.executionId === slideWithError.executionId
+ ? { ...s, errorTimestamp: new Date().getTime() }
+ : s,
+ ),
);
- slide.errorTimestamp = new Date().getTime();
slideDone(slideWithError);
};
- /**
- * Handle region content event.
- *
- * @param {CustomEvent} event
- * The event. The data is contained in detail.
- */
- function regionContentListener(event) {
- const receivedSlides = [...event.detail.slides];
+ // Receive region slides from context.
+ useEffect(() => {
+ if (!incomingSlides) return;
- // Filter out invalid slides.
+ const receivedSlides = [...incomingSlides];
setNewSlides(receivedSlides.filter((slide) => !slide.invalid));
- }
+ }, [incomingSlides]);
- // Setup event listener for region content.
+ // Notify lifecycle on mount/unmount.
useEffect(() => {
logger.info(`Mounting region ${regionId}`);
-
- document.addEventListener(
- `regionContent-${regionId}`,
- regionContentListener,
- );
+ callbacks.current.onRegionReady(regionId);
return function cleanup() {
logger.info(`Unmounting region ${regionId}`);
-
- // Emit event that region has been removed.
- const event = new CustomEvent("regionRemoved", {
- detail: {
- id: regionId,
- },
- });
- document.dispatchEvent(event);
-
- // Cleanup event listener.
- document.removeEventListener(
- `regionContent-${regionId}`,
- regionContentListener,
- );
+ callbacks.current.onRegionRemoved(regionId);
};
- }, []);
-
- // Notify that region is ready.
- useEffect(() => {
- const event = new CustomEvent("regionReady", {
- detail: {
- id: regionId,
- },
- });
- document.dispatchEvent(event);
- }, [region]);
+ }, [regionId]);
// Start the progress if no slide is currently playing.
useEffect(() => {
if (newSlides !== null && !currentSlide) {
setSlides(newSlides);
+ setNewSlides(null);
}
- }, [newSlides]);
+ }, [newSlides, currentSlide]);
// Make sure current slide is set.
useEffect(() => {
@@ -172,17 +153,17 @@ function Region({ region }) {
return res;
}, {}),
);
- }, [slides]);
+ }, [slides, currentSlide]);
return (
-
+
<>
{currentSlide && (
diff --git a/assets/client/components/screen.jsx b/assets/client/components/screen.jsx
index 2560f6d17..de3378a2e 100644
--- a/assets/client/components/screen.jsx
+++ b/assets/client/components/screen.jsx
@@ -1,10 +1,11 @@
-import { Fragment, useEffect, useRef } from "react";
+import { useEffect, useRef } from "react";
import SunCalc from "suncalc";
import { createGrid } from "../../shared/grid-generator/grid-generator";
import Region from "./region.jsx";
-import logger from "../logger/logger";
+import logger from "../core/logger.js";
import TouchRegion from "./touch-region.jsx";
-import ClientConfigLoader from "../util/client-config-loader.js";
+import ClientConfigLoader from "../core/client-config-loader.js";
+import constants from "../util/constants";
import "./screen.scss";
/**
@@ -26,8 +27,8 @@ function Screen({ screen }) {
const rootStyle = {
gridTemplateAreas: createGrid(configColumns, configRows),
- gridTemplateColumns: gridTemplateRows,
- gridTemplateRows: gridTemplateColumns,
+ gridTemplateColumns,
+ gridTemplateRows,
};
const refreshColorScheme = () => {
@@ -72,16 +73,16 @@ function Screen({ screen }) {
if (screen?.enableColorSchemeChange) {
logger.info("Enabling color scheme change.");
refreshColorScheme();
- // Refresh color scheme every 5 minutes.
colorSchemeIntervalRef.current = setInterval(
refreshColorScheme,
- 5 * 60 * 1000,
+ constants.COLOR_SCHEME_REFRESH_INTERVAL,
);
}
return () => {
if (colorSchemeIntervalRef.current !== null) {
clearInterval(colorSchemeIntervalRef.current);
+ colorSchemeIntervalRef.current = null;
}
// Cleanup html root classes.
@@ -90,22 +91,16 @@ function Screen({ screen }) {
"color-scheme-dark",
);
};
- }, [screen]);
+ }, [screen?.enableColorSchemeChange]);
return (
- {screen?.layoutData?.regions?.map((region) => (
-
- {/* Default region type */}
- {(!region.type || region.type === "default") && (
-
- )}
- {/* Special region type: touch-buttons */}
- {region?.type === "touch-buttons" && (
-
- )}
-
- ))}
+ {screen?.layoutData?.regions?.map((region) => {
+ if (region?.type === "touch-buttons") {
+ return ;
+ }
+ return ;
+ })}
);
}
diff --git a/assets/client/components/slide.jsx b/assets/client/components/slide.jsx
index bf5b76d59..ecd7b3db6 100644
--- a/assets/client/components/slide.jsx
+++ b/assets/client/components/slide.jsx
@@ -1,6 +1,8 @@
+import { useEffect, useRef } from "react";
import ErrorBoundary from "./error-boundary.jsx";
-import logger from "../logger/logger";
+import logger from "../core/logger.js";
import { renderSlide } from "../../shared/slide-utils/templates.js";
+import constants from "../util/constants";
import "./slide.scss";
/**
@@ -14,7 +16,17 @@ import "./slide.scss";
* @param {Function} props.slideError - Callback when slide encountered an error.
* @returns {object} - The component.
*/
-function Slide({ slide, id, run, slideDone, slideError }) {
+function Slide({ slide, id, run, slideDone, slideError, forwardRef }) {
+ const errorTimeoutRef = useRef(null);
+
+ useEffect(() => {
+ return () => {
+ if (errorTimeoutRef.current !== null) {
+ clearTimeout(errorTimeoutRef.current);
+ }
+ };
+ }, []);
+
/**
* Handle errors in ErrorBoundary.
*
@@ -23,13 +35,14 @@ function Slide({ slide, id, run, slideDone, slideError }) {
const handleError = () => {
logger.warn("Slide error boundary triggered.");
- setTimeout(() => {
+ errorTimeoutRef.current = setTimeout(() => {
slideError(slide);
- }, 5000);
+ }, constants.SLIDE_ERROR_RECOVERY_TIMEOUT);
};
return (
{
// Set error timestamp to force reload.
- const slide = slides.find(
- (slideElement) => slideElement.executionId === slideWithError.executionId,
+ setSlides((prev) =>
+ prev.map((s) =>
+ s.executionId === slideWithError.executionId
+ ? { ...s, errorTimestamp: new Date().getTime() }
+ : s,
+ ),
);
- slide.errorTimestamp = new Date().getTime();
slideDone(slideWithError);
};
- /**
- * Handle region content event.
- *
- * @param {CustomEvent} event
- * The event. The data is contained in detail.
- */
- function regionContentListener(event) {
- setSlides([...event.detail.slides]);
- }
+ // Receive region slides from context.
+ useEffect(() => {
+ if (!incomingSlides) return;
+
+ setSlides([...incomingSlides].filter((slide) => !slide.invalid));
+ }, [incomingSlides]);
- // Setup event listener for region content.
+ // Notify lifecycle on mount/unmount.
useEffect(() => {
- document.addEventListener(
- `regionContent-${regionId}`,
- regionContentListener,
- );
+ callbacks.current.onRegionReady(regionId);
return function cleanup() {
- // Emit event that region has been removed.
- const event = new CustomEvent("regionRemoved", {
- detail: {
- id: regionId,
- },
- });
- document.dispatchEvent(event);
-
- // Cleanup event listener.
- document.removeEventListener(
- `regionContent-${regionId}`,
- regionContentListener,
- );
+ callbacks.current.onRegionRemoved(regionId);
};
- }, []);
-
- // Notify that region is ready.
- useEffect(() => {
- const event = new CustomEvent("regionReady", {
- detail: {
- id: regionId,
- },
- });
- document.dispatchEvent(event);
- }, [region]);
+ }, [regionId]);
// Make sure current slide is set.
useEffect(() => {
@@ -128,7 +98,7 @@ function TouchRegion({ region }) {
return (
-
+
<>
{currentSlide !== null && (
@@ -149,8 +119,8 @@ function TouchRegion({ region }) {
{displayClose && (
slideDone(currentSlide)}
+ onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") slideDone(currentSlide); }}
role="button"
tabIndex={0}
>
@@ -171,7 +141,7 @@ function TouchRegion({ region }) {
className="touch-button"
key={`button-${slide.executionId}`}
onClick={() => startSlide(slide)}
- onKeyDown={() => startSlide(slide)}
+ onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") startSlide(slide); }}
role="button"
tabIndex={0}
>
diff --git a/assets/client/core/api-query.js b/assets/client/core/api-query.js
new file mode 100644
index 000000000..e26feaa2a
--- /dev/null
+++ b/assets/client/core/api-query.js
@@ -0,0 +1,89 @@
+import logger from "./logger.js";
+import { clientStore } from "../redux/store.js";
+import { clientApi } from "../redux/enhanced-api.ts";
+import defaults from "../util/defaults.js";
+
+/**
+ * Dispatch an RTK Query endpoint and return the unwrapped result.
+ *
+ * @param {string} endpoint The endpoint name.
+ * @param {object} args The endpoint args.
+ * @param {boolean} forceRefetch Whether to bypass RTK Query cache.
+ * @returns {Promise
} The result data.
+ */
+export function query(endpoint, args, forceRefetch = false) {
+ const request = clientStore.dispatch(
+ clientApi.endpoints[endpoint].initiate(args, { forceRefetch }),
+ );
+
+ let timeoutId;
+ const timeout = new Promise((_, reject) => {
+ timeoutId = setTimeout(() => {
+ request.abort();
+ reject(new Error(`Request timeout: ${endpoint}`));
+ }, defaults.queryTimeoutDefault);
+ });
+
+ return Promise.race([request.unwrap(), timeout])
+ .catch((err) => {
+ const cached = clientApi.endpoints[endpoint].select(args)(
+ clientStore.getState(),
+ );
+ if (cached?.data) {
+ logger.warn(`Using cached data for ${endpoint} after fetch failure.`);
+ return cached.data;
+ }
+ throw err;
+ })
+ .finally(() => {
+ clearTimeout(timeoutId);
+ request.unsubscribe();
+ });
+}
+
+/**
+ * Fetch all pages from a paginated endpoint.
+ *
+ * @param {string} endpoint The endpoint name.
+ * @param {object} args The endpoint args (page will be added).
+ * @param {boolean} forceRefetch Whether to bypass RTK Query cache.
+ * @returns {Promise} All hydra:member results concatenated.
+ */
+// Upper bound on pagination — intentionally capped. Content types served to
+// screens should never exceed this number of pages.
+const MAX_PAGES = 50;
+
+export async function queryAllPages(endpoint, args, forceRefetch = false) {
+ let results = [];
+ let page = 1;
+
+ do {
+ try {
+ const responseData = await query(endpoint, { ...args, page }, forceRefetch);
+
+ if (responseData === null || responseData === undefined) {
+ logger.error(`Failed to fetch page ${page} for ${endpoint}`);
+ return results;
+ }
+
+ results = results.concat(responseData["hydra:member"] ?? []);
+
+ if (responseData["hydra:view"]?.["hydra:next"]) {
+ page += 1;
+ } else {
+ break;
+ }
+ } catch (err) {
+ logger.error(
+ `Failed to fetch all pages for ${endpoint}: ${err.message}`,
+ );
+ return results;
+ }
+ } while (page <= MAX_PAGES);
+
+ if (page > MAX_PAGES) {
+ logger.warn(`Reached max page limit (${MAX_PAGES}) for ${endpoint}`);
+ }
+
+ return results;
+}
diff --git a/assets/client/util/app-storage.js b/assets/client/core/app-storage.js
similarity index 100%
rename from assets/client/util/app-storage.js
rename to assets/client/core/app-storage.js
diff --git a/assets/client/core/client-config-loader.js b/assets/client/core/client-config-loader.js
new file mode 100644
index 000000000..d5a2df3b7
--- /dev/null
+++ b/assets/client/core/client-config-loader.js
@@ -0,0 +1,84 @@
+import appStorage from "./app-storage.js";
+import logger from "./logger.js";
+import defaults from "../util/defaults.js";
+
+// Defaults.
+let configData = null;
+
+// Last time the config was fetched.
+let latestFetchTimestamp = 0;
+
+let activePromise = null;
+
+const ClientConfigLoader = {
+ async loadConfig() {
+ if (activePromise) {
+ return activePromise;
+ }
+
+ const nowTimestamp = new Date().getTime();
+
+ // Return cached data directly — no promise, so activePromise stays null
+ // and a future call after the cache interval will trigger a real fetch.
+ if (
+ configData !== null &&
+ latestFetchTimestamp +
+ (configData?.configFetchInterval ?? defaults.configFetchIntervalDefault) >=
+ nowTimestamp
+ ) {
+ return configData;
+ }
+
+ activePromise = new Promise((resolve) => {
+ fetch(`/config/client`)
+ .then((response) => response.json())
+ .then((data) => {
+ latestFetchTimestamp = new Date().getTime();
+ configData = data;
+
+ // Make api endpoint available through localstorage.
+ appStorage.setApiUrl(configData.apiEndpoint);
+
+ resolve(configData);
+ })
+ .catch(() => {
+ if (configData !== null) {
+ resolve(configData);
+ } else {
+ logger.error("Could not load config. Will use default config.");
+
+ // Default config.
+ resolve({
+ apiEndpoint: "/api",
+ dataStrategy: {
+ type: "pull",
+ config: {
+ interval: 30000,
+ },
+ },
+ loginCheckTimeout: 20000,
+ configFetchInterval: 900000,
+ refreshTokenTimeout: 15000,
+ releaseTimestampIntervalTimeout: 600000,
+ colorScheme: {
+ type: "library",
+ lat: 56.0,
+ lng: 10.0,
+ },
+ schedulingInterval: 60000,
+ debug: false,
+ });
+ }
+ })
+ .finally(() => {
+ activePromise = null;
+ });
+ });
+
+ return activePromise;
+ },
+};
+
+Object.freeze(ClientConfigLoader);
+
+export default ClientConfigLoader;
diff --git a/assets/client/util/local-storage-keys.js b/assets/client/core/local-storage-keys.js
similarity index 100%
rename from assets/client/util/local-storage-keys.js
rename to assets/client/core/local-storage-keys.js
diff --git a/assets/client/logger/logger.js b/assets/client/core/logger.js
similarity index 100%
rename from assets/client/logger/logger.js
rename to assets/client/core/logger.js
diff --git a/assets/client/data-sync/api-helper.js b/assets/client/data-sync/api-helper.js
deleted file mode 100644
index eb94e5aef..000000000
--- a/assets/client/data-sync/api-helper.js
+++ /dev/null
@@ -1,109 +0,0 @@
-import logger from "../logger/logger";
-import appStorage from "../util/app-storage";
-
-class ApiHelper {
- endpoint = "";
-
- /**
- * Constructor.
- *
- * @param {string} endpoint The API endpoint.
- */
- constructor(endpoint) {
- this.endpoint = endpoint;
- }
-
- /**
- * Get result from path.
- *
- * @param {string} path Path to the resource.
- * @returns {Promise} Promise with data.
- */
- async getPath(path) {
- if (!path) {
- throw new Error("No path");
- }
-
- let response;
-
- try {
- const url = new URL(window.location.href);
- const previewToken = url.searchParams.get("preview-token");
- const previewTenant = url.searchParams.get("preview-tenant");
-
- logger.log("info", `Fetching: ${this.endpoint + path}`);
-
- const token = appStorage.getToken();
- const tenantKey = appStorage.getTenantKey();
-
- if ((!token || !tenantKey) && (!previewToken || !previewTenant)) {
- logger.error("Token or tenantKey not set.");
-
- return null;
- }
-
- response = await fetch(this.endpoint + path, {
- headers: {
- authorization: `Bearer ${previewToken ?? token}`,
- "Authorization-Tenant-Key": previewTenant ?? tenantKey,
- },
- });
-
- if (response.ok === false) {
- // TODO: Change to a better strategy for triggering reauthenticate.
- if (response.status === 401) {
- document.dispatchEvent(new Event("reauthenticate"));
- }
-
- logger.error(
- `Failed to fetch (status: ${response.status}): ${
- this.endpoint + path
- }`,
- );
-
- return null;
- }
-
- return response.json();
- } catch (err) {
- logger.error(`Failed to fetch: ${this.endpoint + path}`);
-
- return null;
- }
- }
-
- /**
- * Gets all resources from the given path.
- *
- * @param {string} path Path to the resources.
- * @param {object} keys Keys that should be passed along with the result.
- * @returns {Promise<*>} Promise with all resources from a path.
- */
- async getAllResultsFromPath(path, keys = {}) {
- let results = [];
- let nextPath = `${path}`;
- let continueLoop = false;
- let page = 1;
-
- do {
- try {
- // eslint-disable-next-line no-await-in-loop
- const responseData = await this.getPath(nextPath);
- results = results.concat(responseData["hydra:member"]);
- if (results.length < responseData["hydra:totalItems"]) {
- page += 1;
- continueLoop = true;
- nextPath = `${path}?page=${page}`;
- } else {
- continueLoop = false;
- }
- } catch (err) {
- return {};
- }
- } while (continueLoop);
-
- return { path, results, keys };
- }
-}
-
-export default ApiHelper;
diff --git a/assets/client/data-sync/pull-strategy.js b/assets/client/data-sync/pull-strategy.js
deleted file mode 100644
index 5b4ea7eed..000000000
--- a/assets/client/data-sync/pull-strategy.js
+++ /dev/null
@@ -1,487 +0,0 @@
-import isPublished from "../util/isPublished";
-import logger from "../logger/logger";
-import ApiHelper from "./api-helper";
-import { cloneDeep } from "lodash";
-import ClientConfigLoader from "../util/client-config-loader.js";
-
-/**
- * PullStrategy.
- *
- * Handles pull strategy.
- */
-class PullStrategy {
- lastestScreenData;
-
- // Helper for all api calls.
- apiHelper;
-
- // Fetch-interval in ms.
- interval;
-
- // Path to screen that should be loaded data for.
- entryPoint = "";
-
- /**
- * Constructor.
- *
- * @param {object} config
- * The config object.
- */
- constructor(config) {
- this.start = this.start.bind(this);
- this.stop = this.stop.bind(this);
- this.getScreen = this.getScreen.bind(this);
-
- this.interval = config?.interval ?? 60000 * 5;
- this.entryPoint = config.entryPoint;
-
- this.apiHelper = new ApiHelper(config.endpoint ?? "");
- }
-
- /**
- * Gets all campaigns, both from screen and groups.
- *
- * @param {object} screen The screen object to extract campaigns from.
- * @returns {Promise