@griddo/ax 1.66.13 → 1.67.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 (74) hide show
  1. package/package.json +2 -2
  2. package/src/__tests__/components/Fields/ConditionalField/ConditionalField.test.tsx +95 -0
  3. package/src/api/pages.tsx +15 -3
  4. package/src/api/redirects.tsx +4 -2
  5. package/src/api/sites.tsx +12 -4
  6. package/src/components/Browser/index.tsx +9 -22
  7. package/src/components/Browser/style.tsx +1 -6
  8. package/src/components/ErrorCenter/index.tsx +8 -5
  9. package/src/components/ErrorCenter/style.tsx +21 -8
  10. package/src/components/Fields/ComponentArray/MixableComponentArray/AddItemButton/index.tsx +3 -3
  11. package/src/components/Fields/ComponentArray/MixableComponentArray/index.tsx +60 -25
  12. package/src/components/Fields/ComponentContainer/index.tsx +21 -7
  13. package/src/components/Fields/ConditionalField/index.tsx +1 -1
  14. package/src/components/Fields/LinkField/index.tsx +111 -0
  15. package/src/components/Fields/ReferenceField/ItemList/index.tsx +4 -0
  16. package/src/components/Fields/ReferenceField/ManualPanel/index.tsx +12 -2
  17. package/src/components/Fields/ReferenceField/index.tsx +24 -12
  18. package/src/components/Fields/ReferenceField/style.tsx +12 -1
  19. package/src/components/Fields/UrlField/index.tsx +13 -1
  20. package/src/components/Fields/VisualUniqueSelection/utils.tsx +1 -6
  21. package/src/components/Fields/index.tsx +2 -0
  22. package/src/components/FieldsBehavior/index.tsx +14 -1
  23. package/src/components/Icon/components/Copy.js +14 -0
  24. package/src/components/Icon/components/Copy2.js +14 -0
  25. package/src/components/Icon/components/Duplicate.js +3 -5
  26. package/src/components/Icon/components/Page.js +12 -0
  27. package/src/components/Icon/svgs/Copy.svg +3 -0
  28. package/src/components/Icon/svgs/Copy2.svg +3 -0
  29. package/src/components/Icon/svgs/Duplicate.svg +1 -1
  30. package/src/components/Icon/svgs/page.svg +3 -0
  31. package/src/components/MainWrapper/AppBar/index.tsx +21 -10
  32. package/src/components/MainWrapper/AppBar/style.tsx +11 -3
  33. package/src/components/MainWrapper/index.tsx +2 -0
  34. package/src/components/Notification/index.tsx +1 -3
  35. package/src/components/SearchField/index.tsx +37 -4
  36. package/src/components/SearchField/style.tsx +23 -10
  37. package/src/components/index.tsx +2 -0
  38. package/src/containers/Navigation/Defaults/actions.tsx +2 -0
  39. package/src/containers/PageEditor/actions.tsx +92 -17
  40. package/src/containers/PageEditor/utils.tsx +2 -1
  41. package/src/containers/Sites/actions.tsx +53 -24
  42. package/src/containers/Sites/constants.tsx +2 -0
  43. package/src/containers/Sites/interfaces.tsx +12 -5
  44. package/src/containers/Sites/reducer.tsx +8 -0
  45. package/src/containers/StructuredData/actions.tsx +5 -8
  46. package/src/forms/index.tsx +9 -1
  47. package/src/forms/validators.tsx +119 -12
  48. package/src/helpers/index.tsx +2 -0
  49. package/src/helpers/objects.tsx +10 -2
  50. package/src/modules/Categories/CategoriesList/CategoryItem/index.tsx +3 -1
  51. package/src/modules/Categories/CategoriesList/CategoryPanel/index.tsx +15 -9
  52. package/src/modules/Categories/CategoriesList/index.tsx +2 -1
  53. package/src/modules/Content/PageItem/index.tsx +52 -2
  54. package/src/modules/Content/atoms.tsx +41 -3
  55. package/src/modules/Content/index.tsx +44 -2
  56. package/src/modules/Content/style.tsx +8 -1
  57. package/src/modules/FramePreview/index.tsx +85 -0
  58. package/src/modules/FramePreview/style.tsx +18 -0
  59. package/src/modules/GlobalEditor/Editor/index.tsx +3 -1
  60. package/src/modules/GlobalEditor/PageBrowser/index.tsx +3 -0
  61. package/src/modules/GlobalEditor/index.tsx +22 -6
  62. package/src/modules/PageEditor/Editor/index.tsx +5 -1
  63. package/src/modules/PageEditor/PageBrowser/index.tsx +4 -5
  64. package/src/modules/PageEditor/index.tsx +27 -9
  65. package/src/modules/Redirects/index.tsx +40 -10
  66. package/src/modules/Settings/Globals/index.tsx +1 -1
  67. package/src/modules/Sites/index.tsx +2 -2
  68. package/src/modules/StructuredData/StructuredDataList/GlobalPageItem/index.tsx +1 -1
  69. package/src/modules/StructuredData/StructuredDataList/index.tsx +19 -2
  70. package/src/modules/Users/Profile/index.tsx +3 -3
  71. package/src/routes/multisite.tsx +12 -4
  72. package/src/routes/site.tsx +1 -1
  73. package/src/types/index.tsx +13 -4
  74. package/tsconfig.paths.json +2 -1
@@ -1,4 +1,13 @@
1
- import { dateToString, getSchema, getTemplate, isComponentEmpty, isEmptyContainer } from "@ax/helpers";
1
+ import {
2
+ dateToString,
3
+ getDefaultSchema,
4
+ getSchema,
5
+ getTemplate,
6
+ hasProps,
7
+ isComponentEmpty,
8
+ isEmptyContainer,
9
+ } from "@ax/helpers";
10
+ import { findByEditorID } from "@ax/forms";
2
11
  import { IErrorItem } from "@ax/types";
3
12
  import { ERRORS } from "./errors";
4
13
 
@@ -96,6 +105,13 @@ const VALIDATORS = {
96
105
  return { isValid: true, errorCode: "" };
97
106
  }
98
107
  },
108
+ isMockup: (val: any, field: { type: string; defaultValue: any }): IError => {
109
+ const isValid = !checkMockupByType(field.type, val, field.defaultValue);
110
+ return { isValid, errorCode: "ERR016" };
111
+ },
112
+ apiValidator: (val: any, code: string): IError => {
113
+ return { isValid: false, errorCode: code };
114
+ },
99
115
  isSamePass: (pass1: string, pass2: string): IError => {
100
116
  const isValid = pass1 === pass2;
101
117
  return { isValid, errorCode: "ERR041" };
@@ -156,11 +172,33 @@ const isEmptyField = (value: any, fieldType: string, multiple: boolean) => {
156
172
  const { isEmpty } = isEmptyContainer(value, multiple);
157
173
  return isEmpty;
158
174
  }
175
+ case "NumberField":
176
+ return value === null || Number.isNaN(value);
159
177
  default:
160
178
  return typeof value === "string" && value.trim().length === 0;
161
179
  }
162
180
  };
163
181
 
182
+ const checkMockupByType = (type: string, value: any, defaultValue: any) => {
183
+ if (!value || !defaultValue) return false;
184
+ switch (type) {
185
+ case "HeadingField":
186
+ return value.content && defaultValue.content && value.content.trim() === defaultValue.content.trim();
187
+ case "ImageField":
188
+ return (
189
+ (hasProps(value, ["publicId"]) && value.publicId === defaultValue.publicId) ||
190
+ (hasProps(value, ["url"]) && value.url === defaultValue.url)
191
+ );
192
+ default:
193
+ return typeof value === "string" && typeof defaultValue === "string" && value.trim() === defaultValue.trim();
194
+ }
195
+ };
196
+
197
+ const checkMockupContent = (component: string, key: string, type: string, value: any) => {
198
+ const moduleDefault = getDefaultSchema(component);
199
+ return { isMockup: checkMockupByType(type, value, moduleDefault[key]), defaultValue: moduleDefault[key] };
200
+ };
201
+
164
202
  const getValidationErrors = (
165
203
  fields: Record<string, unknown>[],
166
204
  current: any,
@@ -176,8 +214,8 @@ const getValidationErrors = (
176
214
  const isEmpty = isEmptyField(current[field.key], field.type, hasMultipleOptions);
177
215
  if (isEmpty) {
178
216
  errors.push({
179
- type: "Error",
180
- message: "Empty Field",
217
+ type: "error",
218
+ message: getErrorMessage("ERR015", null),
181
219
  validator: { mandatory: true },
182
220
  editorID: current.editorID ? current.editorID : null,
183
221
  component: current.component ? current.component : null,
@@ -189,14 +227,42 @@ const getValidationErrors = (
189
227
  }
190
228
  }
191
229
 
192
- if (Object.prototype.hasOwnProperty.call(field, "validators")) {
193
- const { isValid, errorText } = getValidity(field.validators, current[field.key]);
230
+ if (current.component && field.isMockup) {
231
+ const { isMockup, defaultValue } = checkMockupContent(
232
+ current.component,
233
+ field.key,
234
+ field.type,
235
+ current[field.key]
236
+ );
237
+
238
+ if (isMockup) {
239
+ errors.push({
240
+ type: "error",
241
+ message: getErrorMessage("ERR016", null),
242
+ validator: { isMockup: { type: field.type, defaultValue } },
243
+ editorID: current.editorID ? current.editorID : null,
244
+ component: current.component ? current.component : null,
245
+ name: name ? name : field.title,
246
+ key: field.key,
247
+ tab,
248
+ template,
249
+ });
250
+ }
251
+ }
252
+
253
+ let fieldValidators: Record<string, unknown> = field.maxValue ? { maxValue: field.maxValue } : {};
254
+ fieldValidators = field.minValue ? { ...fieldValidators, minValue: field.minValue } : fieldValidators;
255
+
256
+ if (hasProps(field, ["validators"]) || Object.keys(fieldValidators).length) {
257
+ const allValidators = { ...field.validators, ...fieldValidators };
258
+
259
+ const { isValid, errorText } = getValidity(allValidators, current[field.key]);
194
260
 
195
261
  if (!isValid) {
196
262
  errors.push({
197
- type: "Error",
263
+ type: "error",
198
264
  message: errorText,
199
- validator: field.validators,
265
+ validator: allValidators,
200
266
  editorID: current.editorID ? current.editorID : null,
201
267
  component: current.component ? current.component : null,
202
268
  name: name ? name : field.title,
@@ -208,10 +274,13 @@ const getValidationErrors = (
208
274
  }
209
275
 
210
276
  if (Object.prototype.hasOwnProperty.call(field, "fields") && field.fields.length) {
211
- const innerFields =
212
- field.type === "ConditionalField"
213
- ? field.fields.filter((f: any) => f.condition === undefined || f.condition === current[field.key])
214
- : field.fields;
277
+ let innerFields = field.fields;
278
+
279
+ if (field.type === "ConditionalField") {
280
+ innerFields = field.fields.filter((f: any) => f.condition === undefined || f.condition === current[field.key]);
281
+ const hiddenFields = field.fields.filter((f: any) => f.condition !== current[field.key]);
282
+ hiddenFields.forEach((field: any) => delete current[field.key]);
283
+ }
215
284
 
216
285
  const innerErrors = getValidationErrors(innerFields, current, name, tab, template);
217
286
  errors = [...errors, ...innerErrors];
@@ -274,9 +343,47 @@ const findMandatoryStructuredDataErrors = (content: any, schema: any): IErrorIte
274
343
  return errors;
275
344
  };
276
345
 
346
+ const checkH1content = (content: any): IErrorItem | null => {
347
+ const h1s = content.getElementsByTagName("h1");
348
+
349
+ if (!h1s.length) {
350
+ return {
351
+ type: "warning",
352
+ message: getErrorMessage("ERR018", null),
353
+ validator: {},
354
+ editorID: null,
355
+ component: null,
356
+ name: "",
357
+ key: "",
358
+ tab: "",
359
+ template: false,
360
+ };
361
+ }
362
+
363
+ return null;
364
+ };
365
+
366
+ const parseValidationErrors = (errors: any[], content: any) => {
367
+ return errors.map((err: any) => {
368
+ const { element: module } = findByEditorID(content, err.editorID);
369
+ const schema = getSchema(module.component);
370
+ return {
371
+ type: "error",
372
+ message: getErrorMessage(err.error, null),
373
+ validator: { apiValidator: err.error },
374
+ editorID: err.editorID,
375
+ component: module.component,
376
+ name: schema.displayName,
377
+ key: err.key,
378
+ tab: "content",
379
+ template: false,
380
+ };
381
+ });
382
+ };
383
+
277
384
  interface IError {
278
385
  isValid: boolean;
279
386
  errorCode: string;
280
387
  }
281
388
 
282
- export { getValidity, findFieldsErrors, findMandatoryStructuredDataErrors };
389
+ export { getValidity, findFieldsErrors, findMandatoryStructuredDataErrors, checkH1content, parseValidationErrors };
@@ -34,6 +34,7 @@ import {
34
34
  getNullValue,
35
35
  removeEditorIds,
36
36
  trimObject,
37
+ hasProps,
37
38
  } from "./objects";
38
39
 
39
40
  import {
@@ -114,6 +115,7 @@ export {
114
115
  getNullValue,
115
116
  removeEditorIds,
116
117
  trimObject,
118
+ hasProps,
117
119
  filterDuplicatedValues,
118
120
  areEquals,
119
121
  removeMenuEditorIds,
@@ -1,6 +1,6 @@
1
1
  import { IMenuItem } from "@ax/types";
2
2
 
3
- const isEmptyObj = (obj: any) => {
3
+ const isEmptyObj = (obj: any): boolean => {
4
4
  for (const key in obj) {
5
5
  if (Object.prototype.hasOwnProperty.call(obj, key)) return false;
6
6
  }
@@ -9,7 +9,7 @@ const isEmptyObj = (obj: any) => {
9
9
 
10
10
  const deepClone = (obj: any) => JSON.parse(JSON.stringify(obj));
11
11
 
12
- const isSelectedEditorID = (element: any, id: number) => element.editorID === id;
12
+ const isSelectedEditorID = (element: any, id: number): boolean => element.editorID === id;
13
13
 
14
14
  const resetMultipleValues = (containerValue: any) => {
15
15
  const { id } = containerValue;
@@ -103,6 +103,13 @@ const trimObject = (obj: any) => {
103
103
  return obj;
104
104
  };
105
105
 
106
+ const hasProps = (obj: Record<string, unknown>, props: string[]): boolean => {
107
+ return (
108
+ !!props &&
109
+ props.map((prop) => Object.prototype.hasOwnProperty.call(obj, prop)).filter(Boolean).length === props.length
110
+ );
111
+ };
112
+
106
113
  export {
107
114
  isEmptyObj,
108
115
  deepClone,
@@ -115,4 +122,5 @@ export {
115
122
  getNullValue,
116
123
  removeEditorIds,
117
124
  trimObject,
125
+ hasProps,
118
126
  };
@@ -24,6 +24,7 @@ const CategoryItem = (props: ICategoryItemProps): JSX.Element => {
24
24
  onChange,
25
25
  toggleToast,
26
26
  setDeletedItem,
27
+ getContents,
27
28
  } = props;
28
29
 
29
30
  const { isOpen, toggleModal } = useModal();
@@ -140,7 +141,7 @@ const CategoryItem = (props: ICategoryItemProps): JSX.Element => {
140
141
  <S.StyledActionMenu icon="more" options={menuOptions} tooltip="Actions" />
141
142
  </S.ActionsCell>
142
143
  </S.CategoryRow>
143
- <CategoryPanel isOpen={isOpen} toggleModal={toggleModal} item={category} />
144
+ <CategoryPanel isOpen={isOpen} toggleModal={toggleModal} item={category} getContents={getContents}/>
144
145
  </>
145
146
  );
146
147
  };
@@ -154,6 +155,7 @@ interface IProps {
154
155
  onChange: (e: any) => void;
155
156
  toggleToast(): void;
156
157
  setDeletedItem(item: number): void;
158
+ getContents(dataId: string): void;
157
159
  }
158
160
 
159
161
  interface IDispatchProps {
@@ -16,6 +16,7 @@ const CategoryPanel = (props: IProps): JSX.Element => {
16
16
  item,
17
17
  createStructuredDataContent,
18
18
  updateStructuredDataContent,
19
+ getContents,
19
20
  currentStructuredData,
20
21
  category,
21
22
  entity,
@@ -41,18 +42,22 @@ const CategoryPanel = (props: IProps): JSX.Element => {
41
42
  newCategory = { ...newCategory, id: category.isTranslation ? category.id : item.id };
42
43
  }
43
44
 
44
- const addItemAction = () => {
45
- if (lang && category.isTranslation) {
46
- createStructuredDataContent(newCategory, lang.id);
47
- } else {
48
- createStructuredDataContent(newCategory);
45
+ const addItemAction = async () => {
46
+ const langID = lang && category.isTranslation ? lang.id : undefined;
47
+ const isCreated = await createStructuredDataContent(newCategory, langID);
48
+
49
+ if (isCreated && currentStructuredData) {
50
+ getContents(currentStructuredData.id);
49
51
  }
50
52
 
51
53
  toggleModal();
52
54
  };
53
55
 
54
- const editItemAction = () => {
55
- updateStructuredDataContent(newCategory);
56
+ const editItemAction = async () => {
57
+ const isUpdated = await updateStructuredDataContent(newCategory);
58
+ if (isUpdated && currentStructuredData) {
59
+ getContents(currentStructuredData.id);
60
+ }
56
61
  toggleModal();
57
62
  };
58
63
 
@@ -122,11 +127,12 @@ interface ICategoryPanelProps {
122
127
  item?: IStructuredDataContent;
123
128
  isOpen: boolean;
124
129
  toggleModal(): any;
130
+ getContents(dataId: string): void;
125
131
  }
126
132
 
127
133
  interface IDispatchProps {
128
- createStructuredDataContent(category: IStructuredDataContent, langId?: number | null): void;
129
- updateStructuredDataContent(category: IStructuredDataContent): void;
134
+ createStructuredDataContent: (category: IStructuredDataContent, langId?: number | null) => Promise<boolean>;
135
+ updateStructuredDataContent: (category: IStructuredDataContent) => Promise<boolean>;
130
136
  }
131
137
 
132
138
  type IProps = IDispatchProps & ICategoryPanelProps & IStateProps;
@@ -209,6 +209,7 @@ const CategoriesList = (props: IProps): JSX.Element => {
209
209
  onChange={addToBulkSelection}
210
210
  toggleToast={toggleToast}
211
211
  setDeletedItem={setDeletedItem}
212
+ getContents={getContents}
212
213
  />
213
214
  );
214
215
  })
@@ -216,7 +217,7 @@ const CategoriesList = (props: IProps): JSX.Element => {
216
217
  </TableList>
217
218
  </S.TableWrapper>
218
219
  </S.CategoryListWrapper>
219
- <CategoryPanel isOpen={isOpen} toggleModal={toggleModal} />
220
+ <CategoryPanel isOpen={isOpen} toggleModal={toggleModal} getContents={getContents}/>
220
221
  {isVisible && <Toast {...toastProps} />}
221
222
  </MainWrapper>
222
223
  );
@@ -1,5 +1,7 @@
1
1
  import React, { memo, useState } from "react";
2
2
 
3
+ import { schemas } from "components";
4
+
3
5
  import { useModal } from "@ax/hooks";
4
6
  import { getHumanLastModifiedDate, getTemplateDisplayName, slugify } from "@ax/helpers";
5
7
  import { IPage, ISite, ISavePageParams, ICheck, IColumn, IPageLanguage, IDataPack } from "@ax/types";
@@ -18,7 +20,7 @@ import {
18
20
  CategoryCell,
19
21
  } from "@ax/components";
20
22
 
21
- import { DeleteModal } from "../atoms";
23
+ import { DeleteModal, CopyModal } from "../atoms";
22
24
 
23
25
  import * as S from "./style";
24
26
 
@@ -34,6 +36,7 @@ const PageItem = (props: IPageItemProps): JSX.Element => {
34
36
  categoryColors,
35
37
  addCategoryColors,
36
38
  dataPacks,
39
+ sites,
37
40
  } = props;
38
41
  const { isSelected, siteLanguages, page, lang, isDuplicable } = item;
39
42
  const {
@@ -50,6 +53,7 @@ const PageItem = (props: IPageItemProps): JSX.Element => {
50
53
  deleteBulk,
51
54
  getDataPack,
52
55
  setTemplateInstanceError,
56
+ toggleCopiedToast,
53
57
  } = functions;
54
58
  const { locale } = lang;
55
59
  const {
@@ -65,13 +69,18 @@ const PageItem = (props: IPageItemProps): JSX.Element => {
65
69
  const displayName = getTemplateDisplayName(templateId);
66
70
 
67
71
  const initValue = { title: "", slug: "" };
72
+ const [site, setSite] = useState(null);
68
73
  const [modalState, setModalState] = useState(initValue);
69
74
  const [deleteAllVersions, setDeleteAllVersions] = useState(false);
70
75
  const { isOpen, toggleModal } = useModal();
71
76
  const { isOpen: isRemoveOpen, toggleModal: toggleRemoveModal } = useModal();
72
77
  const { isOpen: isUnpublishOpen, toggleModal: toggleUnpublishModal } = useModal();
73
78
  const { isOpen: isDeleteOpen, toggleModal: toggleDeleteModal } = useModal();
79
+ const { isOpen: isCopyOpen, toggleModal: toggleCopyModal } = useModal();
80
+
81
+ const currentTemplateDataPacks = schemas.templates[templateId].dataPacks;
74
82
 
83
+ const isCopyable = !currentTemplateDataPacks;
75
84
  const isGlobal = origin === "GLOBAL";
76
85
  const isTranslated = pageLanguages.length > 1;
77
86
  const activeColumns = Object.keys(columns).filter((col: string) => columns[col].show);
@@ -311,6 +320,13 @@ const PageItem = (props: IPageItemProps): JSX.Element => {
311
320
  action: toggleModal,
312
321
  };
313
322
 
323
+ const copyOption = {
324
+ label: "Copy page in another site",
325
+ icon: "copy",
326
+ action: toggleCopyModal,
327
+ };
328
+
329
+ if (isCopyable) menuOptions.unshift(copyOption);
314
330
  if (!isGlobal) menuOptions.unshift(duplicateOption);
315
331
 
316
332
  const getPublishItem = (status: string, canBeUnpublished: boolean) => {
@@ -393,6 +409,26 @@ const PageItem = (props: IPageItemProps): JSX.Element => {
393
409
 
394
410
  const secondaryRemoveModalAction = { title: "Cancel", onClick: toggleRemoveModal };
395
411
 
412
+ const copyToOtherSite = () => {
413
+ if (site) {
414
+ const siteID = parseInt(site);
415
+
416
+ duplicatePage(page.id, null, siteID).then((successEvent: boolean) => {
417
+ if (successEvent === true) {
418
+ toggleCopiedToast();
419
+ }
420
+ toggleCopyModal();
421
+ });
422
+ }
423
+ };
424
+
425
+ const secondaryCopyModalAction = {
426
+ title: "Cancel",
427
+ onClick: toggleCopyModal,
428
+ };
429
+
430
+ const mainCopyModalAction = { title: "Copy page", onClick: copyToOtherSite, disabled: !site };
431
+
396
432
  const getLiveStatus = () => (page.haveDraftPage ? "modified" : page.liveStatus.status);
397
433
 
398
434
  const mainUnpublishAction = { title: "Ok", onClick: toggleUnpublishModal };
@@ -471,6 +507,18 @@ const PageItem = (props: IPageItemProps): JSX.Element => {
471
507
  <S.StyledActionMenu icon="more" options={menuOptions} tooltip="Page actions" />
472
508
  </S.ActionsCell>
473
509
  </S.PageRow>
510
+ <CopyModal
511
+ isOpen={isCopyOpen}
512
+ toggleModal={() => {
513
+ setSite(null);
514
+ toggleCopyModal();
515
+ }}
516
+ mainModalAction={mainCopyModalAction}
517
+ secondaryModalAction={secondaryCopyModalAction}
518
+ sites={sites}
519
+ site={site}
520
+ setSite={setSite}
521
+ />
474
522
  <Modal
475
523
  isOpen={isOpen}
476
524
  hide={toggleModal}
@@ -576,11 +624,12 @@ interface IPageItemProps {
576
624
  setHistoryPush(path: string, isEditor: boolean): void;
577
625
  updatePageStatus(ids: number[], status: string, updatedFromList: boolean): Promise<boolean>;
578
626
  setCurrentPageID(currentPageID: number | null): ISetCurrentPageIDAction;
579
- duplicatePage(pageID: number, data: any): Promise<void>;
627
+ duplicatePage(pageID: number, data: any, siteID?: number): Promise<boolean>;
580
628
  removePageFromSite(pageID: number): Promise<boolean>;
581
629
  deleteBulk(ids: number[]): void;
582
630
  setTemplateInstanceError(error: any): void;
583
631
  getDataPack: (id: string) => Promise<void>;
632
+ toggleCopiedToast(): void;
584
633
  };
585
634
  activatedTemplates: any[];
586
635
  toggleToast(): void;
@@ -590,6 +639,7 @@ interface IPageItemProps {
590
639
  categoryColors: any;
591
640
  addCategoryColors(cats: string[]): void;
592
641
  dataPacks: IDataPack[];
642
+ sites: ISite[];
593
643
  }
594
644
 
595
645
  export default memo(PageItem);
@@ -1,7 +1,7 @@
1
1
  import React from "react";
2
2
 
3
- import { IModal } from "@ax/types";
4
- import { Modal, FieldsBehavior, Button } from "@ax/components";
3
+ import { IModal, ISite } from "@ax/types";
4
+ import { Modal, FieldsBehavior, Select, Button } from "@ax/components";
5
5
 
6
6
  import * as S from "./style";
7
7
 
@@ -80,6 +80,38 @@ const SecondaryActionButton = (props: IActionButton): JSX.Element => (
80
80
  </Button>
81
81
  );
82
82
 
83
+ const CopyModal = (props: ICopyModal): JSX.Element => {
84
+ const { isOpen, toggleModal, mainModalAction, secondaryModalAction, setSite, sites, site } = props;
85
+ const sitesOptions = sites.map((site: ISite) => ({ label: site.name, value: site.id.toString() }));
86
+
87
+ return (
88
+ <Modal
89
+ isOpen={isOpen}
90
+ hide={toggleModal}
91
+ size="S"
92
+ title="Copy page in another site"
93
+ mainAction={mainModalAction}
94
+ secondaryAction={secondaryModalAction}
95
+ >
96
+ <S.ModalContent>
97
+ <p>
98
+ <strong>Select a site to copy this page. </strong>
99
+ You can only select sites with the same language as this page.
100
+ </p>
101
+ <S.SelectWrapper>
102
+ <Select
103
+ name="select"
104
+ options={sitesOptions}
105
+ onChange={(value: string) => setSite(value)}
106
+ value={site?.toString() || ""}
107
+ mandatory={true}
108
+ />
109
+ </S.SelectWrapper>
110
+ </S.ModalContent>
111
+ </Modal>
112
+ );
113
+ };
114
+
83
115
  interface IDeleteModal extends IModal {
84
116
  isTranslated: boolean;
85
117
  deleteAllVersions: boolean;
@@ -92,4 +124,10 @@ interface IActionButton {
92
124
  title: string;
93
125
  }
94
126
 
95
- export { DeleteModal, MainActionButton, SecondaryActionButton };
127
+ interface ICopyModal extends IModal {
128
+ setSite: React.Dispatch<React.SetStateAction<any>>;
129
+ sites: ISite[];
130
+ site: string | null;
131
+ }
132
+
133
+ export { DeleteModal, MainActionButton, SecondaryActionButton, CopyModal };
@@ -15,6 +15,7 @@ import {
15
15
  IGetSitePagesParams,
16
16
  IColumn,
17
17
  ISite,
18
+ IUser,
18
19
  } from "@ax/types";
19
20
  import { MainWrapper, Modal, TableList, ErrorToast, Toast, EmptyState, Notification } from "@ax/components";
20
21
  import { getFilteredStructuredData, isGlobalStructuredData, isStructuredDataFromPage } from "@ax/helpers";
@@ -86,6 +87,11 @@ const Content = (props: IProps): JSX.Element => {
86
87
  restorePage,
87
88
  getDataPack,
88
89
  dataPacks,
90
+ resetCurrentSiteErrorPages,
91
+ currentSiteErrorPages,
92
+ getSitesByLang,
93
+ sitesByLang,
94
+ user,
89
95
  } = props;
90
96
 
91
97
  const itemsPerPage = 50;
@@ -109,7 +115,9 @@ const Content = (props: IProps): JSX.Element => {
109
115
  const dataIds = currentDataContent && currentDataContent.map((data: any) => data.id);
110
116
  const contentIds = isStructuredData ? dataIds : pagesIds;
111
117
  const currentSitePagesTemplatesIds = currentSitePages && currentSitePages.map((page: any) => page.templateId);
112
-
118
+ const currentSitesByLang = sitesByLang?.filter(
119
+ (site) => user?.sites?.includes("all") || user.sites.includes(site.id)
120
+ );
113
121
  const categoryColumns =
114
122
  currentStructuredData && currentStructuredData.schema
115
123
  ? currentStructuredData.schema.fields.filter((field: any) => field.showList)
@@ -171,6 +179,11 @@ const Content = (props: IProps): JSX.Element => {
171
179
  } = useToast();
172
180
 
173
181
  const { categoryColors, addCategoryColors } = useCategoryColors();
182
+ const {
183
+ isVisible: isVisibleCopiedToast,
184
+ toggleToast: toggleCopiedToast,
185
+ setIsVisible: setIsVisibleCopiedToast,
186
+ } = useToast();
174
187
 
175
188
  const getParams = useCallback(() => {
176
189
  const siteID = currentSiteInfo ? currentSiteInfo.id : null;
@@ -231,11 +244,15 @@ const Content = (props: IProps): JSX.Element => {
231
244
  // eslint-disable-next-line react-hooks/exhaustive-deps
232
245
  }, [filter]);
233
246
 
247
+ const fetchSitesByLang = async () => await getSitesByLang(lang.id);
248
+
234
249
  useEffect(() => {
235
250
  if (!locationState || locationState.isFromEditor !== true) {
236
251
  setFilter("unique-pages");
237
252
  }
238
253
  resetPageEditor();
254
+ resetCurrentSiteErrorPages();
255
+ fetchSitesByLang();
239
256
  // eslint-disable-next-line react-hooks/exhaustive-deps
240
257
  }, []);
241
258
 
@@ -533,6 +550,7 @@ const Content = (props: IProps): JSX.Element => {
533
550
  deleteBulk: deleteCurrentPageBulk,
534
551
  getDataPack: getDataPack,
535
552
  setTemplateInstanceError,
553
+ toggleCopiedToast,
536
554
  };
537
555
 
538
556
  return (
@@ -540,6 +558,7 @@ const Content = (props: IProps): JSX.Element => {
540
558
  item={item}
541
559
  key={pageItem.id}
542
560
  functions={pageItemFunctions}
561
+ sites={currentSitesByLang}
543
562
  activatedTemplates={activatedTemplates}
544
563
  toggleToast={toggleRemovedToast}
545
564
  setRemovedPage={setRemovedPage}
@@ -650,8 +669,18 @@ const Content = (props: IProps): JSX.Element => {
650
669
  message: "Page deleted.",
651
670
  };
652
671
 
672
+ const copiedToastProps = {
673
+ setIsVisible: setIsVisibleCopiedToast,
674
+ message: "1 Page copied to another Site",
675
+ };
676
+
653
677
  const addNewAction = filter === "unique-pages" || isGlobalPages ? toggleNewModal : addNewData;
654
678
 
679
+ const errorPagesText =
680
+ currentSiteErrorPages.length > 1
681
+ ? "These pages contains some errors, so you can not publish them yet. Please, review the errors on the pages."
682
+ : "This page contains some errors, so you can not publish it yet. Please, review the errors on the page.";
683
+
655
684
  return (
656
685
  <MainWrapper
657
686
  title={title}
@@ -671,6 +700,7 @@ const Content = (props: IProps): JSX.Element => {
671
700
  text={`There can be only one ${templateInstanceError.templateName} page and you already have it.`}
672
701
  />
673
702
  )}
703
+ {!!currentSiteErrorPages.length && <Notification type="error" text={errorPagesText} />}
674
704
  <TableList
675
705
  tableHeader={Header}
676
706
  pagination={pagination}
@@ -720,6 +750,7 @@ const Content = (props: IProps): JSX.Element => {
720
750
  {isVisible && <Toast {...toastProps} />}
721
751
  {isVisibleRemovedToast && <Toast {...removedToastProps} />}
722
752
  {isVisibleDeletedToast && <Toast {...deletedToastProps} />}
753
+ {isVisibleCopiedToast && <Toast {...copiedToastProps} />}
723
754
  </MainWrapper>
724
755
  );
725
756
  };
@@ -741,6 +772,9 @@ const mapStateToProps = (state: IRootState) => ({
741
772
  activatedTemplates: state.dataPacks.templates,
742
773
  isLoading: state.app.isLoading,
743
774
  dataPacks: state.dataPacks.activated,
775
+ currentSiteErrorPages: state.sites.currentSiteErrorPages,
776
+ sitesByLang: state.sites.sitesByLang,
777
+ user: state.users.currentUser,
744
778
  });
745
779
 
746
780
  interface IDispatchProps {
@@ -760,7 +794,7 @@ interface IDispatchProps {
760
794
  getStructuredDataContents(params: any, siteID: number): Promise<void>;
761
795
  resetForm(): void;
762
796
  deleteBulk(ids: any): Promise<boolean>;
763
- duplicatePage(pageID: number, data: any): Promise<void>;
797
+ duplicatePage(pageID: number, data?: any, siteID?: number): Promise<boolean>;
764
798
  deleteDataContent(dataID: number[]): Promise<boolean>;
765
799
  restoreDataContent(catID: number | number[]): void;
766
800
  setFilter(value: string): void;
@@ -770,6 +804,8 @@ interface IDispatchProps {
770
804
  importPageFromGlobal(pageID: number | number[]): Promise<boolean>;
771
805
  restorePage(id: number | number[]): Promise<boolean>;
772
806
  getDataPack: (id: string) => Promise<void>;
807
+ resetCurrentSiteErrorPages: () => Promise<void>;
808
+ getSitesByLang(language: number): Promise<void>;
773
809
  }
774
810
 
775
811
  const mapDispatchToProps = {
@@ -798,6 +834,8 @@ const mapDispatchToProps = {
798
834
  importPageFromGlobal: sitesActions.importPageFromGlobal,
799
835
  restorePage: pageEditorActions.restorePage,
800
836
  getDataPack: dataPacksActions.getSiteDataPack,
837
+ resetCurrentSiteErrorPages: sitesActions.resetCurrentSiteErrorPages,
838
+ getSitesByLang: sitesActions.getSitesByLang,
801
839
  };
802
840
 
803
841
  interface IPagesProps {
@@ -819,6 +857,10 @@ interface IPagesProps {
819
857
  activatedTemplates: any[];
820
858
  isLoading: boolean;
821
859
  dataPacks: IDataPack[];
860
+ currentSiteErrorPages: number[];
861
+ sites: ISite[];
862
+ sitesByLang: ISite[];
863
+ user: IUser;
822
864
  }
823
865
 
824
866
  type IProps = IPagesProps & IDispatchProps;