@griddo/ax 12.7.0 → 12.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/package.json +2 -2
  2. package/src/GlobalStore.tsx +3 -1
  3. package/src/__tests__/components/ConfigPanel/GlobalPageForm/GlobalPageForm.test.tsx +7 -3
  4. package/src/__tests__/components/Fields/Wysiwyg/Wysiwyg.style.test.tsx +44 -0
  5. package/src/__tests__/components/Fields/Wysiwyg/Wysiwyg.test.tsx +109 -0
  6. package/src/__tests__/components/Nav/Nav.test.tsx +75 -2
  7. package/src/__tests__/components/ResizePanel/ResizePanel.test.tsx +50 -21
  8. package/src/__tests__/components/Toast/Toast.test.tsx +37 -1
  9. package/src/__tests__/hooks/broadcast.test.tsx +263 -0
  10. package/src/api/utils.tsx +7 -6
  11. package/src/components/Browser/index.tsx +10 -2
  12. package/src/components/ConfigPanel/GlobalPageForm/index.tsx +3 -4
  13. package/src/components/Fields/Wysiwyg/atoms.tsx +1 -1
  14. package/src/components/Fields/Wysiwyg/index.tsx +28 -0
  15. package/src/components/Fields/Wysiwyg/style.tsx +41 -1
  16. package/src/components/MainWrapper/index.tsx +14 -2
  17. package/src/components/Nav/index.tsx +21 -6
  18. package/src/components/ResizePanel/index.tsx +8 -3
  19. package/src/components/Toast/index.tsx +4 -1
  20. package/src/containers/ActivityLog/actions.tsx +4 -7
  21. package/src/containers/PageEditor/actions.tsx +14 -3
  22. package/src/helpers/containerEvaluations.tsx +8 -0
  23. package/src/hooks/broadcast.ts +160 -0
  24. package/src/hooks/index.tsx +5 -0
  25. package/src/modules/ActivityLog/index.tsx +1 -0
  26. package/src/modules/Analytics/index.tsx +1 -1
  27. package/src/modules/App/index.tsx +21 -6
  28. package/src/modules/Content/PageItem/index.tsx +36 -19
  29. package/src/modules/Content/index.tsx +5 -7
  30. package/src/modules/FileDrive/FileModal/DetailPanel/UsageContent/index.tsx +5 -14
  31. package/src/modules/FileDrive/index.tsx +1 -1
  32. package/src/modules/Forms/FormUseModal/index.tsx +3 -8
  33. package/src/modules/GlobalEditor/atoms.tsx +35 -1
  34. package/src/modules/GlobalEditor/index.tsx +169 -51
  35. package/src/modules/GlobalSettings/Robots/index.tsx +1 -1
  36. package/src/modules/MediaGallery/ImageModal/DetailPanel/UsageContent/index.tsx +6 -15
  37. package/src/modules/MediaGallery/index.tsx +1 -1
  38. package/src/modules/PageEditor/atoms.tsx +35 -1
  39. package/src/modules/PageEditor/index.tsx +168 -48
  40. package/src/modules/Redirects/index.tsx +1 -0
  41. package/src/modules/Settings/Languages/index.tsx +1 -1
  42. package/src/modules/Settings/SeoAnalyticsSettings/Analytics/index.tsx +1 -0
  43. package/src/modules/Settings/Social/index.tsx +1 -1
  44. package/src/modules/StructuredData/Form/index.tsx +108 -10
  45. package/src/modules/StructuredData/StructuredDataList/GlobalPageItem/index.tsx +31 -11
  46. package/src/modules/StructuredData/StructuredDataList/StructuredDataItem/index.tsx +17 -3
  47. package/src/modules/StructuredData/StructuredDataList/index.tsx +6 -4
  48. package/src/modules/StructuredData/atoms.tsx +35 -1
  49. package/src/routes/multisite.tsx +34 -18
  50. package/src/routes/site.tsx +40 -21
  51. package/src/storeRegistry.ts +5 -0
  52. package/src/modules/StructuredData/index.tsx +0 -18
@@ -26,7 +26,15 @@ import { structuredDataActions } from "@ax/containers/StructuredData";
26
26
  import { usersActions } from "@ax/containers/Users";
27
27
  import { RouteLeavingGuard } from "@ax/guards";
28
28
  import { dateToString, getDefaultTheme } from "@ax/helpers";
29
- import { useEditingHeartbeat, useIsDirty, useModals, usePermissionsForPage } from "@ax/hooks";
29
+ import {
30
+ useBroadcastClipboard,
31
+ useBroadcastContentUpdate,
32
+ useEditingHeartbeat,
33
+ useIsDirty,
34
+ useModals,
35
+ usePermissionsForPage,
36
+ } from "@ax/hooks";
37
+ import history from "@ax/routes/history";
30
38
  import type {
31
39
  HeadingFilter,
32
40
  IErrorItem,
@@ -40,7 +48,7 @@ import type {
40
48
  PageAccessGrantsSummary,
41
49
  } from "@ax/types";
42
50
 
43
- import { DeleteModal } from "./atoms";
51
+ import { ConflictSaveModal, DeleteModal } from "./atoms";
44
52
  import Editor from "./Editor";
45
53
  import Preview from "./Preview";
46
54
 
@@ -78,6 +86,7 @@ const GlobalEditor = (props: IProps) => {
78
86
  updatePageAccessGrants,
79
87
  accessGrants,
80
88
  sendPagePing,
89
+ match,
81
90
  } = props;
82
91
 
83
92
  const isAllowedTo = usePermissionsForPage(
@@ -109,9 +118,14 @@ const GlobalEditor = (props: IProps) => {
109
118
  time: "12:00 am",
110
119
  });
111
120
  const [pendingNavigateCallback, setPendingNavigateCallback] = useState<(() => void) | null>(null);
121
+ const [showPageUpdateNotification, setShowPageUpdateNotification] = useState(false);
122
+ const [pendingConflictAction, setPendingConflictAction] = useState<(() => Promise<void>) | null>(null);
112
123
  const browserRef = useRef<HTMLDivElement>(null);
113
124
 
114
- const { isOpen, toggleModal } = useModals([
125
+ const { broadcastPageSave, remotePageUpdate, clearRemoteUpdate } = useBroadcastContentUpdate(pageID);
126
+ useBroadcastClipboard();
127
+
128
+ const { isOpen, toggleModal, closeModal } = useModals([
115
129
  "userEditing",
116
130
  "unpublish",
117
131
  "schedule",
@@ -122,6 +136,7 @@ const GlobalEditor = (props: IProps) => {
122
136
  "dirtyNavigate",
123
137
  "userAccess",
124
138
  "delete",
139
+ "conflictSave",
125
140
  ]);
126
141
 
127
142
  const { isDirty, setIsDirty, resetDirty } = useIsDirty(editorContent, isNewTranslation);
@@ -155,19 +170,34 @@ const GlobalEditor = (props: IProps) => {
155
170
  // biome-ignore lint/correctness/useExhaustiveDependencies: TODO: fix this
156
171
  useEffect(() => {
157
172
  const { setStructuredDataFilter } = props;
173
+ const urlPageID = match?.params?.pageID ? parseInt(match.params.pageID, 10) : undefined;
174
+ if (urlPageID) setCurrentPageID(urlPageID);
175
+ const resolvedPageID = urlPageID ?? pageID;
158
176
 
159
177
  editorContent?.structuredData && setStructuredDataFilter(editorContent.structuredData);
160
- const handleGetPage = async () => await getPage(pageID, true);
178
+ const handleGetPage = async () => await getPage(resolvedPageID, true);
161
179
  setTab(defaultContentTab);
162
180
  setToastMsg(null);
163
181
  handleGetPage();
164
- if (!pageID) {
182
+ if (!resolvedPageID) {
165
183
  setIsDirty(false);
166
184
  }
167
185
  }, []);
168
186
 
169
187
  useEditingHeartbeat(pageID, sendPagePing);
170
188
 
189
+ // sc-111850: the route carries the page id and the mount effect above seeds
190
+ // currentPageID from it, so a URL still pointing at the previous page is not cosmetic —
191
+ // it re-enters on the next mount and drags the editor back to that page. The global
192
+ // editor lives under /data, not /sites: this module is the one routes/multisite.tsx
193
+ // mounts, and routes/site.tsx mounts PageEditor instead.
194
+ useEffect(() => {
195
+ const urlPageID = match?.params?.pageID ? parseInt(match.params.pageID, 10) : undefined;
196
+ if (pageID && urlPageID && pageID !== urlPageID) {
197
+ history.replace(`/data/pages/editor/${pageID}`);
198
+ }
199
+ }, [pageID, match]);
200
+
171
201
  useEffect(() => {
172
202
  setSelectedTab(defaultTab);
173
203
  }, [defaultTab]);
@@ -200,6 +230,12 @@ const GlobalEditor = (props: IProps) => {
200
230
  }
201
231
  }, [editorContent]);
202
232
 
233
+ useEffect(() => {
234
+ if (remotePageUpdate) {
235
+ setShowPageUpdateNotification(true);
236
+ }
237
+ }, [remotePageUpdate]);
238
+
203
239
  const setRoute = (path: string) => props.setHistoryPush(path, true);
204
240
 
205
241
  const removePage = async () => {
@@ -217,31 +253,52 @@ const GlobalEditor = (props: IProps) => {
217
253
  }
218
254
  };
219
255
 
220
- const publishPage = async () => {
221
- const { updatePageStatus, savePage, pageID, validatePage, skipReviewOnPublish } = props;
256
+ // A warned tab must ask before ANY write, not only before the ones that go through
257
+ // savePage. Two traps here: these actions are wired straight to onClick, so their first
258
+ // argument can be a click event and only an explicit `true` may count as "already
259
+ // confirmed"; and the retry is stored as-is, because `() => retry()` would make React
260
+ // read it as a state updater and run the write before the author decides.
261
+ const blockedByConflict = (retry: () => Promise<void>, skipConflictCheck?: boolean) => {
262
+ if (skipConflictCheck === true || !isDirty || !remotePageUpdate) {
263
+ return false;
264
+ }
265
+ setPendingConflictAction(() => retry);
266
+ toggleModal("conflictSave");
267
+ return true;
268
+ };
269
+
270
+ const publishPage = async (skipConflictCheck?: boolean) => {
271
+ const { updatePageStatus, pageID, validatePage, skipReviewOnPublish } = props;
222
272
 
223
273
  const validated = skipReviewOnPublish ? true : await validatePage(true);
224
274
 
225
275
  if (validated) {
226
- const publishPage = {
227
- status: pageStatus.UPLOAD_PENDING,
228
- };
229
-
230
- const isSaved =
231
- pageID && !isNewTranslation
232
- ? await updatePageStatus([pageID], pageStatus.UPLOAD_PENDING)
233
- : await savePage({ publishPage });
276
+ if (pageID && !isNewTranslation) {
277
+ if (blockedByConflict(() => publishPage(true), skipConflictCheck)) {
278
+ return;
279
+ }
280
+
281
+ const isSaved = await updatePageStatus([pageID], pageStatus.UPLOAD_PENDING);
282
+ if (isSaved) {
283
+ resetDirty();
284
+ broadcastPageSave(pageID);
285
+ }
286
+ } else {
287
+ const publishPageData = {
288
+ status: pageStatus.UPLOAD_PENDING,
289
+ };
234
290
 
235
- if (isSaved) {
236
- resetDirty();
291
+ await handleSavePageCommon({ publishPage: publishPageData }, undefined, skipConflictCheck, () =>
292
+ publishPage(true),
293
+ );
237
294
  }
238
295
  } else {
239
296
  setNotification({ text: errorNotificationText, type: "error" });
240
297
  }
241
298
  };
242
299
 
243
- const publishChanges = async () => {
244
- const { savePage, validatePage, skipReviewOnPublish } = props;
300
+ const publishChanges = async (skipConflictCheck?: boolean) => {
301
+ const { validatePage, skipReviewOnPublish } = props;
245
302
 
246
303
  const validated = skipReviewOnPublish ? true : await validatePage(true);
247
304
 
@@ -250,21 +307,23 @@ const GlobalEditor = (props: IProps) => {
250
307
  status: pageStatus.UPLOAD_PENDING,
251
308
  };
252
309
 
253
- const isSaved = await savePage({ publishPage });
254
- if (isSaved) {
255
- resetDirty();
256
- }
310
+ await handleSavePageCommon({ publishPage }, undefined, skipConflictCheck, () => publishChanges(true));
257
311
  } else {
258
312
  setNotification({ text: errorNotificationText, type: "error" });
259
313
  }
260
314
  };
261
315
 
262
- const unpublishPage = async () => {
316
+ const unpublishPage = async (skipConflictCheck?: boolean) => {
263
317
  const { updatePageStatus, pageID } = props;
264
318
 
319
+ if (blockedByConflict(() => unpublishPage(true), skipConflictCheck)) {
320
+ return;
321
+ }
322
+
265
323
  const isSaved = await updatePageStatus([pageID], pageStatus.OFFLINE_PENDING);
266
324
  if (isSaved) {
267
325
  resetDirty();
326
+ broadcastPageSave(pageID);
268
327
  }
269
328
  };
270
329
 
@@ -278,14 +337,13 @@ const GlobalEditor = (props: IProps) => {
278
337
  }
279
338
  };
280
339
 
281
- const handlePublishDraft = async () => {
282
- const { savePage, validatePage, skipReviewOnPublish } = props;
340
+ const handlePublishDraft = async (skipConflictCheck?: boolean) => {
341
+ const { validatePage, skipReviewOnPublish } = props;
283
342
 
284
343
  const validated = skipReviewOnPublish ? true : await validatePage(true);
285
344
 
286
345
  if (validated) {
287
- const isSaved = await savePage({ publishDraft: true });
288
- if (isSaved) resetDirty();
346
+ await handleSavePageCommon({ publishDraft: true }, undefined, skipConflictCheck, () => handlePublishDraft(true));
289
347
  } else {
290
348
  setNotification({ text: errorNotificationText, type: "error" });
291
349
  }
@@ -303,29 +361,38 @@ const GlobalEditor = (props: IProps) => {
303
361
  getPage(pageID, true);
304
362
  };
305
363
 
306
- const handleCreateDraft = async () => {
307
- const { savePage } = props;
308
-
309
- const isSaved = await savePage({ createDraft: true });
310
- if (isSaved) resetDirty();
364
+ const handleCreateDraft = async (skipConflictCheck?: boolean) => {
365
+ await handleSavePageCommon({ createDraft: true }, undefined, skipConflictCheck, () => handleCreateDraft(true));
311
366
  };
312
367
 
313
- const handleSchedulePublication = async () => {
368
+ const handleSchedulePublication = async (skipConflictCheck?: boolean) => {
369
+ if (blockedByConflict(() => handleSchedulePublication(true), skipConflictCheck)) {
370
+ closeModal("schedule");
371
+ return;
372
+ }
373
+
314
374
  const date = new Date(`${scheduleDate.date} ${scheduleDate.time}`);
315
375
  const dateString = dateToString(date, "dd/MM/yyyy HH:mm:ss");
316
376
  const saved = await schedulePublication(dateString, isDraft);
317
377
  if (saved) {
318
378
  resetDirty();
319
- toggleModal("schedule");
379
+ broadcastPageSave(pageID);
380
+ closeModal("schedule");
320
381
  }
321
382
  };
322
383
 
323
- const handleCancelSchedulePublication = async () => {
384
+ const handleCancelSchedulePublication = async (skipConflictCheck?: boolean) => {
385
+ if (blockedByConflict(() => handleCancelSchedulePublication(true), skipConflictCheck)) {
386
+ closeModal("cancelSchedule");
387
+ return;
388
+ }
389
+
324
390
  const saved = await schedulePublication(null, isDraft);
325
391
  if (saved) {
326
392
  setScheduleDate({ date: "", time: "12:00 am" });
327
393
  resetDirty();
328
- toggleModal("cancelSchedule");
394
+ broadcastPageSave(pageID);
395
+ closeModal("cancelSchedule");
329
396
  }
330
397
  };
331
398
 
@@ -433,16 +500,38 @@ const GlobalEditor = (props: IProps) => {
433
500
  const isEditable = editorContent?.editable;
434
501
  const isRestricted = !!(accessGrants.users.length || accessGrants.roles.length);
435
502
 
436
- const handleSavePage = async (setToast?: (msg: string) => void) => {
503
+ const handleSavePageCommon = async (
504
+ saveOptions: SavePageOptions,
505
+ onSuccess?: () => void,
506
+ skipConflictCheck?: boolean,
507
+ recursiveCall: () => Promise<void> = () => Promise.resolve(),
508
+ ) => {
509
+ if (blockedByConflict(recursiveCall, skipConflictCheck)) {
510
+ return;
511
+ }
512
+
437
513
  const { savePage } = props;
438
514
 
439
- const isSaved = await savePage({ setToast });
515
+ const isSaved = await savePage(saveOptions);
440
516
  if (isSaved) {
441
517
  resetDirty();
442
- setToastMsg(null);
518
+ clearRemoteUpdate();
519
+ if (pageID) {
520
+ broadcastPageSave(pageID);
521
+ }
522
+ onSuccess?.();
443
523
  }
444
524
  };
445
525
 
526
+ const handleSavePage = async (setToast?: (msg: string) => void, skipConflictCheck?: boolean) => {
527
+ await handleSavePageCommon(
528
+ { setToast },
529
+ () => setToastMsg(null),
530
+ skipConflictCheck,
531
+ () => handleSavePage(setToast, true),
532
+ );
533
+ };
534
+
446
535
  const goToPages = (path: string) => setRoute(path);
447
536
 
448
537
  const goToError = async (editorID: number | null, tab: string, template: boolean, parentEditorID?: number) => {
@@ -523,6 +612,12 @@ const GlobalEditor = (props: IProps) => {
523
612
  setPendingNavigateCallback(null);
524
613
  };
525
614
 
615
+ const handleConfirmConflictSave = async () => {
616
+ toggleModal("conflictSave");
617
+ await pendingConflictAction?.();
618
+ setPendingConflictAction(null);
619
+ };
620
+
526
621
  let pageStatusActions =
527
622
  props.pageStatus === pageStatus.PUBLISHED || isDraft
528
623
  ? [
@@ -549,10 +644,8 @@ const GlobalEditor = (props: IProps) => {
549
644
  const goBackToSite = () => {
550
645
  if (sitePageID) {
551
646
  setCurrentSiteInfo(savedSiteInfo);
552
- setCurrentPageID(sitePageID);
553
647
  getUserCurrentPermissions();
554
- const path = "/sites/pages/editor";
555
- setHistoryPush(path, true);
648
+ setHistoryPush(`/sites/pages/editor/${sitePageID}`, true);
556
649
  }
557
650
  };
558
651
 
@@ -656,7 +749,9 @@ const GlobalEditor = (props: IProps) => {
656
749
  }
657
750
  };
658
751
 
659
- return isLoading ? (
752
+ const isContentReady = !!(editorContent as any)?.template;
753
+
754
+ return isLoading || !isContentReady ? (
660
755
  <Loading />
661
756
  ) : (
662
757
  <>
@@ -687,6 +782,22 @@ const GlobalEditor = (props: IProps) => {
687
782
  >
688
783
  {selectedTab === "edit" ? (
689
784
  <>
785
+ {showPageUpdateNotification && (
786
+ <S.NotificationWrapper data-testid="page-update-notification">
787
+ <Notification
788
+ type="info"
789
+ text="This page was edited in another tab and you're viewing an older version. Reload to see the latest changes. Any unsaved edits here will be lost."
790
+ btnText="Reload Page"
791
+ onClick={() => {
792
+ window.location.reload();
793
+ }}
794
+ resetError={() => {
795
+ setShowPageUpdateNotification(false);
796
+ clearRemoteUpdate();
797
+ }}
798
+ />
799
+ </S.NotificationWrapper>
800
+ )}
690
801
  {isLivePageChanged && (
691
802
  <S.NotificationWrapper>
692
803
  <Notification type="warning" text={modifiedNotificationText} />
@@ -844,6 +955,11 @@ const GlobalEditor = (props: IProps) => {
844
955
  you want to discard your changes?
845
956
  </S.ModalContent>
846
957
  </Modal>
958
+ <ConflictSaveModal
959
+ isOpen={isOpen("conflictSave")}
960
+ toggleModal={() => toggleModal("conflictSave")}
961
+ onSaveAnyway={handleConfirmConflictSave}
962
+ />
847
963
  </MainWrapper>
848
964
  </>
849
965
  );
@@ -917,14 +1033,16 @@ const mapDispatchToProps = {
917
1033
  updatePageAccessGrants: pageEditorActions.updatePageAccessGrants,
918
1034
  };
919
1035
 
1036
+ type SavePageOptions = {
1037
+ createDraft?: boolean;
1038
+ publishPage?: { status: string };
1039
+ publishDraft?: boolean;
1040
+ setToast?: (msg: string) => void;
1041
+ };
1042
+
920
1043
  interface IPageEditorDispatchProps {
921
1044
  getPage(pageID?: number, global?: boolean, preferDraft?: boolean): Promise<void>;
922
- savePage(data?: {
923
- createDraft?: boolean;
924
- publishPage?: { status: string };
925
- publishDraft?: boolean;
926
- setToast?: (msg: string) => void;
927
- }): Promise<boolean>;
1045
+ savePage(data?: SavePageOptions): Promise<boolean>;
928
1046
  deletePage(params?: ISavePageParams): Promise<boolean>;
929
1047
  validatePage(publish?: boolean): Promise<boolean>;
930
1048
  updatePageStatus(id: number[], status: string): Promise<boolean>;
@@ -950,7 +1068,7 @@ interface IPageEditorDispatchProps {
950
1068
  }): Promise<boolean>;
951
1069
  }
952
1070
 
953
- type IProps = IPageEditorStateProps & IPageEditorDispatchProps & RouteComponentProps;
1071
+ type IProps = IPageEditorStateProps & IPageEditorDispatchProps & RouteComponentProps<{ pageID?: string }>;
954
1072
 
955
1073
  const GlobalEditorWithErrorBoundary = withErrorBoundary(GlobalEditor, {
956
1074
  fallback: <ErrorPage type="wrongEditor" />,
@@ -76,7 +76,7 @@ const Robots = (props: IProps): JSX.Element => {
76
76
  return (
77
77
  <>
78
78
  <RouteLeavingGuard when={isDirty} action={setRoute} text={modalText} />
79
- <MainWrapper backLink={false} title="SEO Settings" rightButton={rightButtonProps}>
79
+ <MainWrapper backLink={false} title="SEO Settings" rightButton={rightButtonProps} screen="robots">
80
80
  <S.Wrapper>
81
81
  <Nav current={currentNavItem} items={navItems} onClick={handleMenuClick} />
82
82
  <S.ContentWrapper>
@@ -1,11 +1,9 @@
1
1
  import React, { useEffect, useState } from "react";
2
2
  import { connect } from "react-redux";
3
3
  import { appActions } from "@ax/containers/App";
4
- import { pageEditorActions } from "@ax/containers/PageEditor";
5
4
  import { sitesActions } from "@ax/containers/Sites";
6
5
  import { structuredDataActions } from "@ax/containers/StructuredData";
7
6
  import { usersActions } from "@ax/containers/Users";
8
- import type { ISetCurrentPageIDAction } from "@ax/containers/PageEditor/interfaces";
9
7
  import type { IImage, IRootState, ISite, IUser } from "@ax/types";
10
8
  import { images } from "@ax/api";
11
9
  import { isReqOk } from "@ax/helpers";
@@ -23,7 +21,6 @@ const UsageContent = (props: IProps) => {
23
21
  selectedTab,
24
22
  currentUser,
25
23
  setHistoryPush,
26
- setCurrentPageID,
27
24
  getSite,
28
25
  setCurrentDataID,
29
26
  setSelectedStructuredData,
@@ -71,8 +68,7 @@ const UsageContent = (props: IProps) => {
71
68
  const data = items.sites.find((site) => site.id === currentSiteID)?.simpleContent || [];
72
69
  const pageItems = pages.map((item: IImageUseItem) => {
73
70
  const handleClick = () => {
74
- setCurrentPageID(item.id);
75
- setHistoryPush("/sites/pages/editor", true);
71
+ setHistoryPush(`/sites/pages/editor/${item.id}`, true);
76
72
  };
77
73
 
78
74
  return <Item title={item.title} type="Page" date={item.date} key={item.id} onClick={handleClick} />;
@@ -84,7 +80,7 @@ const UsageContent = (props: IProps) => {
84
80
  setSelectedStructuredData(item.structuredData.id, "site");
85
81
  setCurrentDataID(item.id);
86
82
  await getDataContent(item.id);
87
- setHistoryPush(`/sites/data/${item.structuredData.id}/editor`);
83
+ setHistoryPush(`/sites/data/${item.structuredData.id}/content`);
88
84
  };
89
85
 
90
86
  return (
@@ -104,13 +100,12 @@ const UsageContent = (props: IProps) => {
104
100
  const handleClick = async (page: IImageUseItem) => {
105
101
  await getSite(item.id);
106
102
  if (!page.structuredData) {
107
- setCurrentPageID(page.id);
108
- setHistoryPush("/sites/pages/editor", true);
103
+ setHistoryPush(`/sites/pages/editor/${page.id}`, true);
109
104
  } else {
110
105
  setSelectedStructuredData(page.structuredData.id, "site");
111
106
  setCurrentDataID(page.id);
112
107
  await getDataContent(page.id);
113
- setHistoryPush(`/sites/data/${page.structuredData.id}/editor`);
108
+ setHistoryPush(`/sites/data/${page.structuredData.id}/content`);
114
109
  }
115
110
  };
116
111
  return (
@@ -132,13 +127,11 @@ const UsageContent = (props: IProps) => {
132
127
  }
133
128
  const hasGlobal = currentUser?.isSuperAdmin || !!currentUser?.roles.find((rol) => rol.siteId === "global");
134
129
  const handleClick = (item: IImageUseItem) => {
135
- setCurrentPageID(item.id);
136
130
  if (isSiteView) {
137
131
  setCurrentSiteInfo(null);
138
132
  getUserCurrentPermissions();
139
133
  }
140
- const path = "/data/pages/editor";
141
- setHistoryPush(path, true);
134
+ setHistoryPush(`/data/pages/editor/${item.id}`, true);
142
135
  };
143
136
  return <ItemGroup title="Global Pages" items={items.globalPages} onClick={handleClick} disabled={!hasGlobal} />;
144
137
  };
@@ -157,7 +150,7 @@ const UsageContent = (props: IProps) => {
157
150
  setCurrentSiteInfo(null);
158
151
  getUserCurrentPermissions();
159
152
  }
160
- setHistoryPush(`/data/${item.structuredData.id}/editor`);
153
+ setHistoryPush(`/data/${item.structuredData.id}/content`);
161
154
  };
162
155
  return (
163
156
  <ItemGroup title="Global Simple Content" items={items.simpleData} onClick={handleClick} disabled={!hasGlobal} />
@@ -196,7 +189,6 @@ interface IProps {
196
189
  selectedTab: "site" | "global";
197
190
  currentUser: IUser | null;
198
191
  setHistoryPush(page: string, isEditor?: boolean): Promise<void>;
199
- setCurrentPageID(currentPageID: number | null): ISetCurrentPageIDAction;
200
192
  getSite(siteID: number): Promise<void>;
201
193
  setCurrentDataID(id: number | null): void;
202
194
  setSelectedStructuredData(id: string, scope: string): void;
@@ -220,7 +212,6 @@ export interface IImageUseItem {
220
212
 
221
213
  const mapDispatchToProps = {
222
214
  setHistoryPush: appActions.setHistoryPush,
223
- setCurrentPageID: pageEditorActions.setCurrentPageID,
224
215
  getSite: sitesActions.getSite,
225
216
  setCurrentSiteInfo: sitesActions.setCurrentSiteInfo,
226
217
  setCurrentDataID: structuredDataActions.setCurrentDataID,
@@ -623,7 +623,7 @@ const MediaGallery = (props: IProps) => {
623
623
  );
624
624
 
625
625
  return (
626
- <MainWrapper backLink={false} title="Media Gallery" rightButton={rightButtonProps}>
626
+ <MainWrapper backLink={false} title="Media Gallery" rightButton={rightButtonProps} screen="gallery">
627
627
  <S.Wrapper ref={wrapperRef}>
628
628
  <S.FolderPanel
629
629
  isOpen={isPanelOpen}
@@ -154,6 +154,40 @@ interface IUserEditingModal {
154
154
  onBack: () => void;
155
155
  }
156
156
 
157
+ const ConflictSaveModal = (props: IConflictSaveModal): JSX.Element => {
158
+ const { isOpen, toggleModal, onSaveAnyway } = props;
159
+
160
+ const mainAction = {
161
+ title: "Continue anyway",
162
+ onClick: onSaveAnyway,
163
+ };
164
+
165
+ const secondaryAction = { title: "Cancel", onClick: toggleModal };
166
+
167
+ return (
168
+ <Modal
169
+ isOpen={isOpen}
170
+ hide={toggleModal}
171
+ size="S"
172
+ height="auto"
173
+ title="Continue with an outdated version?"
174
+ mainAction={mainAction}
175
+ secondaryAction={secondaryAction}
176
+ >
177
+ <S.ModalContent>
178
+ <p>
179
+ This page <strong>was updated in another tab</strong> after you opened it, so what you see here{" "}
180
+ <strong>is out of date</strong>. Reload to get the latest version first.
181
+ </p>
182
+ </S.ModalContent>
183
+ </Modal>
184
+ );
185
+ };
186
+
187
+ interface IConflictSaveModal extends IModal {
188
+ onSaveAnyway: () => void;
189
+ }
190
+
157
191
  const UnpublishModal = (props: IUnpublishModal): JSX.Element => {
158
192
  const { isOpen, toggleModal } = props;
159
193
 
@@ -181,4 +215,4 @@ interface IUnpublishModal {
181
215
  toggleModal: () => void;
182
216
  }
183
217
 
184
- export { UnpublishModal, UserEditingModal, DeleteModal, DirtyNavigateModal };
218
+ export { UnpublishModal, UserEditingModal, DeleteModal, DirtyNavigateModal, ConflictSaveModal };