@griddo/ax 11.6.1 → 11.6.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 (24) hide show
  1. package/package.json +2 -3
  2. package/src/components/Browser/index.tsx +2 -1
  3. package/src/components/Browser/style.tsx +2 -1
  4. package/src/components/Fields/ComponentArray/MixableComponentArray/AddItemButton/index.tsx +1 -1
  5. package/src/components/Fields/ComponentArray/MixableComponentArray/index.tsx +12 -5
  6. package/src/components/Fields/FormContainer/FormModal/index.tsx +11 -23
  7. package/src/components/Fields/FormContainer/FormModal/style.tsx +2 -0
  8. package/src/components/SearchField/index.tsx +1 -0
  9. package/src/components/SideModal/SideModalOption/index.tsx +2 -3
  10. package/src/components/SideModal/SideModalOption/style.tsx +2 -2
  11. package/src/components/SideModal/index.tsx +7 -37
  12. package/src/containers/Forms/actions.tsx +7 -3
  13. package/src/containers/Forms/reducer.tsx +1 -1
  14. package/src/modules/Forms/FormEditor/Editor/SideModal/SectionOption/index.tsx +23 -0
  15. package/src/modules/Forms/FormEditor/Editor/SideModal/SectionOption/style.tsx +20 -0
  16. package/src/modules/Forms/FormEditor/Editor/SideModal/SideModalOption/index.tsx +34 -0
  17. package/src/modules/Forms/FormEditor/Editor/SideModal/SideModalOption/style.tsx +29 -0
  18. package/src/modules/Forms/FormEditor/Editor/SideModal/index.tsx +118 -0
  19. package/src/modules/Forms/FormEditor/Editor/SideModal/style.tsx +116 -0
  20. package/src/modules/Forms/FormEditor/Editor/index.tsx +53 -3
  21. package/src/modules/Forms/FormEditor/Editor/style.tsx +9 -0
  22. package/src/modules/Forms/FormEditor/PageBrowser/style.tsx +0 -1
  23. package/src/modules/MediaGallery/index.tsx +6 -0
  24. package/src/types/index.tsx +1 -0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@griddo/ax",
3
3
  "description": "Griddo Author Experience",
4
- "version": "11.6.1",
4
+ "version": "11.6.2",
5
5
  "authors": [
6
6
  "Álvaro Sánchez' <alvaro.sanches@secuoyas.com>",
7
7
  "Diego M. Béjar <diego.bejar@secuoyas.com>",
@@ -99,7 +99,6 @@
99
99
  "jsdom-global": "3.0.2",
100
100
  "lodash.isequal": "4.5.0",
101
101
  "markdown-draft-js": "2.4.0",
102
- "masonic": "4.1.0",
103
102
  "mini-css-extract-plugin": "0.11.3",
104
103
  "optimize-css-assets-webpack-plugin": "6.0.1",
105
104
  "pkg-dir": "5.0.0",
@@ -224,5 +223,5 @@
224
223
  "publishConfig": {
225
224
  "access": "public"
226
225
  },
227
- "gitHead": "07d7b29c342d4683beafc6fd8e9c26a572c38e25"
226
+ "gitHead": "ce1be2946a2f6320ce6b82d38a3cdd6277b13000"
228
227
  }
@@ -35,6 +35,7 @@ const Browser = (props: IBrowserProps): JSX.Element => {
35
35
  const domain = window.location.origin;
36
36
  const urlPreview = `${domain}/editor/page-preview?preview=${!!isPreview}&disabled=${!!disabled}&type=${editorType}`;
37
37
  const isPageEditor = editorType === "page";
38
+ const isFormEditor = editorType === "form";
38
39
 
39
40
  const [resolution, setResolution] = useState("desktop");
40
41
  const { isVisible, toggleToast, setIsVisible } = useToast();
@@ -146,7 +147,7 @@ const Browser = (props: IBrowserProps): JSX.Element => {
146
147
  </S.NavBar>
147
148
  )}
148
149
  {showIframe ? (
149
- <S.FrameWrapper hasBorder={isPageEditor} data-testid="navbar-iframe-wrapper">
150
+ <S.FrameWrapper hasBorder={isPageEditor} isFormEditor={isFormEditor} data-testid="navbar-iframe-wrapper">
150
151
  <iframe
151
152
  title="Preview"
152
153
  width={getWidth(resolution)}
@@ -46,13 +46,14 @@ const IconWrapper = styled.div<{ active?: boolean }>`
46
46
  }
47
47
  `;
48
48
 
49
- const FrameWrapper = styled.div<{ hasBorder: boolean }>`
49
+ const FrameWrapper = styled.div<{ hasBorder: boolean; isFormEditor: boolean }>`
50
50
  border-left: ${(p) => (p.hasBorder ? `1px solid ${p.theme.color.uiLine}` : "none")};
51
51
  border-right: 1px solid ${(p) => p.theme.color.uiLine};
52
52
  border-bottom: ${(p) => (p.hasBorder ? `1px solid ${p.theme.color.uiLine}` : "none")};
53
53
  display: flex;
54
54
  justify-content: center;
55
55
  height: 100%;
56
+ padding: ${(p) => (p.isFormEditor ? p.theme.spacing.m : "0")};
56
57
  `;
57
58
 
58
59
  const Wrapper = styled.div`
@@ -3,7 +3,7 @@ import { IconAction, SideModal, Tooltip } from "@ax/components";
3
3
 
4
4
  const AddItemButton = (props: IProps) => {
5
5
  const { handleClick, whiteList, categories, theme, isOpen, contentType, toggleModal } = props;
6
- const addAction = whiteList.length <= 1 ? () => handleClick(whiteList[0]) : toggleModal;
6
+ const addAction = whiteList.length <= 1 && contentType !== "fields" ? () => handleClick(whiteList[0]) : toggleModal;
7
7
 
8
8
  const tooltip = contentType === "fields" ? "Add field" : contentType === "module" ? "Add module" : "Add component";
9
9
 
@@ -54,6 +54,7 @@ const MixableComponentArray = (props: IMixableComponentArrayProps): JSX.Element
54
54
  setNotificationAction,
55
55
  copyModuleAction,
56
56
  duplicateModuleAction,
57
+ setSelectedFormFieldAction,
57
58
  } = actions || {};
58
59
 
59
60
  // fix for old not array values
@@ -76,7 +77,7 @@ const MixableComponentArray = (props: IMixableComponentArrayProps): JSX.Element
76
77
  const isModuleArr = contentType === "modules" || contentType === "fields";
77
78
  const isFormArr = contentType === "fields";
78
79
 
79
- const { isOpen, toggleModal } = useModal(isFormArr, false);
80
+ const { isOpen, toggleModal } = useModal(false, false);
80
81
  const [isBulkOpen, setIsBulkOpen] = useState(false);
81
82
  const [draggingId, setDraggingId] = useState<number | null>(null);
82
83
  const { isVisible, toggleToast, setIsVisible, state: toastState } = useToast();
@@ -123,10 +124,15 @@ const MixableComponentArray = (props: IMixableComponentArrayProps): JSX.Element
123
124
 
124
125
  const selectItems = () => (checkState.isAllSelected ? resetBulkSelection() : selectAllItems());
125
126
 
126
- const showAddItemButton = (!maxItems || fixedValue.length < maxItems) && !disabled && whiteList.length > 0;
127
+ const handleToggleModal = () =>
128
+ isFormArr && setSelectedFormFieldAction ? setSelectedFormFieldAction(field) : toggleModal();
129
+
130
+ const isAbletoAdd = (!maxItems || fixedValue.length < maxItems) && !disabled && whiteList.length > 0;
131
+
132
+ const showAddItemButton = isAbletoAdd && (!isFormArr || (isFormArr && !!setSelectedFormFieldAction));
127
133
 
128
134
  const showPasteModuleButton =
129
- showAddItemButton &&
135
+ isAbletoAdd &&
130
136
  (isModuleArr || objKey === "componentModules") &&
131
137
  (modulesToPaste.length > 0 || unavailableModules.length > 0);
132
138
 
@@ -167,7 +173,7 @@ const MixableComponentArray = (props: IMixableComponentArrayProps): JSX.Element
167
173
  actions={actions}
168
174
  selectedContent={selectedContent}
169
175
  disabled={disabled || isModuleDisabled}
170
- canDuplicate={showAddItemButton && !isModuleDeactivated}
176
+ canDuplicate={isAbletoAdd && !isModuleDeactivated}
171
177
  canDelete={canDelete}
172
178
  parentKey={objKey}
173
179
  theme={theme}
@@ -293,7 +299,7 @@ const MixableComponentArray = (props: IMixableComponentArrayProps): JSX.Element
293
299
  {showAddItemButton && !disabled && (
294
300
  <AddItemButton
295
301
  isOpen={isOpen}
296
- toggleModal={toggleModal}
302
+ toggleModal={handleToggleModal}
297
303
  whiteList={whiteList}
298
304
  categories={categories}
299
305
  handleClick={handleAdd}
@@ -351,6 +357,7 @@ export interface IMixableComponentArrayProps {
351
357
  pasteModuleAction: (editorID: number, key: string, modulesToPaste: IModule[]) => Promise<{ error?: INotification }>;
352
358
  setNotificationAction: (notification: INotification) => void;
353
359
  replaceModuleAction: (module: any, parent: any, objKey: string) => void;
360
+ setSelectedFormFieldAction: (field: ISchemaField | null) => void;
354
361
  };
355
362
  categories?: any[];
356
363
  disabled?: boolean;
@@ -1,7 +1,5 @@
1
1
  import React, { Dispatch, SetStateAction, useCallback, useEffect, useState } from "react";
2
2
  import { connect } from "react-redux";
3
- import { Masonry, RenderComponentProps } from "masonic";
4
- import { v4 as uuidv4 } from "uuid";
5
3
 
6
4
  import { FormContent, GetFormsParams, IModal, IQueryValue, IRootState, ISite } from "@ax/types";
7
5
  import {
@@ -110,19 +108,6 @@ const FormModal = (props: IFormModal): JSX.Element => {
110
108
  </S.Filters>
111
109
  );
112
110
 
113
- const MasonryCard = (props: RenderComponentProps<FormContent>) => {
114
- const { data: form } = props;
115
- const handleClick = () => setSelectedForm(form);
116
- return (
117
- <S.FormWrapper key={form.id}>
118
- <S.FormItem onClick={handleClick} isSelected={form.id === selectedForm?.id}>
119
- <img src={form.thumbnail} alt={form.title} title={form.title} />
120
- </S.FormItem>
121
- <S.FormTitle>{form.title}</S.FormTitle>
122
- </S.FormWrapper>
123
- );
124
- };
125
-
126
111
  const emptySearchStateProps = {
127
112
  icon: "search",
128
113
  title: "Oh! No Results Found",
@@ -174,14 +159,17 @@ const FormModal = (props: IFormModal): JSX.Element => {
174
159
  </S.EmptyWrapper>
175
160
  ) : (
176
161
  <S.FormsGrid>
177
- <Masonry
178
- key={uuidv4()}
179
- items={forms}
180
- columnGutter={16}
181
- columnWidth={336}
182
- render={MasonryCard}
183
- columnCount={3}
184
- />
162
+ {forms.map((form) => {
163
+ const handleClick = () => setSelectedForm(form);
164
+ return (
165
+ <S.FormWrapper key={form.id}>
166
+ <S.FormItem onClick={handleClick} isSelected={form.id === selectedForm?.id}>
167
+ <img src={form.thumbnail} alt={form.title} title={form.title} />
168
+ </S.FormItem>
169
+ <S.FormTitle>{form.title}</S.FormTitle>
170
+ </S.FormWrapper>
171
+ );
172
+ })}
185
173
  </S.FormsGrid>
186
174
  )}
187
175
  </S.ModalContent>
@@ -69,12 +69,14 @@ const TabsWrapper = styled.div`
69
69
 
70
70
  const FormsGrid = styled.div`
71
71
  padding-bottom: ${(p) => p.theme.spacing.l};
72
+ columns: 300px;
72
73
  `;
73
74
 
74
75
  const FormWrapper = styled.div`
75
76
  display: flex;
76
77
  flex-direction: column;
77
78
  padding-bottom: ${(p) => p.theme.spacing.s};
79
+ break-inside: avoid-column;
78
80
  `;
79
81
 
80
82
  const FormItem = styled.div<{ isSelected: boolean }>`
@@ -100,6 +100,7 @@ const SearchField = (props: ISearchFieldProps): JSX.Element => {
100
100
  // eslint-disable-next-line jsx-a11y/no-autofocus
101
101
  autoFocus={focus && showField}
102
102
  inputSize={size}
103
+ name="searchInput"
103
104
  />
104
105
  {inputValue.trim() !== "" && searchOnEnter && <S.HelpText>Press ENTER</S.HelpText>}
105
106
  {closeOnInactive || inputValue.length > 0 ? (
@@ -12,7 +12,7 @@ const getThumbnailData = (option: any, theme: string) => {
12
12
  };
13
13
 
14
14
  const SideModalOption = (props: IProps) => {
15
- const { option, handleClick, theme, smallMargin } = props;
15
+ const { option, handleClick, theme } = props;
16
16
 
17
17
  const isNavigationDefault = ["header", "footer"].includes(option.type);
18
18
 
@@ -36,7 +36,7 @@ const SideModalOption = (props: IProps) => {
36
36
  ) : null;
37
37
 
38
38
  return (
39
- <S.Item onClick={setOption} smallMargin={smallMargin} data-testid="side-modal-option">
39
+ <S.Item onClick={setOption} data-testid="side-modal-option">
40
40
  <S.Thumbnail data-testid="side-modal-option-img" {...thumbnailProps} />
41
41
  {label}
42
42
  {defaultTag}
@@ -49,7 +49,6 @@ interface IProps {
49
49
  handleClick?: (option: any) => void;
50
50
  children: string;
51
51
  theme: string;
52
- smallMargin: boolean;
53
52
  }
54
53
 
55
54
  export default memo(SideModalOption);
@@ -1,10 +1,10 @@
1
1
  import styled from "styled-components";
2
2
 
3
- const Item = styled.li<{ smallMargin: boolean }>`
3
+ const Item = styled.li`
4
4
  cursor: pointer;
5
5
  padding: ${(p) => p.theme.spacing.xs};
6
6
  padding-bottom: ${(p) => p.theme.spacing.s};
7
- margin-bottom: ${(p) => (p.smallMargin ? p.theme.spacing.xs : p.theme.spacing.s)};
7
+ margin-bottom: ${(p) => p.theme.spacing.s};
8
8
  box-shadow: ${(p) => p.theme.shadow.shadowS};
9
9
  border-radius: ${(p) => p.theme.radii.s};
10
10
  ${(p) => p.theme.textStyle.uiS};
@@ -4,7 +4,7 @@ import { createPortal } from "react-dom";
4
4
  import { useHandleClickOutside } from "@ax/hooks";
5
5
  import { getDisplayName, filterByCategory } from "@ax/helpers";
6
6
  import SideModalOption from "@ax/components/SideModal/SideModalOption";
7
- import { MenuItem, SearchField, IconAction, FloatingButton } from "@ax/components";
7
+ import { MenuItem, SearchField, IconAction } from "@ax/components";
8
8
  import { ModuleCategoryInfo } from "@ax/types";
9
9
 
10
10
  import * as S from "./style";
@@ -25,7 +25,6 @@ const SideModal = (props: ISideModalProps): JSX.Element | null => {
25
25
 
26
26
  const componentList: any = [];
27
27
  const hasCategories = categories && categories.length > 1;
28
- const hasFieldsStyle = optionsType === "fields";
29
28
 
30
29
  const optionList = componentOptions ? componentList : whiteList;
31
30
 
@@ -63,7 +62,7 @@ const SideModal = (props: ISideModalProps): JSX.Element | null => {
63
62
  if (onClick) {
64
63
  onClick(moduleType);
65
64
  }
66
- !hasFieldsStyle && handleCloseModal();
65
+ handleCloseModal();
67
66
  };
68
67
 
69
68
  const handleCloseModal = () => {
@@ -72,7 +71,7 @@ const SideModal = (props: ISideModalProps): JSX.Element | null => {
72
71
  };
73
72
 
74
73
  const handleClickOutside = (e: any) => {
75
- if (node.current?.contains(e.target) || hasFieldsStyle) {
74
+ if (node.current?.contains(e.target)) {
76
75
  return;
77
76
  }
78
77
 
@@ -137,13 +136,7 @@ const SideModal = (props: ISideModalProps): JSX.Element | null => {
137
136
  if (!name.includes(search)) return null;
138
137
  }
139
138
  return (
140
- <SideModalOption
141
- option={option}
142
- handleClick={handleClick}
143
- key={`${option}${i}`}
144
- theme={theme}
145
- smallMargin={hasFieldsStyle}
146
- >
139
+ <SideModalOption option={option} handleClick={handleClick} key={`${option}${i}`} theme={theme}>
147
140
  {displayName}
148
141
  </SideModalOption>
149
142
  );
@@ -151,23 +144,12 @@ const SideModal = (props: ISideModalProps): JSX.Element | null => {
151
144
 
152
145
  return createPortal(
153
146
  <>
154
- <S.Wrapper
155
- ref={node}
156
- optionsType={optionsType}
157
- isOpen={isOpen}
158
- data-testid="side-modal"
159
- className={hasFieldsStyle ? "form-fields" : ""}
160
- >
147
+ <S.Wrapper ref={node} optionsType={optionsType} isOpen={isOpen} data-testid="side-modal">
161
148
  <S.Header>
162
- {!hasFieldsStyle && <S.Title data-testid="side-modal-title">{optionsType}</S.Title>}
149
+ <S.Title data-testid="side-modal-title">{optionsType}</S.Title>
163
150
  {showSearch && optionsType !== "components" && (
164
151
  <S.SearchWrapper>
165
- <SearchField
166
- onChange={setSearchQuery}
167
- closeOnInactive={!hasFieldsStyle}
168
- small={!filters}
169
- placeholder={hasFieldsStyle ? "Search field" : ""}
170
- />
152
+ <SearchField onChange={setSearchQuery} closeOnInactive={true} small={!filters} value={searchQuery} />
171
153
  </S.SearchWrapper>
172
154
  )}
173
155
  {!showSearch && (
@@ -177,11 +159,6 @@ const SideModal = (props: ISideModalProps): JSX.Element | null => {
177
159
  )}
178
160
  </S.Header>
179
161
  <S.ColumnsWrapper>
180
- {hasFieldsStyle && (
181
- <S.FloatingButtonWrapper>
182
- <FloatingButton icon="leftArrow" size="S" onClick={handleCloseModal} />
183
- </S.FloatingButtonWrapper>
184
- )}
185
162
  {(filters || featuredFilters) && (
186
163
  <S.Content>
187
164
  {featuredFilters && <S.FeaturedWrapper>{featuredFilters}</S.FeaturedWrapper>}
@@ -191,13 +168,6 @@ const SideModal = (props: ISideModalProps): JSX.Element | null => {
191
168
  <S.Content>{filteredOptions}</S.Content>
192
169
  </S.ColumnsWrapper>
193
170
  </S.Wrapper>
194
- {hasFieldsStyle && (
195
- <S.ClosedWrapper isOpen={!isOpen}>
196
- <S.ClosedFloatingButtonWrapper>
197
- <FloatingButton text="fields" icon="rightArrow" size="S" onClick={toggleModal} />
198
- </S.ClosedFloatingButtonWrapper>
199
- </S.ClosedWrapper>
200
- )}
201
171
  </>,
202
172
  document.body
203
173
  );
@@ -597,9 +597,13 @@ function moveModule(
597
597
  if (!formContent) return;
598
598
 
599
599
  const templateContent = formContent.template;
600
+ const isRoot = selectedContent.component === "FormPage";
601
+
602
+ const contentElements = isRoot ? [...selectedContent.template[key]] : [...selectedContent[key]];
603
+ const editorID = isRoot ? templateContent.editorID : selectedContent.editorID;
604
+
605
+ const { element: selectedModule } = findByEditorID(templateContent, editorID);
600
606
 
601
- const contentElements = [...(selectedContent as any).template[key]];
602
- const { element: selectedModule } = findByEditorID(templateContent, templateContent.editorID);
603
607
  selectedModule[key] = moveElement(elementID, contentElements, newIndex);
604
608
 
605
609
  const updatedContent = {
@@ -610,7 +614,7 @@ function moveModule(
610
614
  };
611
615
 
612
616
  dispatch(setFormContent(updatedContent));
613
- dispatch(setSelectedFormContent(updatedContent));
617
+ dispatch(setSelectedFormContent(isRoot ? updatedContent : selectedModule));
614
618
  };
615
619
  }
616
620
 
@@ -25,7 +25,7 @@ export interface IFormsState {
25
25
  tab: string;
26
26
  currentFormID: number | null;
27
27
  currentFormStatus: string | null;
28
- selectedContent: Record<string, unknown>;
28
+ selectedContent: Record<string, any>;
29
29
  selectedEditorID: number;
30
30
  selectedParent: Record<string, unknown> | null;
31
31
  breadcrumb: IBreadcrumbItem[];
@@ -0,0 +1,23 @@
1
+ import React, { memo } from "react";
2
+ import { ISchemaField } from "@ax/types";
3
+
4
+ import * as S from "./style";
5
+
6
+ const SectionOption = (props: IProps) => {
7
+ const { option, handleClick } = props;
8
+
9
+ const setOption = () => handleClick(option);
10
+
11
+ return (
12
+ <S.Item onClick={setOption} data-testid="section-option">
13
+ Fields for {option.title}
14
+ </S.Item>
15
+ );
16
+ };
17
+
18
+ interface IProps {
19
+ option: ISchemaField;
20
+ handleClick: (option: ISchemaField) => void;
21
+ }
22
+
23
+ export default memo(SectionOption);
@@ -0,0 +1,20 @@
1
+ import styled from "styled-components";
2
+
3
+ const Item = styled.li`
4
+ ${(p) => p.theme.textStyle.uiS};
5
+ cursor: pointer;
6
+ padding: ${(p) => p.theme.spacing.s};
7
+ padding-bottom: ${(p) => p.theme.spacing.s};
8
+ margin-bottom: ${(p) => p.theme.spacing.xs};
9
+ box-shadow: ${(p) => p.theme.shadow.shadowS};
10
+ border-radius: ${(p) => p.theme.radii.s};
11
+ background-color: ${(p) => p.theme.color.interactiveBackground};
12
+ &:hover {
13
+ background: ${(p) => p.theme.color.overlayHoverPrimary};
14
+ }
15
+ &:focus {
16
+ background-color: ${(p) => p.theme.color.overlayFocusPrimary};
17
+ }
18
+ `;
19
+
20
+ export { Item };
@@ -0,0 +1,34 @@
1
+ import React, { memo } from "react";
2
+
3
+ import { getDisplayName, getThumbnailProps, filterImageText } from "@ax/helpers";
4
+
5
+ import * as S from "./style";
6
+
7
+ const getThumbnailData = (option: string, theme: string) => {
8
+ const optionText = filterImageText(option);
9
+ return getThumbnailProps(optionText, false, theme);
10
+ };
11
+
12
+ const SideModalOption = (props: IProps) => {
13
+ const { option, handleClick, theme } = props;
14
+ const thumbnailProps = getThumbnailData(option, theme);
15
+ const label = getDisplayName(option);
16
+
17
+ const setOption = () => handleClick(option);
18
+
19
+ return (
20
+ <S.Item onClick={setOption} data-testid="side-modal-option">
21
+ <S.Thumbnail data-testid="side-modal-option-img" {...thumbnailProps} />
22
+ {label}
23
+ </S.Item>
24
+ );
25
+ };
26
+
27
+ interface IProps {
28
+ option: string;
29
+ handleClick: (option: string) => void;
30
+ children: string;
31
+ theme: string;
32
+ }
33
+
34
+ export default memo(SideModalOption);
@@ -0,0 +1,29 @@
1
+ import styled from "styled-components";
2
+
3
+ const Item = styled.li`
4
+ cursor: pointer;
5
+ padding: ${(p) => p.theme.spacing.xs};
6
+ padding-bottom: ${(p) => p.theme.spacing.s};
7
+ margin-bottom: ${(p) => p.theme.spacing.xs};
8
+ box-shadow: ${(p) => p.theme.shadow.shadowS};
9
+ border-radius: ${(p) => p.theme.radii.s};
10
+ ${(p) => p.theme.textStyle.uiS};
11
+ background-color: ${(p) => p.theme.color.interactiveBackground};
12
+ &:hover {
13
+ background: ${(p) => p.theme.color.overlayHoverPrimary};
14
+ }
15
+ &:focus {
16
+ background-color: ${(p) => p.theme.color.overlayFocusPrimary};
17
+ }
18
+ `;
19
+
20
+ const Thumbnail = styled.img`
21
+ cursor: pointer;
22
+ padding-bottom: ${(p) => p.theme.spacing.s};
23
+ `;
24
+
25
+ const TagWrapper = styled.div`
26
+ margin-top: ${(p) => p.theme.spacing.xs};
27
+ `;
28
+
29
+ export { Item, Thumbnail, TagWrapper };
@@ -0,0 +1,118 @@
1
+ import React, { useState } from "react";
2
+
3
+ import { getDisplayName } from "@ax/helpers";
4
+ import { SearchField, FloatingButton, NoteField } from "@ax/components";
5
+ import { ISchemaField } from "@ax/types";
6
+
7
+ import SideModalOption from "./SideModalOption";
8
+ import SectionOption from "./SectionOption";
9
+
10
+ import * as S from "./style";
11
+
12
+ const SideModal = (props: ISideModalProps): JSX.Element | null => {
13
+ const { onClick, theme, fieldArrays, selectedField, selectField } = props;
14
+
15
+ const isMultiArray = fieldArrays.length > 1;
16
+
17
+ const [isOpen, setIsOpen] = useState(true);
18
+ const [searchQuery, setSearchQuery] = useState("");
19
+
20
+ const handleClick = (option: string) => onClick(option);
21
+
22
+ const handleSectionClick = (option: ISchemaField) => selectField(option);
23
+ const handleBackClick = () => selectField(null);
24
+
25
+ const toggleOpen = () => {
26
+ setSearchQuery("");
27
+ setIsOpen((state) => !state);
28
+ };
29
+
30
+ const filteredOptions = selectedField?.whiteList?.map((option: string, i: number) => {
31
+ const displayName = getDisplayName(option);
32
+ if (searchQuery.length > 0) {
33
+ const name = displayName.toLowerCase();
34
+ const search = searchQuery.toLocaleLowerCase();
35
+ if (!name.includes(search)) return <></>;
36
+ }
37
+ return (
38
+ <SideModalOption option={option} handleClick={handleClick} key={`${option}${i}`} theme={theme}>
39
+ {displayName}
40
+ </SideModalOption>
41
+ );
42
+ }) || <></>;
43
+
44
+ const sectionOptions = fieldArrays.map((option) => (
45
+ <SectionOption option={option} handleClick={handleSectionClick} key={option.key} />
46
+ ));
47
+
48
+ const noteText = {
49
+ text: (
50
+ <>
51
+ Select <strong>which section</strong> you want to add the fields to.
52
+ </>
53
+ ),
54
+ };
55
+
56
+ return (
57
+ <>
58
+ <S.Wrapper isOpen={isOpen} data-testid="side-modal" className="form-fields">
59
+ {isMultiArray && !selectedField ? (
60
+ <>
61
+ <S.Header>
62
+ <NoteField value={noteText} />
63
+ </S.Header>
64
+ <S.ColumnsWrapper>
65
+ <S.FloatingButtonWrapper>
66
+ {isOpen && <FloatingButton icon="leftArrow" size="S" onClick={toggleOpen} />}
67
+ </S.FloatingButtonWrapper>
68
+ <S.Content>{sectionOptions}</S.Content>
69
+ </S.ColumnsWrapper>
70
+ </>
71
+ ) : (
72
+ <>
73
+ <S.Header>
74
+ {isMultiArray && selectedField && (
75
+ <S.BreadCrumb>
76
+ <S.BreadCrumbItem isLastItem={false} onClick={handleBackClick}>
77
+ Back
78
+ </S.BreadCrumbItem>
79
+ /<S.BreadCrumbItem isLastItem={true}>{selectedField.title}</S.BreadCrumbItem>
80
+ </S.BreadCrumb>
81
+ )}
82
+ <S.SearchWrapper>
83
+ <SearchField
84
+ onChange={setSearchQuery}
85
+ value={searchQuery}
86
+ closeOnInactive={false}
87
+ small={true}
88
+ placeholder="Search field"
89
+ />
90
+ </S.SearchWrapper>
91
+ </S.Header>
92
+ <S.ColumnsWrapper>
93
+ <S.FloatingButtonWrapper>
94
+ {isOpen && <FloatingButton icon="leftArrow" size="S" onClick={toggleOpen} />}
95
+ </S.FloatingButtonWrapper>
96
+ <S.Content>{filteredOptions}</S.Content>
97
+ </S.ColumnsWrapper>
98
+ </>
99
+ )}
100
+ </S.Wrapper>
101
+ <S.ClosedWrapper isOpen={!isOpen}>
102
+ <S.ClosedFloatingButtonWrapper>
103
+ {!isOpen && <FloatingButton text="fields" icon="rightArrow" size="S" onClick={toggleOpen} />}
104
+ </S.ClosedFloatingButtonWrapper>
105
+ </S.ClosedWrapper>
106
+ </>
107
+ );
108
+ };
109
+
110
+ export interface ISideModalProps {
111
+ onClick: (option: string) => void;
112
+ theme: string;
113
+ fieldArrays: ISchemaField[];
114
+ selectedField: ISchemaField | null;
115
+ selectField: (field: ISchemaField | null) => void;
116
+ }
117
+
118
+ export default SideModal;
@@ -0,0 +1,116 @@
1
+ import styled from "styled-components";
2
+
3
+ const Header = styled.div`
4
+ display: flex;
5
+ flex-direction: column;
6
+ width: 100%;
7
+ padding: ${(p) => `${p.theme.spacing.s} ${p.theme.spacing.s} 0`};
8
+ `;
9
+
10
+ const Content = styled.div`
11
+ list-style: none;
12
+ padding: ${(p) => p.theme.spacing.s};
13
+ height: ${(p) => `calc(100vh - ${p.theme.spacing.xl})`};
14
+ width: ${(p) => `calc(${p.theme.spacing.xl} * 3)`};
15
+ overflow: auto;
16
+ border-right: 1px solid ${(p) => p.theme.colors.uiLine};
17
+ &:last-child {
18
+ border-right: 0;
19
+ width: ${(p) => `calc(${p.theme.spacing.xl} * 4)`};
20
+ }
21
+ `;
22
+
23
+ const Wrapper = styled.div<{ isOpen: boolean }>`
24
+ position: relative;
25
+ height: 100%;
26
+ background: ${(p) => p.theme.colors.uiBackground01};
27
+ box-shadow: ${(p) => p.theme.shadow.leftPanel};
28
+ margin-left: ${(p) => (p.isOpen ? "0" : "-256px")};
29
+ opacity: ${(p) => (p.isOpen ? "1" : "0")};
30
+ z-index: 1000;
31
+ width: 256px;
32
+ flex-shrink: 0;
33
+ transition: ${(p) =>
34
+ !p.isOpen
35
+ ? "margin-left 0.5s ease-in-out 0s, opacity 0.1s ease 0.5s"
36
+ : "opacity 0.1s ease, margin-left 0.5s ease-in-out"};
37
+ `;
38
+
39
+ const ColumnsWrapper = styled.div`
40
+ display: flex;
41
+ `;
42
+
43
+ const SearchWrapper = styled.div`
44
+ width: 100%;
45
+ `;
46
+
47
+ const FloatingButtonWrapper = styled.div`
48
+ position: absolute;
49
+ right: -16px;
50
+ top: 60px;
51
+ `;
52
+
53
+ const ClosedWrapper = styled.div<{ isOpen: boolean }>`
54
+ position: relative;
55
+ background-color: ${(p) => p.theme.colors.uiBackground02};
56
+ width: ${(p) => p.theme.spacing.xs};
57
+ border-right: ${(p) => `2px solid ${p.theme.colors.interactive01}`};
58
+ height: 100vh;
59
+ margin-left: ${(p) => (p.isOpen ? "0" : "-100px")};
60
+ display: ${(p) => (p.isOpen ? "flex" : "none")};
61
+ transition: display 0.1s ease 1s;
62
+ `;
63
+
64
+ const ClosedFloatingButtonWrapper = styled.div`
65
+ position: absolute;
66
+ right: -77px;
67
+ top: 56px;
68
+ `;
69
+
70
+ const BreadCrumb = styled.div`
71
+ ${(p) => p.theme.textStyle.uiS};
72
+ color: ${(p) => p.theme.color.textMediumEmphasis};
73
+ margin-bottom: ${(p) => p.theme.spacing.s};
74
+ `;
75
+
76
+ const BreadCrumbItem = styled.button<{ isLastItem: boolean }>`
77
+ border: none;
78
+ ${(p) => p.theme.textStyle.uiS};
79
+ color: ${(p) => (p.isLastItem ? p.theme.color.textMediumEmphasis : p.theme.color.interactive01)};
80
+ background: transparent;
81
+ padding: 0 ${(p) => p.theme.spacing.xxs};
82
+ border-radius: ${(p) => p.theme.radii.s};
83
+ cursor: ${(p) => (p.isLastItem ? "default" : "pointer")};
84
+ border: 1px solid transparent;
85
+
86
+ &:active,
87
+ &:focus {
88
+ outline: none;
89
+ }
90
+
91
+ &:focus {
92
+ border: ${(p) => (p.isLastItem ? "none" : `1px solid ${p.theme.color.interactive01}`)};
93
+ background: ${(p) => (p.isLastItem ? "none" : p.theme.color.overlayFocusPrimary)};
94
+ }
95
+
96
+ &:active {
97
+ background: ${(p) => (p.isLastItem ? "none" : p.theme.color.overlayPressedPrimary)};
98
+ }
99
+
100
+ &:hover {
101
+ background: ${(p) => (p.isLastItem ? "none" : p.theme.color.overlayHoverPrimary)};
102
+ }
103
+ `;
104
+
105
+ export {
106
+ Wrapper,
107
+ Content,
108
+ Header,
109
+ ColumnsWrapper,
110
+ SearchWrapper,
111
+ FloatingButtonWrapper,
112
+ ClosedWrapper,
113
+ ClosedFloatingButtonWrapper,
114
+ BreadCrumb,
115
+ BreadCrumbItem,
116
+ };
@@ -1,10 +1,14 @@
1
- import React from "react";
1
+ import React, { useEffect, useState } from "react";
2
2
  import { connect } from "react-redux";
3
3
  import { ResizePanel } from "@ax/components";
4
- import { IBreadcrumbItem, IErrorItem, IModule, INotification, IRootState, ISchema } from "@ax/types";
4
+ import { IBreadcrumbItem, IErrorItem, IModule, INotification, IRootState, ISchema, ISchemaField } from "@ax/types";
5
5
  import { formsActions } from "@ax/containers/Forms";
6
+ import { getFormTemplate } from "@ax/helpers";
6
7
  import PageBrowser from "../PageBrowser";
7
8
  import FormConfigPanel from "./FormConfigPanel";
9
+ import SideModal from "./SideModal";
10
+
11
+ import * as S from "./style";
8
12
 
9
13
  const Editor = (props: IProps) => {
10
14
  const {
@@ -17,6 +21,7 @@ const Editor = (props: IProps) => {
17
21
  breadcrumb,
18
22
  selectedParent,
19
23
  errors,
24
+ template,
20
25
  setErrors,
21
26
  setNotification,
22
27
  setSelectedTab,
@@ -29,6 +34,35 @@ const Editor = (props: IProps) => {
29
34
  pasteModule,
30
35
  } = props;
31
36
 
37
+ const [selectedField, setSelectedField] = useState<ISchemaField | null>(null);
38
+ const [fieldArrays, setFieldArrays] = useState<ISchemaField[]>([]);
39
+
40
+ const hasFieldArrays = !!fieldArrays && fieldArrays.length > 0;
41
+ const isMultiArray = fieldArrays.length > 1;
42
+
43
+ useEffect(() => {
44
+ const getFormFieldArrays = (schema: ISchema | Record<string, never>) => {
45
+ let content: ISchemaField[] = [];
46
+ if (schema.component === "FormPage") {
47
+ const templateSchema = getFormTemplate(template);
48
+ content = templateSchema?.content || [];
49
+ } else {
50
+ content = schema.configTabs[0].fields;
51
+ }
52
+
53
+ return content.filter((field: ISchemaField) => field.type === "FormFieldArray");
54
+ };
55
+
56
+ const fields = getFormFieldArrays(schema);
57
+ const initState = fields.length === 1 ? fields[0] : null;
58
+ setFieldArrays(fields);
59
+ setSelectedField(initState);
60
+ }, [schema, template]);
61
+
62
+ const handleClick = (fieldType: string) => !!selectedField && addModule(fieldType, selectedField.key);
63
+
64
+ const handleSetSelectedField = (field: ISchemaField | null) => setSelectedField(field);
65
+
32
66
  const actions = {
33
67
  setNotificationAction: setNotification,
34
68
  addModuleAction: addModule,
@@ -37,11 +71,25 @@ const Editor = (props: IProps) => {
37
71
  moveModuleAction: moveModule,
38
72
  copyModuleAction: copyModule,
39
73
  pasteModuleAction: pasteModule,
74
+ setSelectedFormFieldAction: isMultiArray ? handleSetSelectedField : undefined,
40
75
  };
41
76
 
42
77
  return (
43
78
  <ResizePanel
44
- leftPanel={<PageBrowser isReadOnly={false} theme={theme} browserRef={browserRef} />}
79
+ leftPanel={
80
+ <S.EditorWrapper>
81
+ {hasFieldArrays && (
82
+ <SideModal
83
+ onClick={handleClick}
84
+ theme={theme}
85
+ fieldArrays={fieldArrays}
86
+ selectedField={selectedField}
87
+ selectField={handleSetSelectedField}
88
+ />
89
+ )}
90
+ <PageBrowser isReadOnly={false} theme={theme} browserRef={browserRef} />
91
+ </S.EditorWrapper>
92
+ }
45
93
  rightPanel={
46
94
  <FormConfigPanel
47
95
  schema={schema}
@@ -69,6 +117,7 @@ interface IEditorStateProps {
69
117
  schema: ISchema | Record<string, never>;
70
118
  breadcrumb: IBreadcrumbItem[];
71
119
  selectedParent: Record<string, unknown> | null;
120
+ template: string;
72
121
  }
73
122
 
74
123
  interface IPageBrowserDispatchProps {
@@ -96,6 +145,7 @@ const mapStateToProps = (state: IRootState): IEditorStateProps => ({
96
145
  schema: state.forms.schema,
97
146
  breadcrumb: state.forms.breadcrumb,
98
147
  selectedParent: state.forms.selectedParent,
148
+ template: state.forms.template,
99
149
  });
100
150
 
101
151
  const mapDispatchToProps = {
@@ -0,0 +1,9 @@
1
+ import styled from "styled-components";
2
+
3
+ const EditorWrapper = styled.div`
4
+ display: flex;
5
+ height: 100%;
6
+ width: 100%;
7
+ `;
8
+
9
+ export { EditorWrapper };
@@ -3,7 +3,6 @@ import styled from "styled-components";
3
3
  const Wrapper = styled.div`
4
4
  width: 100%;
5
5
  height: 100%;
6
- padding-left: ${(p) => p.theme.spacing.s};
7
6
  `;
8
7
 
9
8
  export { Wrapper };
@@ -75,6 +75,7 @@ const MediaGallery = (props: IProps) => {
75
75
  displayMode,
76
76
  selectedTab,
77
77
  foldersTree,
78
+ isUploading,
78
79
  } = props;
79
80
 
80
81
  const initState: IImageFolder = { images: { items: [], totalItems: 0, currentPage: 1 }, folders: [] };
@@ -271,6 +272,7 @@ const MediaGallery = (props: IProps) => {
271
272
 
272
273
  const handleBackClick = () => {
273
274
  const parentID = breadcrumb.length >= 2 ? breadcrumb[breadcrumb.length - 2].id : null;
275
+ resetBulkSelection();
274
276
  setPage(firstPage);
275
277
  updateCurrentFolder(parentID);
276
278
  };
@@ -440,6 +442,7 @@ const MediaGallery = (props: IProps) => {
440
442
 
441
443
  const handleUpdateCurrentFolder = (folderID: number | null) => {
442
444
  setSearchQuery("");
445
+ resetBulkSelection();
443
446
  setPage(firstPage);
444
447
  updateCurrentFolder(folderID);
445
448
  };
@@ -585,6 +588,7 @@ const MediaGallery = (props: IProps) => {
585
588
  ? {
586
589
  label: "Upload",
587
590
  action: () => toggleUploadModal(),
591
+ disabled: isUploading,
588
592
  }
589
593
  : undefined;
590
594
 
@@ -862,6 +866,7 @@ interface IProps {
862
866
  displayMode: "grid" | "list";
863
867
  selectedTab: "site" | "global";
864
868
  foldersTree: IFolderTree[];
869
+ isUploading: boolean;
865
870
  getFolderContent(params: IGetFolderParams): Promise<void>;
866
871
  getFoldersTree(siteID: number | "global"): Promise<void>;
867
872
  updateCurrentFolder(folderID: number | null): void;
@@ -926,6 +931,7 @@ const mapStateToProps = (state: IRootState) => ({
926
931
  displayMode: state.gallery.displayMode,
927
932
  selectedTab: state.gallery.selectedTab,
928
933
  foldersTree: state.gallery.foldersTree,
934
+ isUploading: state.gallery.isUploading,
929
935
  });
930
936
 
931
937
  export default connect(mapStateToProps, mapDispatchToProps)(MediaGallery);
@@ -191,6 +191,7 @@ export interface ISchema {
191
191
  category?: string;
192
192
  styles?: Record<string, string>;
193
193
  maxModulesPerPage?: number;
194
+ content?: ISchemaField[];
194
195
  [key: string]: any;
195
196
  default?: {
196
197
  title?: string | { content: string };