@griddo/ax 1.66.13 → 1.67.2

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 (76) 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 +101 -19
  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/errors.tsx +1 -0
  47. package/src/forms/index.tsx +13 -1
  48. package/src/forms/validators.tsx +181 -13
  49. package/src/helpers/dataPacks.tsx +8 -1
  50. package/src/helpers/index.tsx +4 -1
  51. package/src/helpers/objects.tsx +10 -2
  52. package/src/modules/Categories/CategoriesList/CategoryItem/index.tsx +3 -1
  53. package/src/modules/Categories/CategoriesList/CategoryPanel/index.tsx +15 -9
  54. package/src/modules/Categories/CategoriesList/index.tsx +2 -1
  55. package/src/modules/Content/PageItem/index.tsx +52 -2
  56. package/src/modules/Content/atoms.tsx +41 -3
  57. package/src/modules/Content/index.tsx +44 -2
  58. package/src/modules/Content/style.tsx +8 -1
  59. package/src/modules/FramePreview/index.tsx +85 -0
  60. package/src/modules/FramePreview/style.tsx +18 -0
  61. package/src/modules/GlobalEditor/Editor/index.tsx +3 -1
  62. package/src/modules/GlobalEditor/PageBrowser/index.tsx +3 -0
  63. package/src/modules/GlobalEditor/index.tsx +22 -6
  64. package/src/modules/PageEditor/Editor/index.tsx +5 -1
  65. package/src/modules/PageEditor/PageBrowser/index.tsx +4 -5
  66. package/src/modules/PageEditor/index.tsx +40 -12
  67. package/src/modules/Redirects/index.tsx +40 -10
  68. package/src/modules/Settings/Globals/index.tsx +1 -1
  69. package/src/modules/Sites/index.tsx +2 -2
  70. package/src/modules/StructuredData/StructuredDataList/GlobalPageItem/index.tsx +1 -1
  71. package/src/modules/StructuredData/StructuredDataList/index.tsx +19 -2
  72. package/src/modules/Users/Profile/index.tsx +3 -3
  73. package/src/routes/multisite.tsx +12 -4
  74. package/src/routes/site.tsx +1 -1
  75. package/src/types/index.tsx +13 -4
  76. package/tsconfig.paths.json +2 -1
@@ -1,5 +1,16 @@
1
- import { dateToString, getSchema, getTemplate, isComponentEmpty, isEmptyContainer } from "@ax/helpers";
2
- import { IErrorItem } from "@ax/types";
1
+ import {
2
+ dateToString,
3
+ getDeactivatedModules,
4
+ getDefaultSchema,
5
+ getSchema,
6
+ getTemplate,
7
+ hasProps,
8
+ isComponentEmpty,
9
+ isEmptyContainer,
10
+ isModuleDisabled,
11
+ } from "@ax/helpers";
12
+ import { findByEditorID } from "@ax/forms";
13
+ import { IErrorItem, ITemplate } from "@ax/types";
3
14
  import { ERRORS } from "./errors";
4
15
 
5
16
  const VALIDATORS = {
@@ -96,6 +107,13 @@ const VALIDATORS = {
96
107
  return { isValid: true, errorCode: "" };
97
108
  }
98
109
  },
110
+ isMockup: (val: any, field: { type: string; defaultValue: any }): IError => {
111
+ const isValid = !checkMockupByType(field.type, val, field.defaultValue);
112
+ return { isValid, errorCode: "ERR016" };
113
+ },
114
+ apiValidator: (val: any, code: string): IError => {
115
+ return { isValid: false, errorCode: code };
116
+ },
99
117
  isSamePass: (pass1: string, pass2: string): IError => {
100
118
  const isValid = pass1 === pass2;
101
119
  return { isValid, errorCode: "ERR041" };
@@ -156,11 +174,33 @@ const isEmptyField = (value: any, fieldType: string, multiple: boolean) => {
156
174
  const { isEmpty } = isEmptyContainer(value, multiple);
157
175
  return isEmpty;
158
176
  }
177
+ case "NumberField":
178
+ return value === null || Number.isNaN(value);
159
179
  default:
160
180
  return typeof value === "string" && value.trim().length === 0;
161
181
  }
162
182
  };
163
183
 
184
+ const checkMockupByType = (type: string, value: any, defaultValue: any) => {
185
+ if (!value || !defaultValue) return false;
186
+ switch (type) {
187
+ case "HeadingField":
188
+ return value.content && defaultValue.content && value.content.trim() === defaultValue.content.trim();
189
+ case "ImageField":
190
+ return (
191
+ (hasProps(value, ["publicId"]) && value.publicId === defaultValue.publicId) ||
192
+ (hasProps(value, ["url"]) && value.url === defaultValue.url)
193
+ );
194
+ default:
195
+ return typeof value === "string" && typeof defaultValue === "string" && value.trim() === defaultValue.trim();
196
+ }
197
+ };
198
+
199
+ const checkMockupContent = (component: string, key: string, type: string, value: any) => {
200
+ const moduleDefault = getDefaultSchema(component);
201
+ return { isMockup: checkMockupByType(type, value, moduleDefault[key]), defaultValue: moduleDefault[key] };
202
+ };
203
+
164
204
  const getValidationErrors = (
165
205
  fields: Record<string, unknown>[],
166
206
  current: any,
@@ -176,8 +216,8 @@ const getValidationErrors = (
176
216
  const isEmpty = isEmptyField(current[field.key], field.type, hasMultipleOptions);
177
217
  if (isEmpty) {
178
218
  errors.push({
179
- type: "Error",
180
- message: "Empty Field",
219
+ type: "error",
220
+ message: getErrorMessage("ERR015", null),
181
221
  validator: { mandatory: true },
182
222
  editorID: current.editorID ? current.editorID : null,
183
223
  component: current.component ? current.component : null,
@@ -189,14 +229,42 @@ const getValidationErrors = (
189
229
  }
190
230
  }
191
231
 
192
- if (Object.prototype.hasOwnProperty.call(field, "validators")) {
193
- const { isValid, errorText } = getValidity(field.validators, current[field.key]);
232
+ if (current.component && field.isMockup) {
233
+ const { isMockup, defaultValue } = checkMockupContent(
234
+ current.component,
235
+ field.key,
236
+ field.type,
237
+ current[field.key]
238
+ );
239
+
240
+ if (isMockup) {
241
+ errors.push({
242
+ type: "error",
243
+ message: getErrorMessage("ERR016", null),
244
+ validator: { isMockup: { type: field.type, defaultValue } },
245
+ editorID: current.editorID ? current.editorID : null,
246
+ component: current.component ? current.component : null,
247
+ name: name ? name : field.title,
248
+ key: field.key,
249
+ tab,
250
+ template,
251
+ });
252
+ }
253
+ }
254
+
255
+ let fieldValidators: Record<string, unknown> = field.maxValue ? { maxValue: field.maxValue } : {};
256
+ fieldValidators = field.minValue ? { ...fieldValidators, minValue: field.minValue } : fieldValidators;
257
+
258
+ if (hasProps(field, ["validators"]) || Object.keys(fieldValidators).length) {
259
+ const allValidators = { ...field.validators, ...fieldValidators };
260
+
261
+ const { isValid, errorText } = getValidity(allValidators, current[field.key]);
194
262
 
195
263
  if (!isValid) {
196
264
  errors.push({
197
- type: "Error",
265
+ type: "error",
198
266
  message: errorText,
199
- validator: field.validators,
267
+ validator: allValidators,
200
268
  editorID: current.editorID ? current.editorID : null,
201
269
  component: current.component ? current.component : null,
202
270
  name: name ? name : field.title,
@@ -208,10 +276,13 @@ const getValidationErrors = (
208
276
  }
209
277
 
210
278
  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;
279
+ let innerFields = field.fields;
280
+
281
+ if (field.type === "ConditionalField") {
282
+ innerFields = field.fields.filter((f: any) => f.condition === undefined || f.condition === current[field.key]);
283
+ const hiddenFields = field.fields.filter((f: any) => f.condition !== current[field.key]);
284
+ hiddenFields.forEach((field: any) => delete current[field.key]);
285
+ }
215
286
 
216
287
  const innerErrors = getValidationErrors(innerFields, current, name, tab, template);
217
288
  errors = [...errors, ...innerErrors];
@@ -221,6 +292,57 @@ const getValidationErrors = (
221
292
  return errors;
222
293
  };
223
294
 
295
+ const isTemplateActivated = (templates: ITemplate[], currentTemplateType: string): boolean =>
296
+ templates.find((temp: ITemplate) => temp.id === currentTemplateType) ? true : false;
297
+
298
+ const findPackagesActivationErrors = (
299
+ pageEditor: any,
300
+ modules: string[],
301
+ templates: ITemplate[]
302
+ ): IErrorItem | null => {
303
+ const {
304
+ schema,
305
+ selectedContent: { component },
306
+ } = pageEditor;
307
+
308
+ let deactivatedModules: string[] = [];
309
+ let isCurrentTemplateActivated = true;
310
+ let hasDeactivatedModules = false;
311
+
312
+ const {
313
+ editorContent: { template },
314
+ } = pageEditor?.editorContent;
315
+
316
+ if (template) {
317
+ const mainContentModules = template?.mainContent?.modules;
318
+
319
+ if (mainContentModules) {
320
+ deactivatedModules = getDeactivatedModules(modules, mainContentModules);
321
+ hasDeactivatedModules = deactivatedModules.length > 0;
322
+ } else {
323
+ hasDeactivatedModules = isModuleDisabled(component, schema.schemaType, modules);
324
+ }
325
+
326
+ isCurrentTemplateActivated = isTemplateActivated(templates, template.templateType);
327
+ }
328
+
329
+ if (!isCurrentTemplateActivated || hasDeactivatedModules) {
330
+ return {
331
+ type: "error",
332
+ message: getErrorMessage("ERR042", null),
333
+ validator: {},
334
+ editorID: null,
335
+ component: "",
336
+ name: "",
337
+ key: "",
338
+ tab: "",
339
+ template: false,
340
+ };
341
+ }
342
+
343
+ return null;
344
+ };
345
+
224
346
  const findFieldsErrors = (content: any): IErrorItem[] => {
225
347
  const queue: any[] = [content];
226
348
  let errors: IErrorItem[] = [];
@@ -274,9 +396,55 @@ const findMandatoryStructuredDataErrors = (content: any, schema: any): IErrorIte
274
396
  return errors;
275
397
  };
276
398
 
399
+ const checkH1content = (content: any): IErrorItem | null => {
400
+ const h1s = content.getElementsByTagName("h1");
401
+
402
+ if (!h1s.length) {
403
+ return {
404
+ type: "warning",
405
+ message: getErrorMessage("ERR018", null),
406
+ validator: {},
407
+ editorID: null,
408
+ component: null,
409
+ name: "",
410
+ key: "",
411
+ tab: "",
412
+ template: false,
413
+ };
414
+ }
415
+
416
+ return null;
417
+ };
418
+
419
+ const parseValidationErrors = (errors: any[], content: any) => {
420
+ return errors.map((err: any) => {
421
+ const { element: module } = findByEditorID(content, err.editorID);
422
+ const schema = getSchema(module.component);
423
+ return {
424
+ type: "error",
425
+ message: getErrorMessage(err.error, null),
426
+ validator: { apiValidator: err.error },
427
+ editorID: err.editorID,
428
+ component: module.component,
429
+ name: schema.displayName,
430
+ key: err.key,
431
+ tab: "content",
432
+ template: false,
433
+ };
434
+ });
435
+ };
436
+
277
437
  interface IError {
278
438
  isValid: boolean;
279
439
  errorCode: string;
280
440
  }
281
441
 
282
- export { getValidity, findFieldsErrors, findMandatoryStructuredDataErrors };
442
+ export {
443
+ getValidity,
444
+ isTemplateActivated,
445
+ findPackagesActivationErrors,
446
+ findFieldsErrors,
447
+ findMandatoryStructuredDataErrors,
448
+ checkH1content,
449
+ parseValidationErrors,
450
+ };
@@ -8,4 +8,11 @@ const getActivatedDataPacksIds = (activatedDataPacks: any) => {
8
8
  const isModuleDisabled = (selectedComponent: string, type: string, activatedModules: string[]): boolean =>
9
9
  type === "module" && !activatedModules.includes(selectedComponent);
10
10
 
11
- export { getActivatedDataPacksIds, isModuleDisabled };
11
+ const getDeactivatedModules = (modules: any, currentModules: any) => {
12
+ const deactivatedModules = currentModules
13
+ .map((module: any) => (isModuleDisabled(module.component, "module", modules) ? module.component : null))
14
+ .filter((module: string | null) => !!module);
15
+ return deactivatedModules;
16
+ };
17
+
18
+ export { getActivatedDataPacksIds, isModuleDisabled, getDeactivatedModules };
@@ -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 {
@@ -91,7 +92,7 @@ import { imageResizeCropAndCompress, compressImage } from "./imageResize";
91
92
 
92
93
  import { isEmptyArray, moveArrayElement } from "./arrays";
93
94
 
94
- import { getActivatedDataPacksIds, isModuleDisabled } from "./dataPacks";
95
+ import { getActivatedDataPacksIds, isModuleDisabled, getDeactivatedModules } from "./dataPacks";
95
96
 
96
97
  import { isDevelopment } from "./environment";
97
98
 
@@ -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,
@@ -153,6 +155,7 @@ export {
153
155
  handleRequest,
154
156
  getActivatedDataPacksIds,
155
157
  isModuleDisabled,
158
+ getDeactivatedModules,
156
159
  getInitials,
157
160
  getSchemaType,
158
161
  getModuleCategories,
@@ -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 };