Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
c936033
Updated changelog
tuj Sep 4, 2025
ca077e4
Update Changelog
turegjorup Sep 25, 2025
5aaa4b0
Merge pull request #286 from os2display/release/2.5.2
turegjorup Sep 25, 2025
05897a5
Merge tag '2.5.2' into develop
turegjorup Sep 25, 2025
2b85f26
Initial commit
agnetemoos May 23, 2025
45b3f94
Change API endpoint
agnetemoos May 27, 2025
e90f378
Change secrets settings
agnetemoos Jun 3, 2025
0519fee
Change time formats
agnetemoos Jun 17, 2025
5d60da1
Various bugfixes
agnetemoos Jul 1, 2025
5a27c01
Better data example
agnetemoos Jul 4, 2025
ac26287
Update CHANGELOG
agnetemoos Sep 18, 2025
a90f6c7
Update CHANGELOG
agnetemoos Sep 18, 2025
a56cc6e
Fix markdownlint errors
agnetemoos Sep 18, 2025
64f6dad
Fix psalm errors
agnetemoos Sep 18, 2025
13597d1
Fix psalm errors
agnetemoos Sep 18, 2025
fb5d3c0
Correct php-cs-fixer errors
agnetemoos Sep 23, 2025
e2b4b67
Correct php-cs-fixer errors
agnetemoos Sep 23, 2025
b1126f3
Update src/Feed/BrndFeedType.php
agnetemoos Oct 30, 2025
2ac6e37
Improve error handling
agnetemoos Oct 30, 2025
7a1df72
Validate required fields
agnetemoos Oct 30, 2025
f9ae36d
Refactor getData method to improve error handling
agnetemoos Oct 30, 2025
b06ca77
Remove trailing white space
agnetemoos Oct 30, 2025
2bba4a9
Improve variable naming
agnetemoos Oct 31, 2025
83d4d81
Update src/Feed/BrndFeedType.php
agnetemoos Nov 3, 2025
3088278
Add LoggerInterface
agnetemoos Nov 3, 2025
e1509fa
Fix indentation
agnetemoos Nov 3, 2025
2ebcb62
Merge pull request #313 from os2display/feature/brnd-feed
tuj Nov 4, 2025
b0de9c4
Remove cancelled status code from default booking status codes in Api…
agnetemoos Nov 21, 2025
6001d84
5740: Composer update
tuj Nov 21, 2025
72c3e43
5740: Applied coding standards
tuj Nov 24, 2025
1febce1
Add timezone support for booking date parsing in BrndFeedType
agnetemoos Nov 25, 2025
d1ad65b
Add BRND booking bug fix to changelog
agnetemoos Nov 25, 2025
d7a527a
Fix entry in changelog
agnetemoos Nov 25, 2025
c922996
Remove unused DateTimeZone import from BrndFeedType
agnetemoos Nov 25, 2025
2c09859
Update src/Feed/BrndFeedType.php
agnetemoos Nov 27, 2025
fedc264
Refactor timezone handling in BrndFeedType to use a constant for BRND…
agnetemoos Dec 5, 2025
cc55346
Merge pull request #333 from os2display/feature/327-skewed-times-in-b…
tuj Dec 5, 2025
6b775f4
Merge branch 'develop' into feature/sec-2025-11-21
tuj Dec 5, 2025
2d553c1
Merge pull request #330 from os2display/feature/sec-2025-11-21
tuj Dec 5, 2025
98a079e
5740: Updated changelog
tuj Dec 5, 2025
1b87c53
Merge pull request #335 from os2display/release/2.6.0
tuj Dec 17, 2025
eb549ca
Merge branch 'main' into develop
tuj Dec 17, 2025
59fa91b
6592: Add onFlush listener and tests for ManyToMany checksum propagation
turegjorup Feb 25, 2026
b3a7c36
6592: Add PR link to changelog
turegjorup Feb 25, 2026
09d3b1b
6592: Fix PHP CS fixer violations in docblocks
turegjorup Feb 25, 2026
991cf7e
Fix: Use correct BRND field names
Feb 26, 2026
bff0caf
Revert "Fix: Use correct BRND field names"
Feb 26, 2026
87f14cc
Add checksum recalculation command and extract calculator service
turegjorup Feb 27, 2026
d85399f
Fix Psalm errors in RecalculateChecksumCommand
turegjorup Feb 27, 2026
204f866
6592: Updated changelog
tuj Mar 2, 2026
4e6c47f
Merge pull request #347 from os2display/hotfix/2.6.1
tuj Mar 2, 2026
a69c6dd
6592: Fixed merge
tuj Mar 6, 2026
fbdf31a
6592: Composer update
tuj Mar 6, 2026
2492e21
6592: Added changes from admin
tuj Mar 6, 2026
6f472ca
6592: Added changes to templates
tuj Mar 6, 2026
b2155f4
6592: Applied coding standards
tuj Mar 6, 2026
2d3b0ef
6592: Fixed import path
tuj Mar 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ All notable changes to this project will be documented in this file.
- Aligned with v. 2.5.2.
- Removed themes.
- Added command to migrate config.json files.
- Fix data fetching bug + tests
- Fix data fetching bug and tests
- Refactored screen layout commands.
- Moved list components (search and checkboxes) around.
- Aligned environment variable names.
Expand Down
19 changes: 10 additions & 9 deletions assets/admin/components/slide/slide-manager.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ulid } from "ulid";
import { useDispatch } from "react-redux";
import dayjs from "dayjs";
import { useNavigate } from "react-router-dom";
import rebuildMediaFromContent from "./slide-media-utils";
import UserContext from "../../context/user-context";
import {
enhancedApi,
Expand Down Expand Up @@ -337,13 +338,11 @@ function SlideManager({
const localFormStateObject = { ...formStateObject };
const localMediaData = { ...mediaData };
// Set field as a field to look into for new references.
setMediaFields([...new Set([...mediaFields, fieldId])]);
const updatedMediaFields = [...new Set([...mediaFields, fieldId])];
setMediaFields(updatedMediaFields);

const newField = [];

if (Array.isArray(fieldValue) && fieldValue.length === 0) {
localFormStateObject.media = [];
}
// Handle each entry in field.
if (Array.isArray(fieldValue)) {
fieldValue.forEach((entry) => {
Expand Down Expand Up @@ -383,17 +382,19 @@ function SlideManager({
!Object.prototype.hasOwnProperty.call(localMediaData, entry["@id"])
) {
set(localMediaData, entry["@id"], entry);

localFormStateObject.media.push(entry["@id"]);
}
}
});
}

set(localFormStateObject.content, fieldId, newField);
set(localFormStateObject, "media", [
...new Set([...localFormStateObject.media]),
]);

// Rebuild the media array from all content fields to keep it in sync.
set(
localFormStateObject,
"media",
rebuildMediaFromContent(localFormStateObject.content),
);

setFormStateObject(localFormStateObject);
setMediaData(localMediaData);
Expand Down
61 changes: 61 additions & 0 deletions assets/admin/components/slide/slide-media-utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* Rebuild the media array from all content fields that reference media.
*
* This ensures that the top-level `media` array (sent to the API as slide_media
* associations) always matches the media actually referenced in the slide's
* `content` object.
*
* @param {object} content - The slide content object.
* @returns {string[]} Deduplicated array of media IRIs.
*/
export default function rebuildMediaFromContent(content) {
const media = [];

const mediaIriRegex = /\/v2\/media\/.+/;

const isMediaIri = (value) =>
typeof value === "string" &&
!value.startsWith("TEMP--") &&
mediaIriRegex.test(value);

const collectMediaFromValue = (value, seen = new Set()) => {
// 1) Ignore empty values early (nothing to scan)
if (value === null || value === undefined) return;

// 2) If it's a string, it might be a media IRI; validate and collect it
if (typeof value === "string") {
if (isMediaIri(value)) media.push(value);
return;
}

// 3) If it's not an object (e.g. number/boolean/function), it cannot contain nested media
if (typeof value !== "object") return;

// 4) Defensive guard against circular references:
// - JSON content won't have cycles, but runtime objects might.
// - If we've seen this object/array already, stop to avoid infinite recursion.
if (seen.has(value)) return;
seen.add(value);

// 5) If it's an array, scan each element (elements can be strings, objects, or more arrays)
if (Array.isArray(value)) {
value.forEach((item) => collectMediaFromValue(item, seen));
return;
}

// 6) Otherwise it's a plain object: scan its property values recursively
Object.values(value).forEach((item) => collectMediaFromValue(item, seen));
};

const fieldsToScan = new Set([]);

// Scan content for media references.
if (content && typeof content === "object") {
Object.keys(content).forEach((key) => fieldsToScan.add(key));
}

// Scan the entire content object (one traversal)
collectMediaFromValue(content);

return [...new Set(media)];
}
41 changes: 26 additions & 15 deletions assets/shared/templates/image-text.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ function renderSlide(slide, run, slideDone) {
*/
function ImageText({ slide, content, run, slideDone, executionId }) {
const imageTimeoutRef = useRef();
const imagesRef = useRef([]);
const [images, setImages] = useState([]);
const [currentImage, setCurrentImage] = useState();
const logo = slide?.theme?.logo;
Expand Down Expand Up @@ -132,13 +133,15 @@ function ImageText({ slide, content, run, slideDone, executionId }) {
}

const changeImage = (newIndex) => {
if (newIndex < images.length) {
setCurrentImage(images[newIndex]);
const currentImages = imagesRef.current;

if (newIndex < images.length - 1) {
if (newIndex < currentImages.length) {
setCurrentImage(currentImages[newIndex]);

if (newIndex < currentImages.length - 1) {
imageTimeoutRef.current = setTimeout(
() => changeImage(newIndex + 1),
duration / images.length,
duration / currentImages.length,
);
}
}
Expand All @@ -152,33 +155,41 @@ function ImageText({ slide, content, run, slideDone, executionId }) {
);

if (imageUrls?.length > 0) {
const newImages = imageUrls.map((url) => {
return {
url,
nodeRef: createRef(),
};
});
const newImages = imageUrls.map((url) => ({
url,
nodeRef: createRef(),
}));

imagesRef.current = newImages;

setImages(newImages);
} else {
imagesRef.current = [];
setImages([]);
}
}
}, [slide]);

const startTheShow = () => {
if (images?.length > 0 && !currentImage) {
setCurrentImage(images[0]);
if (imageTimeoutRef.current) {
clearTimeout(imageTimeoutRef.current);
}

// If there are multiple images, we are going to loop through these WITHIN the set duration.
if (images?.length > 1) {
const currentImages = imagesRef.current;

if (currentImages.length > 1) {
// Kickoff the display of multiple images with the zero indexed
changeImage(0);
} else if (currentImages.length === 1) {
setCurrentImage(currentImages[0]);
}
};

useEffect(() => {
if (!currentImage) {
if (images.length > 0) {
startTheShow();
} else {
setCurrentImage(undefined);
}
}, [images]);

Expand Down
116 changes: 116 additions & 0 deletions assets/tests/admin/admin-slide-media-sync.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { test, expect } from "@playwright/test";
import rebuildMediaFromContent from "../../admin/components/slide/slide-media-utils";

test.describe("Slide media sync", () => {
test("It returns media IRIs referenced in content fields", () => {
const content = {
mainImage: ["/v2/media/1", "/v2/media/2"],
backgroundVideo: ["/v2/media/3"],
};

const result = rebuildMediaFromContent(content);

expect(result).toEqual(["/v2/media/1", "/v2/media/2", "/v2/media/3"]);
});

test("It excludes TEMP-- IDs that have not been uploaded yet", () => {
const content = {
mainImage: ["TEMP--abc123", "/v2/media/1"],
};

const result = rebuildMediaFromContent(content);

expect(result).toEqual(["/v2/media/1"]);
});

test("It removes media no longer referenced in any content field", () => {
const content = {
mainImage: ["/v2/media/2"],
};

const result = rebuildMediaFromContent(content);

expect(result).toEqual(["/v2/media/2"]);
expect(result).not.toContain("/v2/media/1");
});

test("It returns empty array when all media is removed from content", () => {
const content = {
mainImage: [],
backgroundVideo: ["/v2/media/3"],
};

const result = rebuildMediaFromContent(content);

expect(result).toEqual(["/v2/media/3"]);
});

test("It deduplicates media used across multiple content fields", () => {
const content = {
mainImage: ["/v2/media/1"],
thumbnail: ["/v2/media/1"],
};

const result = rebuildMediaFromContent(content);

expect(result).toEqual(["/v2/media/1"]);
});

test("It handles non-existent content fields gracefully", () => {
const content = {};

const result = rebuildMediaFromContent(content);

expect(result).toEqual([]);
});

test("It handles nested content field paths", () => {
const content = {
sections: {
hero: ["/v2/media/1"],
},
};

const result = rebuildMediaFromContent(content);

expect(result).toEqual(["/v2/media/1"]);
});

test("It ignores non-media content values when scanning top-level keys", () => {
const content = {
images: ["/v2/media/1"],
title: "Some text",
separator: true,
contacts: [{ name: "John", image: ["/v2/media/2"], tags: ["news"] }],
};

const result = rebuildMediaFromContent(content);

expect(result).toContain("/v2/media/1");
expect(result).toContain("/v2/media/2");
expect(result).not.toContain("news");
});

test("It does not include non-media string arrays from content", () => {
const content = {
images: ["/v2/media/1"],
tags: ["news", "sports"],
};

const result = rebuildMediaFromContent(content);

expect(result).toEqual(["/v2/media/1"]);
expect(result).not.toContain("news");
expect(result).not.toContain("sports");
});

test("It avoids infinite recursion when content contains circular references", () => {
const circular = { images: ["/v2/media/1"] };
circular.self = circular; // create an explicit cycle

expect(() => rebuildMediaFromContent(circular)).not.toThrow();

const result = rebuildMediaFromContent(circular);
expect(result).toContain("/v2/media/1");
});
});
Loading