diff --git a/.docker/vhost.conf b/.docker/vhost.conf index 49dbc642..e48754e5 100644 --- a/.docker/vhost.conf +++ b/.docker/vhost.conf @@ -7,6 +7,10 @@ server { proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header Host $http_host; proxy_pass http://node:3000; + + add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0"; + add_header Pragma "no-cache"; + add_header Expires "0"; } location /admin/ws { diff --git a/CHANGELOG.md b/CHANGELOG.md index cd16e8bf..4ee588cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,31 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +## [2.1.1] - 2024-10-23 + +- [#266](https://github.com/os2display/display-admin-client/pull/266) + - Fixed search from local storage. +- [#265](https://github.com/os2display/display-admin-client/pull/265) + - Add no-cache directive +- [#263](https://github.com/os2display/display-admin-client/pull/263) + - Added prefix to local storage keys. +- [#262](https://github.com/os2display/display-admin-client/pull/262) + - Add multi select styling for `invalid` state + - Add possibility of sending error via props to multiselect component + - Add validation checking if layout is selected on screen before save + - Add validation checking if template is selected on slide before save +- [#260](https://github.com/os2display/display-admin-client/pull/260) + - Bug in multiselect, fixed by removing duplicates by key both `@id`and `id` +- [#265](https://github.com/os2display/display-admin-client/pull/265) + - Bug in multiselect, fixed by removing duplicates by key both `@id`and `id` +- [#259](https://github.com/os2display/display-admin-client/pull/259) + - Add saving of playlists/groups with screen (as opposed to _after_) + - Clean up `screen-manager.jsx` + - Change bootstrap column class from `col-md-8` -> `col-md-12` + - update api.generated.ts to match [related pr](https://github.com/os2display/display-api-service/pull/213) + - Add @rtk-incubator/rtk-query-codegen-openapi to package.json in `src/redux/api` + - Sort playlists based on weight in drag/drop component + ## [2.1.0] - 2024-10-23 - [#258](https://github.com/os2display/display-admin-client/pull/258) diff --git a/e2e/slides.spec.js b/e2e/slides.spec.js index 7895caeb..5507c01c 100644 --- a/e2e/slides.spec.js +++ b/e2e/slides.spec.js @@ -145,11 +145,7 @@ test.describe("Create slide page works", () => { page.locator(".Toastify").locator(".Toastify__toast--error") ).toBeVisible(); await expect( - page - .locator(".Toastify") - .locator(".Toastify__toast--error") - .getByText(/An error occurred/) - .first() + page.locator(".Toastify").locator(".Toastify__toast--error").first() ).toBeVisible(); await expect(page).toHaveURL(/slide\/create/); }); diff --git a/infrastructure/itkdev/etc/confd/templates/default.conf.tmpl b/infrastructure/itkdev/etc/confd/templates/default.conf.tmpl index 658ee1ed..27f75763 100644 --- a/infrastructure/itkdev/etc/confd/templates/default.conf.tmpl +++ b/infrastructure/itkdev/etc/confd/templates/default.conf.tmpl @@ -4,15 +4,21 @@ server { root /var/www/html; index index.html index.htm; + add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0"; + add_header Pragma "no-cache"; + add_header Expires "0"; + # Any route containing a file extension (e.g. /devicesfile.js) location ~* ^{{ getenv "APP_ADMIN_CLIENT_PATH" "" }}/(.+\..+)$ { rewrite ^{{ getenv "APP_ADMIN_CLIENT_PATH" "/" }}(.*) /$1 break; + try_files $uri =404; } location ~* ^{{ getenv "APP_ADMIN_CLIENT_PATH" "/" }} { rewrite ^{{ getenv "APP_ADMIN_CLIENT_PATH" "/" }}(.*) /$1 break; autoindex off; + try_files $uri /index.html; } diff --git a/infrastructure/os2display/etc/confd/templates/default.conf.tmpl b/infrastructure/os2display/etc/confd/templates/default.conf.tmpl index 658ee1ed..27f75763 100644 --- a/infrastructure/os2display/etc/confd/templates/default.conf.tmpl +++ b/infrastructure/os2display/etc/confd/templates/default.conf.tmpl @@ -4,15 +4,21 @@ server { root /var/www/html; index index.html index.htm; + add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0"; + add_header Pragma "no-cache"; + add_header Expires "0"; + # Any route containing a file extension (e.g. /devicesfile.js) location ~* ^{{ getenv "APP_ADMIN_CLIENT_PATH" "" }}/(.+\..+)$ { rewrite ^{{ getenv "APP_ADMIN_CLIENT_PATH" "/" }}(.*) /$1 break; + try_files $uri =404; } location ~* ^{{ getenv "APP_ADMIN_CLIENT_PATH" "/" }} { rewrite ^{{ getenv "APP_ADMIN_CLIENT_PATH" "/" }}(.*) /$1 break; autoindex off; + try_files $uri /index.html; } diff --git a/src/app.scss b/src/app.scss index 7ba15e71..4114c6df 100644 --- a/src/app.scss +++ b/src/app.scss @@ -57,13 +57,8 @@ body, padding: 5em; z-index: 1021; - .spinner-container { - display: flex; - position: fixed; - - .loading-spinner { - margin-right: 1em; - } + .loading-spinner { + margin-right: 0.6em; } } diff --git a/src/components/playlist-drag-and-drop/playlist-drag-and-drop.jsx b/src/components/playlist-drag-and-drop/playlist-drag-and-drop.jsx index e0cc038a..755a50c6 100644 --- a/src/components/playlist-drag-and-drop/playlist-drag-and-drop.jsx +++ b/src/components/playlist-drag-and-drop/playlist-drag-and-drop.jsx @@ -20,9 +20,17 @@ import ScreenGanttChart from "../screen/util/screen-gantt-chart"; * @param {string} props.name - The id of the form element * @param {string} props.screenId - The screen id for get request * @param {string} props.regionId - The region id for get request + * @param {string} props.regionIdForInitializeCallback - The region id to add + * regions to formstateobject. * @returns {object} A drag and drop component */ -function PlaylistDragAndDrop({ handleChange, name, screenId, regionId }) { +function PlaylistDragAndDrop({ + handleChange, + name, + screenId, + regionId, + regionIdForInitializeCallback, +}) { const { t } = useTranslation("common", { keyPrefix: "playlist-drag-and-drop", }); @@ -49,16 +57,35 @@ function PlaylistDragAndDrop({ handleChange, name, screenId, regionId }) { sharedWithMe: onlySharedPlaylists, }); + /** + * @param regionsAndPlaylists This method initializes playlists, so the + * initial formstate object in screen manager is not empty + */ + function callbackToinitializePlaylists(regionsAndPlaylists) { + handleChange({ + target: { + id: regionIdForInitializeCallback, + value: regionsAndPlaylists["hydra:member"].map( + ({ playlist }) => playlist + ), + }, + }); + } + /** Set loaded data into form state. */ useEffect(() => { if (selectedPlaylistsByRegion) { setTotalItems(selectedPlaylistsByRegion["hydra:totalItems"]); const newPlaylists = selectedPlaylistsByRegion["hydra:member"].map( - ({ playlist }) => { - return playlist; - } + ({ playlist, weight }) => ({ ...playlist, weight }) + ); + + const selected = [...selectedData, ...newPlaylists].sort( + (a, b) => a.weight - b.weight ); - setSelectedData([...selectedData, ...newPlaylists]); + + setSelectedData(selected); + callbackToinitializePlaylists(selectedPlaylistsByRegion); } }, [selectedPlaylistsByRegion]); @@ -157,6 +184,7 @@ function PlaylistDragAndDrop({ handleChange, name, screenId, regionId }) { PlaylistDragAndDrop.propTypes = { name: PropTypes.string.isRequired, screenId: PropTypes.string.isRequired, + regionIdForInitializeCallback: PropTypes.string.isRequired, regionId: PropTypes.string.isRequired, handleChange: PropTypes.func.isRequired, }; diff --git a/src/components/screen/screen-form.jsx b/src/components/screen/screen-form.jsx index a6e68491..0c702c3b 100644 --- a/src/components/screen/screen-form.jsx +++ b/src/components/screen/screen-form.jsx @@ -50,6 +50,7 @@ function ScreenForm({ const { t } = useTranslation("common", { keyPrefix: "screen-form" }); const navigate = useNavigate(); const dispatch = useDispatch(); + const [layoutError, setLayoutError] = useState(false); const [selectedLayout, setSelectedLayout] = useState(); const [layoutOptions, setLayoutOptions] = useState(); const [bindKey, setBindKey] = useState(""); @@ -59,6 +60,21 @@ function ScreenForm({ order: { createdAt: "desc" }, }); + /** Check if published is set */ + const checkInputsHandleSubmit = () => { + setLayoutError(false); + let submit = true; + if (!selectedLayout) { + displayError(t("remember-layout-error")); + setLayoutError(true); + submit = false; + } + + if (submit) { + handleSubmit(); + } + }; + useEffect(() => { if (layouts) { setLayoutOptions(layouts["hydra:member"]); @@ -72,6 +88,11 @@ function ScreenForm({ ); if (localSelectedLayout) { setSelectedLayout(localSelectedLayout); + // Initialize regions in the formstate object of screenmanager. used to save "empty" playlists, in the situation + // we are deleting all playlists from a screen region + handleInput({ + target: { id: "regions", value: localSelectedLayout.regions }, + }); } } }, [screen.layout, layoutOptions]); @@ -84,6 +105,7 @@ function ScreenForm({ */ const handleAdd = ({ target }) => { const { value, id } = target; + setSelectedLayout(value); handleInput({ target: { id, value: value.map((item) => item["@id"]).shift() }, @@ -250,7 +272,7 @@ function ScreenForm({ noSelectedString={t("nothing-selected-resolution")} handleSelection={handleInput} options={resolutionOptions} - selected={screen.resolution || ""} + selected={screen.resolution || []} name="resolution" singleSelect /> @@ -259,7 +281,7 @@ function ScreenForm({ noSelectedString={t("nothing-selected-orientation")} handleSelection={handleInput} options={orientationOptions} - selected={screen.orientation || ""} + selected={screen.orientation || []} name="orientation" singleSelect /> @@ -277,6 +299,7 @@ function ScreenForm({ helpText={t("search-to-se-possible-selections")} selected={selectedLayout ? [selectedLayout] : []} name="layout" + error={layoutError} singleSelect /> @@ -321,7 +344,7 @@ function ScreenForm({ type="button" id="save_screen" size="lg" - onClick={handleSubmit} + onClick={checkInputsHandleSubmit} > {t("save-button")} @@ -340,7 +363,11 @@ ScreenForm.propTypes = { enableColorSchemeChange: PropTypes.bool, layout: PropTypes.string, location: PropTypes.string, - regions: PropTypes.arrayOf(PropTypes.string), + regions: PropTypes.arrayOf( + PropTypes.shape({ + "@id": PropTypes.string, + }) + ), screenUser: PropTypes.string, size: PropTypes.string, title: PropTypes.string, diff --git a/src/components/screen/screen-manager.jsx b/src/components/screen/screen-manager.jsx index ba552bb8..6983fb01 100644 --- a/src/components/screen/screen-manager.jsx +++ b/src/components/screen/screen-manager.jsx @@ -6,8 +6,6 @@ import { useNavigate } from "react-router-dom"; import { usePostV2ScreensMutation, usePutV2ScreensByIdMutation, - usePutV2ScreensByIdScreenGroupsMutation, - usePutPlaylistScreenRegionItemMutation, } from "../../redux/api/api.generated.ts"; import ScreenForm from "./screen-form"; import { @@ -38,22 +36,18 @@ function ScreenManager({ }) { const { t } = useTranslation("common", { keyPrefix: "screen-manager" }); const navigate = useNavigate(); - const [orientationOptions] = useState([ + const orientationOptions = [ { title: "Vertikal", "@id": "vertical" }, { title: "Horisontal", "@id": "horizontal" }, - ]); - const [resolutionOptions] = useState([ + ]; + const resolutionOptions = [ { title: "4K", "@id": "4K" }, { title: "HD", "@id": "HD" }, - ]); + ]; const headerText = saveMethod === "PUT" ? t("edit-screen-header") : t("create-screen-header"); const [loadingMessage, setLoadingMessage] = useState(""); const [savingScreen, setSavingScreen] = useState(false); - const [savingGroups, setSavingGroups] = useState(false); - const [savingPlaylists, setSavingPlaylists] = useState(false); - const [groupsToAdd, setGroupsToAdd] = useState(); - const [playlistsToAdd, setPlaylistsToAdd] = useState([]); // Initialize to empty screen object. const [formStateObject, setFormStateObject] = useState(null); @@ -64,63 +58,9 @@ function ScreenManager({ // Handler for creating screen. const [ PostV2Screens, - { data: postData, error: saveErrorPost, isSuccess: isSaveSuccessPost }, + { error: saveErrorPost, isSuccess: isSaveSuccessPost }, ] = usePostV2ScreensMutation(); - // @TODO: Handle errors. - const [ - putPlaylistScreenRegionItem, - { error: savePlaylistError, isSuccess: isSavePlaylistSuccess }, - ] = usePutPlaylistScreenRegionItemMutation(); - - const [ - PutV2ScreensByIdScreenGroups, - { error: saveErrorGroups, isSuccess: isSaveSuccessGroups }, - ] = usePutV2ScreensByIdScreenGroupsMutation(); - - /** When the screen is saved, the groups will be saved. */ - useEffect(() => { - if ((isSaveSuccessPut || isSaveSuccessPost) && groupsToAdd) { - setLoadingMessage(t("loading-messages.saving-groups")); - PutV2ScreensByIdScreenGroups({ - id: id || idFromUrl(postData["@id"]), - body: JSON.stringify(groupsToAdd), - }); - } - }, [isSaveSuccessPost, isSaveSuccessPut]); - - // Playlists are saved successfully, display a message - useEffect(() => { - if (isSavePlaylistSuccess && playlistsToAdd.length === 0) { - setSavingPlaylists(false); - displaySuccess(t("success-messages.saved-playlists")); - } - }, [isSavePlaylistSuccess]); - - // Groups are saved successfully, display a message - useEffect(() => { - if (isSaveSuccessGroups) { - setSavingGroups(false); - displaySuccess(t("success-messages.saved-groups")); - } - }, [isSaveSuccessGroups]); - - // Playlists are not saved successfully, display an error message - useEffect(() => { - if (savePlaylistError) { - setSavingPlaylists(false); - displayError(t("error-messages.save-playlists-error"), savePlaylistError); - } - }, [savePlaylistError]); - - // Groups are not saved successfully, display an error message - useEffect(() => { - if (saveErrorGroups) { - setSavingGroups(false); - displayError(t("error-messages.save-groups-error"), saveErrorGroups); - } - }, [saveErrorGroups]); - /** If the screen is saved, display the success message */ useEffect(() => { if (isSaveSuccessPost || isSaveSuccessPut) { @@ -166,102 +106,123 @@ function ScreenManager({ const localFormStateObject = JSON.parse(JSON.stringify(initialState)); if (localFormStateObject.orientation) { localFormStateObject.orientation = orientationOptions.filter( - ({ id: localOrientationId }) => - localOrientationId === localFormStateObject.orientation + (orientation) => + orientation["@id"] === localFormStateObject.orientation ); } if (localFormStateObject.resolution) { localFormStateObject.resolution = resolutionOptions.filter( - ({ id: localResolutioId }) => - localResolutioId === localFormStateObject.resolution + (resolution) => resolution["@id"] === localFormStateObject.resolution ); } + setFormStateObject(localFormStateObject); } }, [initialState]); - /** Adds playlists to regions. */ - useEffect(() => { - if ( - (isSaveSuccessPost || isSaveSuccessPut) && - playlistsToAdd && - playlistsToAdd.length > 0 - ) { - setLoadingMessage(t("loading-messages.saving-playlists")); - const playlistToAdd = playlistsToAdd.splice(0, 1).shift(); - putPlaylistScreenRegionItem({ - body: JSON.stringify(playlistToAdd?.list), - id: playlistToAdd.screenId || idFromUrl(postData["@id"]), - regionId: playlistToAdd.regionId, + /** + * Map group ids for submitting. + * + * @returns {Array | null} A mapped array with group ids or null + */ + function mapGroups() { + if (formStateObject.inScreenGroups) { + return formStateObject.inScreenGroups.map((group) => { + return idFromUrl(group); }); } - }, [isSavePlaylistSuccess, isSaveSuccessPut, isSaveSuccessPost]); - - /** Set playlists to save, if any */ - function savePlaylists() { - const toSave = []; - const formStateObjectPlaylists = formStateObject.playlists?.map( - (playlist) => { - return { - id: idFromUrl(playlist["@id"]), - regionId: idFromUrl(playlist.region), - }; - } - ); - if (formStateObjectPlaylists) { - // Unique regions that will have a playlist connected. - const regions = [ - ...new Set( - formStateObjectPlaylists.map((playlists) => playlists.regionId) - ), - ]; + return []; + } - // Filter playlists by region - regions.forEach((element) => { - const filteredPlaylists = formStateObjectPlaylists - .map((localPlaylists, index) => { - if (element === localPlaylists.regionId) { - return { playlist: localPlaylists.id, weight: index }; - } - return undefined; - }) - .filter((anyValue) => typeof anyValue !== "undefined"); + /** + * Creates an array of playlist ids and weight filtered by region id or null + * + * @param regionId RegionId for filtering + * @returns {Array | null} A mapped array with playlist ids and weight + * filtered by region id or null + */ + function getPlaylistsByRegionId(regionId) { + const { playlists } = formStateObject; - // Collect playlists with according ids for saving - toSave.push({ - list: filteredPlaylists, - regionId: element, - screenId: id, - }); + return playlists + .filter(({ region }) => idFromUrl(region) === idFromUrl(regionId)) + .map((playlist, index) => { + return { id: idFromUrl(playlist["@id"]), weight: index }; }); + } - if (formStateObject.playlists?.length === 0) { - formStateObject.regions.forEach((element) => { - toSave.push({ - list: [], - regionId: idFromUrl(element, 1), - screenId: id, - }); - }); - } - - // Set playlists to save - setPlaylistsToAdd(toSave); - setSavingPlaylists(true); + /** + * @param {string} id The item to remove. + * @param {Array} array The array to remove from. + */ + function removeFromArray(id, array) { + if (array.indexOf(id) >= 0) { + array.splice(array.indexOf(id), 1); } } - /** Set groups to save, if any */ - function saveGroups() { - if (Array.isArray(formStateObject.inScreenGroups)) { - setSavingGroups(true); - setGroupsToAdd( - formStateObject.inScreenGroups.map((group) => { - return idFromUrl(group); + /** + * Map playlists with regions and weight for submitting. + * + * @returns {Array | null} A mapped array with playlist, regions and weight or null + */ + function mapPlaylistsWithRegion() { + const returnArray = []; + const { playlists, regions } = formStateObject; + const regionIds = regions.map((r) => r["@id"]); + + // The playlists all have a regionId, the following creates a unique list of relevant regions If there are not + // playlists, then an empty playlist is to be saved per region + let playlistRegions = []; + if (playlists?.length > 0) { + playlistRegions = [...new Set(playlists.map(({ region }) => region))]; + } + + // Then the playlists are mapped by region Looping through the regions that have a playlist connected... + playlistRegions.forEach((regionId) => { + // remove region id from list of regionids to finally end up with an array of region ids with empty playlist + // arrays connected + removeFromArray(regionId, regionIds); + + // Add regionsId and connected playlists to the returnarray + returnArray.push({ + playlists: getPlaylistsByRegionId(regionId), + regionId: idFromUrl(regionId), + }); + }); + + // The remaining regions are added with empty playlist arrays. + if (regionIds.length > 0) { + regionIds.forEach((regionId) => + returnArray.push({ + playlists: [], + regionId: idFromUrl(regionId), }) ); } + + return returnArray; + } + + /** + * Gets orientation for submitting + * + * @returns {string} Orientation or empty string + */ + function getOrientation() { + const { orientation } = formStateObject; + return orientation ? orientation[0]["@id"] : ""; + } + + /** + * Gets resolution for submitting + * + * @returns {string} Resolution or empty string + */ + function getResolution() { + const { resolution } = formStateObject; + return resolution && resolution.length > 0 ? resolution[0]["@id"] : ""; } /** Handles submit. */ @@ -269,62 +230,50 @@ function ScreenManager({ setSavingScreen(true); setLoadingMessage(t("loading-messages.saving-screen")); const localFormStateObject = JSON.parse(JSON.stringify(formStateObject)); - const resolution = - localFormStateObject.resolution && - localFormStateObject.resolution.length > 0 - ? localFormStateObject.resolution[0].id - : ""; + const { + title, + description, + size, + modifiedBy, + createdBy, + layout, + location, + enableColorSchemeChange, + } = localFormStateObject; + const saveData = { screenScreenInput: JSON.stringify({ - title: localFormStateObject.title, - description: localFormStateObject.description, - size: localFormStateObject.size, - modifiedBy: localFormStateObject.modifiedBy, - createdBy: localFormStateObject.createdBy, - layout: localFormStateObject.layout, - location: localFormStateObject.location, - resolution, - orientation: localFormStateObject.orientation - ? localFormStateObject.orientation[0].id - : "", - enableColorSchemeChange: localFormStateObject.enableColorSchemeChange, + title, + description, + size, + modifiedBy, + createdBy, + layout, + location, + enableColorSchemeChange, + resolution: getResolution(), + groups: mapGroups(), + orientation: getOrientation(), + regions: mapPlaylistsWithRegion(), }), }; + setLoadingMessage(t("loading-messages.saving-screen")); + if (saveMethod === "POST") { - setLoadingMessage(t("loading-messages.saving-screen")); PostV2Screens(saveData); } else if (saveMethod === "PUT") { - setLoadingMessage(t("loading-messages.saving-screen")); - const putData = { ...saveData, id }; - - PutV2Screens(putData); - } else { - throw new Error("Unsupported save method"); + PutV2Screens({ ...saveData, id }); } - - saveGroups(); - savePlaylists(); }; /** Handle submitting is done. */ useEffect(() => { - if ( - (isSaveSuccessPut || isSaveSuccessPost) && - !savingPlaylists && - !savingGroups - ) { + if (isSaveSuccessPut || isSaveSuccessPost) { setSavingScreen(false); navigate("/screen/list"); } - }, [ - isSaveSuccessPut, - isSaveSuccessPost, - isSavePlaylistSuccess, - isSaveSuccessGroups, - savingGroups, - savingPlaylists, - ]); + }, [isSaveSuccessPut, isSaveSuccessPost]); return ( <> @@ -336,9 +285,7 @@ function ScreenManager({ headerText={headerText} handleInput={handleInput} handleSubmit={handleSubmit} - isLoading={ - savingScreen || savingPlaylists || savingGroups || isLoading - } + isLoading={savingScreen || isLoading} loadingMessage={loadingMessage} groupId={groupId} /> diff --git a/src/components/screen/util/grid-generation-and-select.jsx b/src/components/screen/util/grid-generation-and-select.jsx index 0e212261..3af57aaf 100644 --- a/src/components/screen/util/grid-generation-and-select.jsx +++ b/src/components/screen/util/grid-generation-and-select.jsx @@ -113,7 +113,7 @@ function GridGenerationAndSelect({ -
+
{regions.length > 0 && ( <>

@@ -136,6 +136,7 @@ function GridGenerationAndSelect({ id="playlist_drag_and_drop" handleChange={handleChange} name={data["@id"]} + regionIdForInitializeCallback={data["@id"]} screenId={screenId} regionId={idFromUrl(data["@id"])} /> diff --git a/src/components/slide/slide-form.jsx b/src/components/slide/slide-form.jsx index 2ae24f73..1755b490 100644 --- a/src/components/slide/slide-form.jsx +++ b/src/components/slide/slide-form.jsx @@ -72,6 +72,7 @@ function SlideForm({ const [searchTextTheme, setSearchTextTheme] = useState(""); const [selectedTemplates, setSelectedTemplates] = useState([]); const [themesOptions, setThemesOptions] = useState(); + const [templateError, setTemplateError] = useState(false); // Load templates. const { data: templates, isLoading: loadingTemplates } = @@ -87,6 +88,21 @@ function SlideForm({ order: { createdAt: "desc" }, }); + /** Check if published is set */ + const checkInputsHandleSubmit = () => { + setTemplateError(false); + let submit = true; + if (!selectedTemplate) { + setTemplateError(true); + submit = false; + displayError(t("slide-form.remember-template-error")); + } + + if (submit) { + handleSubmit(); + } + }; + /** * For closing overlay on escape key. * @@ -227,6 +243,7 @@ function SlideForm({ handleSelection={selectTemplate} options={templateOptions} selected={selectedTemplates} + error={templateError} name="templateInfo" filterCallback={onFilterTemplate} singleSelect @@ -484,7 +501,7 @@ function SlideForm({