@griddo/ax 1.66.3 → 1.66.4

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 (71) hide show
  1. package/package.json +2 -2
  2. package/src/api/pages.tsx +15 -3
  3. package/src/api/redirects.tsx +4 -2
  4. package/src/api/sites.tsx +4 -2
  5. package/src/components/Browser/index.tsx +3 -1
  6. package/src/components/Browser/style.tsx +2 -2
  7. package/src/components/ConfigPanel/Form/ConnectedField/PageConnectedField/Field/index.tsx +0 -1
  8. package/src/components/ErrorCenter/index.tsx +8 -5
  9. package/src/components/ErrorCenter/style.tsx +21 -8
  10. package/src/components/Fields/ColorPicker/index.tsx +1 -0
  11. package/src/components/Fields/LinkField/index.tsx +85 -0
  12. package/src/components/Fields/ReferenceField/ItemList/index.tsx +5 -1
  13. package/src/components/Fields/ReferenceField/index.tsx +18 -14
  14. package/src/components/Fields/UrlField/index.tsx +13 -1
  15. package/src/components/Fields/index.tsx +2 -0
  16. package/src/components/FieldsBehavior/index.tsx +14 -1
  17. package/src/components/Icon/components/Copy.js +14 -0
  18. package/src/components/Icon/svgs/Copy2.svg +3 -0
  19. package/src/components/MainWrapper/AppBar/index.tsx +21 -10
  20. package/src/components/MainWrapper/AppBar/style.tsx +11 -3
  21. package/src/components/MainWrapper/index.tsx +2 -0
  22. package/src/components/Modal/style.tsx +0 -1
  23. package/src/components/SearchField/index.tsx +36 -4
  24. package/src/components/SearchField/style.tsx +23 -10
  25. package/src/components/SideModal/style.tsx +6 -6
  26. package/src/components/TableFilters/StatusFilter/index.tsx +2 -2
  27. package/src/components/index.tsx +2 -0
  28. package/src/containers/App/actions.tsx +3 -7
  29. package/src/containers/PageEditor/actions.tsx +91 -22
  30. package/src/containers/PageEditor/constants.tsx +1 -1
  31. package/src/containers/PageEditor/interfaces.tsx +6 -6
  32. package/src/containers/PageEditor/reducer.tsx +4 -4
  33. package/src/containers/PageEditor/utils.tsx +2 -1
  34. package/src/containers/Sites/actions.tsx +35 -23
  35. package/src/containers/Sites/constants.tsx +1 -0
  36. package/src/containers/Sites/interfaces.tsx +6 -0
  37. package/src/containers/Sites/reducer.tsx +4 -0
  38. package/src/forms/editor.tsx +34 -1
  39. package/src/forms/errors.tsx +1 -0
  40. package/src/forms/index.tsx +15 -1
  41. package/src/forms/validators.tsx +168 -9
  42. package/src/guards/error/index.tsx +1 -1
  43. package/src/helpers/dataPacks.tsx +8 -1
  44. package/src/helpers/index.tsx +2 -1
  45. package/src/modules/Content/PageItem/index.tsx +54 -4
  46. package/src/modules/Content/atoms.tsx +41 -3
  47. package/src/modules/Content/index.tsx +111 -64
  48. package/src/modules/Content/style.tsx +8 -1
  49. package/src/modules/GlobalEditor/Editor/index.tsx +3 -1
  50. package/src/modules/GlobalEditor/PageBrowser/index.tsx +3 -0
  51. package/src/modules/GlobalEditor/index.tsx +8 -6
  52. package/src/modules/Navigation/Menus/List/Table/SidePanel/Form/index.tsx +8 -0
  53. package/src/modules/PageEditor/Editor/index.tsx +6 -2
  54. package/src/modules/PageEditor/PageBrowser/index.tsx +3 -0
  55. package/src/modules/PageEditor/index.tsx +29 -15
  56. package/src/modules/Redirects/index.tsx +40 -10
  57. package/src/modules/Settings/ContentTypes/DataPacks/Config/Form/TemplateConfig/TemplateEditor/Editor/index.tsx +1 -1
  58. package/src/modules/Settings/ContentTypes/DataPacks/Config/Form/TemplateConfig/TemplateEditor/index.tsx +1 -1
  59. package/src/modules/Settings/ContentTypes/DataPacks/Config/index.tsx +1 -1
  60. package/src/modules/Settings/ContentTypes/DataPacks/index.tsx +1 -1
  61. package/src/modules/Sites/index.tsx +3 -3
  62. package/src/modules/StructuredData/StructuredDataList/GlobalPageItem/index.tsx +1 -1
  63. package/src/modules/StructuredData/StructuredDataList/atoms.tsx +1 -1
  64. package/src/modules/Users/Profile/index.tsx +3 -4
  65. package/src/modules/Users/UserCreate/SiteItem/index.tsx +1 -1
  66. package/src/modules/Users/UserCreate/SiteItem/style.tsx +1 -1
  67. package/src/modules/Users/UserForm/style.tsx +3 -3
  68. package/src/modules/Users/UserList/UserItem/index.tsx +3 -1
  69. package/src/modules/Users/UserList/hooks.tsx +1 -1
  70. package/src/modules/Users/UserList/index.tsx +2 -2
  71. package/src/types/index.tsx +16 -3
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  SET_SITES,
3
+ SET_SITES_BY_LANG,
3
4
  SET_CURRENT_SITE_INFO,
4
5
  SET_CURRENT_SITE_PAGES,
5
6
  SET_FILTER,
@@ -17,6 +18,7 @@ export interface ISitesState {
17
18
  currentSiteName: string | null;
18
19
  currentSitePages: IPage[];
19
20
  sites: ISite[];
21
+ sitesByLang: ISite[];
20
22
  currentSiteInfo: any;
21
23
  currentFilter: string | null;
22
24
  totalItems: number;
@@ -28,6 +30,7 @@ export const initialState = {
28
30
  currentSiteName: null,
29
31
  currentSitePages: [],
30
32
  sites: [],
33
+ sitesByLang: [],
31
34
  currentSiteInfo: null,
32
35
  currentFilter: "unique-pages",
33
36
  totalItems: 0,
@@ -39,6 +42,7 @@ export function reducer(state = initialState, action: SitesActionsCreators): ISi
39
42
  switch (action.type) {
40
43
  case SET_FILTER:
41
44
  case SET_SITES:
45
+ case SET_SITES_BY_LANG:
42
46
  case SET_CURRENT_SITE_INFO:
43
47
  case SET_CURRENT_SITE_PAGES:
44
48
  case SET_TOTAL_ITEMS:
@@ -1,4 +1,4 @@
1
- import { deepClone } from "@ax/helpers";
1
+ import { deepClone, getSchema } from "@ax/helpers";
2
2
  import { IPage, IBreadcrumbItem } from "@ax/types";
3
3
 
4
4
  const configKeys = ["headerConfig", "footerConfig"];
@@ -164,6 +164,38 @@ const getParentKey = (parentModule: any, editorID: number) => {
164
164
  return keyFound;
165
165
  };
166
166
 
167
+ const checkMaxModules = (content: any, type: string): { isMaxModules: boolean; errorMessage?: string } => {
168
+ const { maxModulesPerPage } = getSchema(type);
169
+ const queue: any[] = [content];
170
+ let counter = 0;
171
+
172
+ while (queue.length > 0 && counter < maxModulesPerPage) {
173
+ const obj = queue.shift();
174
+ const currentObj = obj;
175
+
176
+ if (currentObj.component === type) {
177
+ counter++;
178
+ }
179
+
180
+ const keys = currentObj instanceof Object ? Object.keys(currentObj) : [];
181
+
182
+ for (const key of keys) {
183
+ const objVal = currentObj[key];
184
+ if (objVal instanceof Object) {
185
+ queue.push(objVal);
186
+ }
187
+ }
188
+ }
189
+
190
+ const isMaxModules = counter >= maxModulesPerPage;
191
+ const errorMessage = `There can be only ${maxModulesPerPage} ${type} on page. You already have it.`;
192
+
193
+ return {
194
+ isMaxModules,
195
+ ...(isMaxModules && { errorMessage }),
196
+ };
197
+ };
198
+
167
199
  export {
168
200
  parseData,
169
201
  cleanContent,
@@ -177,4 +209,5 @@ export {
177
209
  getLastModuleEditorID,
178
210
  getLastComponentEditorID,
179
211
  getParentKey,
212
+ checkMaxModules,
180
213
  };
@@ -40,6 +40,7 @@ const ERRORS: Record<string, string> = {
40
40
  ERR039: "Sorry, this color doesn't exist. Please add new one.",
41
41
  ERR040: "Sorry, the file is not in a valid format.",
42
42
  ERR041: "Sorry, the password doesn’t match.",
43
+ ERR042: "This content is part of disabled content type package. To publish it, you must first activate it."
43
44
  };
44
45
 
45
46
  export { ERRORS };
@@ -10,6 +10,7 @@ import {
10
10
  getLastModuleEditorID,
11
11
  getLastComponentEditorID,
12
12
  getParentKey,
13
+ checkMaxModules,
13
14
  } from "./editor";
14
15
  import {
15
16
  getUpdatedComponents,
@@ -23,7 +24,15 @@ import {
23
24
  replaceElements,
24
25
  } from "./elements";
25
26
  import { getInnerFields, getStructuredDataInnerFields } from "./fields";
26
- import { getValidity, findFieldsErrors, findMandatoryStructuredDataErrors } from "./validators";
27
+ import {
28
+ getValidity,
29
+ findPackagesActivationErrors,
30
+ findFieldsErrors,
31
+ isTemplateActivated,
32
+ findMandatoryStructuredDataErrors,
33
+ checkH1content,
34
+ parseValidationErrors,
35
+ } from "./validators";
27
36
 
28
37
  export {
29
38
  parseData,
@@ -49,6 +58,11 @@ export {
49
58
  getLastComponentEditorID,
50
59
  getParentKey,
51
60
  getValidity,
61
+ findPackagesActivationErrors,
52
62
  findFieldsErrors,
63
+ isTemplateActivated,
53
64
  findMandatoryStructuredDataErrors,
65
+ checkMaxModules,
66
+ checkH1content,
67
+ parseValidationErrors,
54
68
  };
@@ -1,5 +1,15 @@
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
+ isComponentEmpty,
8
+ isEmptyContainer,
9
+ isModuleDisabled,
10
+ } from "@ax/helpers";
11
+ import { findByEditorID } from "@ax/forms";
12
+ import { IErrorItem, ITemplate } from "@ax/types";
3
13
  import { ERRORS } from "./errors";
4
14
 
5
15
  const VALIDATORS = {
@@ -96,6 +106,13 @@ const VALIDATORS = {
96
106
  return { isValid: true, errorCode: "" };
97
107
  }
98
108
  },
109
+ isMockup: (val: any, field: { type: string; defaultValue: any }): IError => {
110
+ const isValid = !checkMockupByType(field.type, val, field.defaultValue);
111
+ return { isValid, errorCode: "ERR016" };
112
+ },
113
+ apiValidator: (val: any, code: string): IError => {
114
+ return { isValid: false, errorCode: code };
115
+ },
99
116
  isSamePass: (pass1: string, pass2: string): IError => {
100
117
  const isValid = pass1 === pass2;
101
118
  return { isValid, errorCode: "ERR041" };
@@ -161,6 +178,23 @@ const isEmptyField = (value: any, fieldType: string, multiple: boolean) => {
161
178
  }
162
179
  };
163
180
 
181
+ const checkMockupByType = (type: string, value: any, defaultValue: any) => {
182
+ if (!value || !defaultValue) return false;
183
+ switch (type) {
184
+ case "HeadingField":
185
+ return value.content && defaultValue.content && value.content.trim() === defaultValue.content.trim();
186
+ case "ImageField":
187
+ return value.publicId === defaultValue.publicId;
188
+ default:
189
+ return value.trim() === defaultValue.trim();
190
+ }
191
+ };
192
+
193
+ const checkMockupContent = (component: string, key: string, type: string, value: any) => {
194
+ const moduleDefault = getDefaultSchema(component);
195
+ return { isMockup: checkMockupByType(type, value, moduleDefault[key]), defaultValue: moduleDefault[key] };
196
+ };
197
+
164
198
  const getValidationErrors = (
165
199
  fields: Record<string, unknown>[],
166
200
  current: any,
@@ -176,8 +210,8 @@ const getValidationErrors = (
176
210
  const isEmpty = isEmptyField(current[field.key], field.type, hasMultipleOptions);
177
211
  if (isEmpty) {
178
212
  errors.push({
179
- type: "Error",
180
- message: "Empty Field",
213
+ type: "error",
214
+ message: getErrorMessage("ERR015", null),
181
215
  validator: { mandatory: true },
182
216
  editorID: current.editorID ? current.editorID : null,
183
217
  component: current.component ? current.component : null,
@@ -189,14 +223,42 @@ const getValidationErrors = (
189
223
  }
190
224
  }
191
225
 
192
- if (Object.prototype.hasOwnProperty.call(field, "validators")) {
193
- const { isValid, errorText } = getValidity(field.validators, current[field.key]);
226
+ if (current.component && field.isMockup) {
227
+ const { isMockup, defaultValue } = checkMockupContent(
228
+ current.component,
229
+ field.key,
230
+ field.type,
231
+ current[field.key]
232
+ );
233
+
234
+ if (isMockup) {
235
+ errors.push({
236
+ type: "error",
237
+ message: getErrorMessage("ERR016", null),
238
+ validator: { isMockup: { type: field.type, defaultValue } },
239
+ editorID: current.editorID ? current.editorID : null,
240
+ component: current.component ? current.component : null,
241
+ name: name ? name : field.title,
242
+ key: field.key,
243
+ tab,
244
+ template,
245
+ });
246
+ }
247
+ }
248
+
249
+ let fieldValidators: Record<string, unknown> = field.maxValue ? { maxValue: field.maxValue } : {};
250
+ fieldValidators = field.minValue ? { ...fieldValidators, minValue: field.minValue } : fieldValidators;
251
+
252
+ if (Object.prototype.hasOwnProperty.call(field, "validators") || Object.keys(fieldValidators).length) {
253
+ const allValidators = { ...field.validators, ...fieldValidators };
254
+
255
+ const { isValid, errorText } = getValidity(allValidators, current[field.key]);
194
256
 
195
257
  if (!isValid) {
196
258
  errors.push({
197
- type: "Error",
259
+ type: "error",
198
260
  message: errorText,
199
- validator: field.validators,
261
+ validator: allValidators,
200
262
  editorID: current.editorID ? current.editorID : null,
201
263
  component: current.component ? current.component : null,
202
264
  name: name ? name : field.title,
@@ -221,6 +283,57 @@ const getValidationErrors = (
221
283
  return errors;
222
284
  };
223
285
 
286
+ const isTemplateActivated = (templates: ITemplate[], currentTemplateType: string): boolean =>
287
+ templates.find((temp: ITemplate) => temp.id === currentTemplateType) ? true : false;
288
+
289
+ const findPackagesActivationErrors = (
290
+ pageEditor: any,
291
+ modules: string[],
292
+ templates: ITemplate[]
293
+ ): IErrorItem | null => {
294
+ const {
295
+ schema,
296
+ selectedContent: { component },
297
+ } = pageEditor;
298
+
299
+ let deactivatedModules: string[] = [];
300
+ let isCurrentTemplateActivated = true;
301
+ let hasDeactivatedModules = false;
302
+
303
+ const {
304
+ editorContent: { template },
305
+ } = pageEditor?.editorContent;
306
+
307
+ if (template) {
308
+ const mainContentModules = template?.mainContent?.modules;
309
+
310
+ if (mainContentModules) {
311
+ deactivatedModules = getDeactivatedModules(modules, mainContentModules);
312
+ hasDeactivatedModules = deactivatedModules.length > 0;
313
+ } else {
314
+ hasDeactivatedModules = isModuleDisabled(component, schema.schemaType, modules);
315
+ }
316
+
317
+ isCurrentTemplateActivated = isTemplateActivated(templates, template.templateType);
318
+ }
319
+
320
+ if (!isCurrentTemplateActivated || hasDeactivatedModules) {
321
+ return {
322
+ type: "error",
323
+ message: getErrorMessage("ERR042", null),
324
+ validator: {},
325
+ editorID: null,
326
+ component: "",
327
+ name: "",
328
+ key: "",
329
+ tab: "",
330
+ template: false,
331
+ };
332
+ }
333
+
334
+ return null;
335
+ };
336
+
224
337
  const findFieldsErrors = (content: any): IErrorItem[] => {
225
338
  const queue: any[] = [content];
226
339
  let errors: IErrorItem[] = [];
@@ -274,9 +387,55 @@ const findMandatoryStructuredDataErrors = (content: any, schema: any): IErrorIte
274
387
  return errors;
275
388
  };
276
389
 
390
+ const checkH1content = (content: any): IErrorItem | null => {
391
+ const h1s = content.getElementsByTagName("h1");
392
+
393
+ if (!h1s.length) {
394
+ return {
395
+ type: "warning",
396
+ message: getErrorMessage("ERR018", null),
397
+ validator: {},
398
+ editorID: null,
399
+ component: null,
400
+ name: "",
401
+ key: "",
402
+ tab: "",
403
+ template: false,
404
+ };
405
+ }
406
+
407
+ return null;
408
+ };
409
+
410
+ const parseValidationErrors = (errors: any[], content: any) => {
411
+ return errors.map((err: any) => {
412
+ const { element: module } = findByEditorID(content, err.editorID);
413
+ const schema = getSchema(module.component);
414
+ return {
415
+ type: "error",
416
+ message: getErrorMessage(err.error, null),
417
+ validator: { apiValidator: err.error },
418
+ editorID: err.editorID,
419
+ component: module.component,
420
+ name: schema.displayName,
421
+ key: err.key,
422
+ tab: "content",
423
+ template: false,
424
+ };
425
+ });
426
+ };
427
+
277
428
  interface IError {
278
429
  isValid: boolean;
279
430
  errorCode: string;
280
431
  }
281
432
 
282
- export { getValidity, findFieldsErrors, findMandatoryStructuredDataErrors };
433
+ export {
434
+ getValidity,
435
+ isTemplateActivated,
436
+ findPackagesActivationErrors,
437
+ findFieldsErrors,
438
+ findMandatoryStructuredDataErrors,
439
+ checkH1content,
440
+ parseValidationErrors,
441
+ };
@@ -50,7 +50,7 @@ const ErrorGuard = (props: IProps) => {
50
50
  return domNode && createPortal(Notifications, domNode);
51
51
  };
52
52
 
53
- return code ? isBlocking ? <ErrorView code={code} text={text} /> : createErrorNotification() : null;
53
+ return code || text ? isBlocking ? <ErrorView code={code} text={text} /> : createErrorNotification() : null;
54
54
  };
55
55
 
56
56
  const mapStateToProps = (state: IRootState) => {
@@ -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 };
@@ -91,7 +91,7 @@ import { imageResizeCropAndCompress, compressImage } from "./imageResize";
91
91
 
92
92
  import { isEmptyArray, moveArrayElement } from "./arrays";
93
93
 
94
- import { getActivatedDataPacksIds, isModuleDisabled } from "./dataPacks";
94
+ import { getActivatedDataPacksIds, isModuleDisabled, getDeactivatedModules } from "./dataPacks";
95
95
 
96
96
  import { isDevelopment } from "./environment";
97
97
 
@@ -153,6 +153,7 @@ export {
153
153
  handleRequest,
154
154
  getActivatedDataPacksIds,
155
155
  isModuleDisabled,
156
+ getDeactivatedModules,
156
157
  getInitials,
157
158
  getSchemaType,
158
159
  getModuleCategories,
@@ -1,8 +1,9 @@
1
1
  import React, { memo, useState } from "react";
2
2
 
3
+ import { schemas } from "components";
3
4
  import { useModal } from "@ax/hooks";
4
5
  import { getHumanLastModifiedDate, getTemplateDisplayName, slugify } from "@ax/helpers";
5
- import { IPage, ISite, ISavePageParams, ICheck, IColumn, IPageLanguage, IDataPack } from "@ax/types";
6
+ import { IPage, ISite, ISavePageParams, ICheck, IColumn, IDataPack, IPageLanguage } from "@ax/types";
6
7
  import { pageStatus, ISetCurrentPageIDAction } from "@ax/containers/PageEditor/interfaces";
7
8
 
8
9
  import {
@@ -18,7 +19,7 @@ import {
18
19
  CategoryCell,
19
20
  } from "@ax/components";
20
21
 
21
- import { DeleteModal } from "../atoms";
22
+ import { DeleteModal, CopyModal } from "../atoms";
22
23
 
23
24
  import * as S from "./style";
24
25
 
@@ -34,6 +35,7 @@ const PageItem = (props: IPageItemProps): JSX.Element => {
34
35
  categoryColors,
35
36
  addCategoryColors,
36
37
  dataPacks,
38
+ sites,
37
39
  } = props;
38
40
  const { isSelected, siteLanguages, page, lang, isDuplicable } = item;
39
41
  const {
@@ -47,9 +49,10 @@ const PageItem = (props: IPageItemProps): JSX.Element => {
47
49
  languageActions,
48
50
  duplicatePage,
49
51
  removePageFromSite,
50
- deleteBulk,
51
52
  getDataPack,
53
+ deleteBulk,
52
54
  setTemplateInstanceError,
55
+ toggleCopiedToast,
53
56
  } = functions;
54
57
  const { locale } = lang;
55
58
  const {
@@ -62,16 +65,22 @@ const PageItem = (props: IPageItemProps): JSX.Element => {
62
65
  templateId,
63
66
  structuredDataContent,
64
67
  } = page;
68
+
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) => {
@@ -403,6 +419,25 @@ const PageItem = (props: IPageItemProps): JSX.Element => {
403
419
  };
404
420
 
405
421
  const secondaryDeleteModalAction = { title: "Cancel", onClick: toggleDeleteModal };
422
+ const copyToOtherSite = () => {
423
+ if (site) {
424
+ const siteID = parseInt(site);
425
+
426
+ duplicatePage(page.id, null, siteID).then((successEvent: boolean) => {
427
+ if (successEvent) {
428
+ toggleCopiedToast();
429
+ }
430
+ toggleCopyModal();
431
+ });
432
+ }
433
+ };
434
+
435
+ const secondaryCopyModalAction = {
436
+ title: "Cancel",
437
+ onClick: toggleCopyModal,
438
+ };
439
+
440
+ const mainCopyModalAction = { title: "Copy page", onClick: copyToOtherSite, disabled: !site };
406
441
 
407
442
  const CategoryColumns =
408
443
  isGlobal &&
@@ -471,6 +506,18 @@ const PageItem = (props: IPageItemProps): JSX.Element => {
471
506
  <S.StyledActionMenu icon="more" options={menuOptions} tooltip="Page actions" />
472
507
  </S.ActionsCell>
473
508
  </S.PageRow>
509
+ <CopyModal
510
+ isOpen={isCopyOpen}
511
+ toggleModal={() => {
512
+ setSite(null);
513
+ toggleCopyModal();
514
+ }}
515
+ mainModalAction={mainCopyModalAction}
516
+ secondaryModalAction={secondaryCopyModalAction}
517
+ sites={sites}
518
+ site={site}
519
+ setSite={setSite}
520
+ />
474
521
  <Modal
475
522
  isOpen={isOpen}
476
523
  hide={toggleModal}
@@ -534,6 +581,7 @@ const PageItem = (props: IPageItemProps): JSX.Element => {
534
581
  </S.ModalContent>
535
582
  )}
536
583
  </Modal>
584
+
537
585
  <DeleteModal
538
586
  isOpen={isDeleteOpen}
539
587
  toggleModal={toggleDeleteModal}
@@ -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
 
@@ -68,6 +68,38 @@ const DeleteModal = (props: IDeleteModal): JSX.Element => {
68
68
  );
69
69
  };
70
70
 
71
+ const CopyModal = (props: ICopyModal): JSX.Element => {
72
+ const { isOpen, toggleModal, mainModalAction, secondaryModalAction, setSite, sites, site } = props;
73
+ const sitesOptions = sites.map((site: ISite) => ({ label: site.name, value: site.id.toString() }));
74
+
75
+ return (
76
+ <Modal
77
+ isOpen={isOpen}
78
+ hide={toggleModal}
79
+ size="S"
80
+ title="Copy page in another site"
81
+ mainAction={mainModalAction}
82
+ secondaryAction={secondaryModalAction}
83
+ >
84
+ <S.ModalContent>
85
+ <p>
86
+ <strong>Select a site to copy this page. </strong>
87
+ <br></br>
88
+ You can only select sites with the same language as this page.
89
+ </p>
90
+ <S.SelectWrapper>
91
+ <Select
92
+ name="select"
93
+ options={sitesOptions}
94
+ onChange={(value: string) => setSite(value)}
95
+ value={site?.toString() || ""}
96
+ mandatory={true}
97
+ />
98
+ </S.SelectWrapper>
99
+ </S.ModalContent>
100
+ </Modal>
101
+ );
102
+ };
71
103
  const MainActionButton = (props: IActionButton): JSX.Element => (
72
104
  <Button type="button" onClick={props.onClick}>
73
105
  {props.title}
@@ -87,9 +119,15 @@ interface IDeleteModal extends IModal {
87
119
  title?: string;
88
120
  }
89
121
 
122
+ interface ICopyModal extends IModal {
123
+ setSite: React.Dispatch<React.SetStateAction<any>>;
124
+ sites: ISite[];
125
+ site: string | null;
126
+ }
127
+
90
128
  interface IActionButton {
91
129
  onClick: () => void;
92
130
  title: string;
93
131
  }
94
132
 
95
- export { DeleteModal, MainActionButton, SecondaryActionButton };
133
+ export { DeleteModal, CopyModal, MainActionButton, SecondaryActionButton };