@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.
- package/package.json +2 -2
- package/src/__tests__/components/Fields/ConditionalField/ConditionalField.test.tsx +95 -0
- package/src/api/pages.tsx +15 -3
- package/src/api/redirects.tsx +4 -2
- package/src/api/sites.tsx +12 -4
- package/src/components/Browser/index.tsx +9 -22
- package/src/components/Browser/style.tsx +1 -6
- package/src/components/ErrorCenter/index.tsx +8 -5
- package/src/components/ErrorCenter/style.tsx +21 -8
- package/src/components/Fields/ComponentArray/MixableComponentArray/AddItemButton/index.tsx +3 -3
- package/src/components/Fields/ComponentArray/MixableComponentArray/index.tsx +60 -25
- package/src/components/Fields/ComponentContainer/index.tsx +21 -7
- package/src/components/Fields/ConditionalField/index.tsx +1 -1
- package/src/components/Fields/LinkField/index.tsx +111 -0
- package/src/components/Fields/ReferenceField/ItemList/index.tsx +4 -0
- package/src/components/Fields/ReferenceField/ManualPanel/index.tsx +12 -2
- package/src/components/Fields/ReferenceField/index.tsx +24 -12
- package/src/components/Fields/ReferenceField/style.tsx +12 -1
- package/src/components/Fields/UrlField/index.tsx +13 -1
- package/src/components/Fields/VisualUniqueSelection/utils.tsx +1 -6
- package/src/components/Fields/index.tsx +2 -0
- package/src/components/FieldsBehavior/index.tsx +14 -1
- package/src/components/Icon/components/Copy.js +14 -0
- package/src/components/Icon/components/Copy2.js +14 -0
- package/src/components/Icon/components/Duplicate.js +3 -5
- package/src/components/Icon/components/Page.js +12 -0
- package/src/components/Icon/svgs/Copy.svg +3 -0
- package/src/components/Icon/svgs/Copy2.svg +3 -0
- package/src/components/Icon/svgs/Duplicate.svg +1 -1
- package/src/components/Icon/svgs/page.svg +3 -0
- package/src/components/MainWrapper/AppBar/index.tsx +21 -10
- package/src/components/MainWrapper/AppBar/style.tsx +11 -3
- package/src/components/MainWrapper/index.tsx +2 -0
- package/src/components/Notification/index.tsx +1 -3
- package/src/components/SearchField/index.tsx +37 -4
- package/src/components/SearchField/style.tsx +23 -10
- package/src/components/index.tsx +2 -0
- package/src/containers/Navigation/Defaults/actions.tsx +2 -0
- package/src/containers/PageEditor/actions.tsx +92 -17
- package/src/containers/PageEditor/utils.tsx +2 -1
- package/src/containers/Sites/actions.tsx +53 -24
- package/src/containers/Sites/constants.tsx +2 -0
- package/src/containers/Sites/interfaces.tsx +12 -5
- package/src/containers/Sites/reducer.tsx +8 -0
- package/src/containers/StructuredData/actions.tsx +5 -8
- package/src/forms/index.tsx +9 -1
- package/src/forms/validators.tsx +119 -12
- package/src/helpers/index.tsx +2 -0
- package/src/helpers/objects.tsx +10 -2
- package/src/modules/Categories/CategoriesList/CategoryItem/index.tsx +3 -1
- package/src/modules/Categories/CategoriesList/CategoryPanel/index.tsx +15 -9
- package/src/modules/Categories/CategoriesList/index.tsx +2 -1
- package/src/modules/Content/PageItem/index.tsx +52 -2
- package/src/modules/Content/atoms.tsx +41 -3
- package/src/modules/Content/index.tsx +44 -2
- package/src/modules/Content/style.tsx +8 -1
- package/src/modules/FramePreview/index.tsx +85 -0
- package/src/modules/FramePreview/style.tsx +18 -0
- package/src/modules/GlobalEditor/Editor/index.tsx +3 -1
- package/src/modules/GlobalEditor/PageBrowser/index.tsx +3 -0
- package/src/modules/GlobalEditor/index.tsx +22 -6
- package/src/modules/PageEditor/Editor/index.tsx +5 -1
- package/src/modules/PageEditor/PageBrowser/index.tsx +4 -5
- package/src/modules/PageEditor/index.tsx +27 -9
- package/src/modules/Redirects/index.tsx +40 -10
- package/src/modules/Settings/Globals/index.tsx +1 -1
- package/src/modules/Sites/index.tsx +2 -2
- package/src/modules/StructuredData/StructuredDataList/GlobalPageItem/index.tsx +1 -1
- package/src/modules/StructuredData/StructuredDataList/index.tsx +19 -2
- package/src/modules/Users/Profile/index.tsx +3 -3
- package/src/routes/multisite.tsx +12 -4
- package/src/routes/site.tsx +1 -1
- package/src/types/index.tsx +13 -4
- package/tsconfig.paths.json +2 -1
package/src/forms/validators.tsx
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
import {
|
|
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: "
|
|
180
|
-
message: "
|
|
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 (
|
|
193
|
-
const {
|
|
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: "
|
|
263
|
+
type: "error",
|
|
198
264
|
message: errorText,
|
|
199
|
-
validator:
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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 };
|
package/src/helpers/index.tsx
CHANGED
|
@@ -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,
|
package/src/helpers/objects.tsx
CHANGED
|
@@ -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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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)
|
|
129
|
-
updateStructuredDataContent(category: IStructuredDataContent)
|
|
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<
|
|
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
|
-
|
|
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
|
|
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;
|