@griddo/ax 10.3.7 → 10.3.8

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@griddo/ax",
3
3
  "description": "Griddo Author Experience",
4
- "version": "10.3.7",
4
+ "version": "10.3.8",
5
5
  "authors": [
6
6
  "Álvaro Sánchez' <alvaro.sanches@secuoyas.com>",
7
7
  "Carlos Torres <carlos.torres@secuoyas.com>",
@@ -230,5 +230,5 @@
230
230
  "publishConfig": {
231
231
  "access": "public"
232
232
  },
233
- "gitHead": "5052758b2c3b8837eb2cc70506ed58026254ae18"
233
+ "gitHead": "7e355ae2bd1b87bc210353cb2c891bd6aecb123c"
234
234
  }
@@ -26,8 +26,8 @@ afterEach(() => {
26
26
  });
27
27
 
28
28
  const defaultProps = mock<IReferenceFieldProps>();
29
- defaultProps.value = { source: ["ARTICLES"] };
30
- defaultProps.source = ["ARTICLES"];
29
+ defaultProps.value = { sources: [{ structuredData: "ARTICLES" }] };
30
+ defaultProps.sources = [{ structuredData: "ARTICLES" }];
31
31
 
32
32
  const initialStore = {
33
33
  structuredData: structuredDataMock,
@@ -134,7 +134,7 @@ describe("ReferenceField component rendering", () => {
134
134
  });
135
135
 
136
136
  test("should render the component ReferenceField with fixed is empty", async () => {
137
- defaultProps.source = ["ARTICLES"];
137
+ defaultProps.sources = [{ structuredData: "ARTICLES" }];
138
138
  defaultProps.value = {
139
139
  mode: "manual",
140
140
  fixed: [],
@@ -175,19 +175,23 @@ describe("ReferenceField component rendering", () => {
175
175
 
176
176
  describe("onChange events", () => {
177
177
  test("should apply change in AutoPanel", async () => {
178
- defaultProps.source = ["STORIES"];
178
+ defaultProps.sources = [{ structuredData: "STORIES" }];
179
179
  defaultProps.value = {
180
- source: ["STORIES"],
181
- filter: [
182
- {
183
- id: 4418,
184
- label: "Story Type 1",
185
- source: "STORIES",
186
- },
180
+ sources: [
187
181
  {
188
- id: 4419,
189
- label: "Story Type 2",
190
- source: "STORIES",
182
+ structuredData: "STORIES",
183
+ filters: [
184
+ {
185
+ id: 4418,
186
+ label: "Story Type 1",
187
+ },
188
+ {
189
+ id: 4419,
190
+ label: "Story Type 2",
191
+ },
192
+ ],
193
+ globalOperator: "AND",
194
+ filterOperator: "ANY",
191
195
  },
192
196
  ],
193
197
  quantity: 1,
@@ -257,7 +261,7 @@ describe("onChange events", () => {
257
261
  });
258
262
 
259
263
  test("should add item in manual mode", async () => {
260
- defaultProps.source = ["ARTICLES"];
264
+ defaultProps.sources = [{ structuredData: "ARTICLES" }];
261
265
  defaultProps.value = {
262
266
  mode: "manual",
263
267
  fixed: [4492, 4493],
@@ -309,7 +313,7 @@ describe("onChange events", () => {
309
313
  });
310
314
 
311
315
  test("should remove item in manual mode", async () => {
312
- defaultProps.source = ["ARTICLES"];
316
+ defaultProps.sources = [{ structuredData: "ARTICLES" }];
313
317
  defaultProps.value = {
314
318
  mode: "manual",
315
319
  fixed: [4492, 4493],
@@ -363,19 +367,23 @@ describe("onChange events", () => {
363
367
  });
364
368
 
365
369
  test("should delete filter tags", async () => {
366
- defaultProps.source = ["STORIES"];
370
+ defaultProps.sources = [{ structuredData: "STORIES" }];
367
371
  defaultProps.value = {
368
- source: ["STORIES"],
369
- filter: [
370
- {
371
- id: 4418,
372
- label: "Story Type 1",
373
- source: "STORIES",
374
- },
372
+ sources: [
375
373
  {
376
- id: 4419,
377
- label: "Story Type 2",
378
- source: "STORIES",
374
+ structuredData: "STORIES",
375
+ filters: [
376
+ {
377
+ id: 4418,
378
+ label: "Story Type 1",
379
+ },
380
+ {
381
+ id: 4419,
382
+ label: "Story Type 2",
383
+ },
384
+ ],
385
+ globalOperator: "AND",
386
+ filterOperator: "ANY",
379
387
  },
380
388
  ],
381
389
  };
@@ -8,6 +8,7 @@ enum buttonStyles {
8
8
  TEXT = "text",
9
9
  LINE = "line",
10
10
  INVERSE = "lineInverse",
11
+ MINIMAL = "minimal",
11
12
  }
12
13
 
13
14
  const Button = (props: IButtonProps): JSX.Element => {
@@ -60,6 +61,18 @@ const Button = (props: IButtonProps): JSX.Element => {
60
61
  {buttonContent}
61
62
  </S.LineButton>
62
63
  );
64
+ case buttonStyles.MINIMAL:
65
+ return (
66
+ <S.MinimalButton
67
+ data-testid="button-minimal"
68
+ className={className}
69
+ type={type}
70
+ disabled={disabled}
71
+ onClick={handleOnClick}
72
+ >
73
+ {buttonContent}
74
+ </S.MinimalButton>
75
+ );
63
76
  default:
64
77
  return (
65
78
  <S.Button
@@ -77,12 +90,12 @@ const Button = (props: IButtonProps): JSX.Element => {
77
90
 
78
91
  export interface IButtonProps {
79
92
  children: JSX.Element | string;
80
- type: "button" | "submit" | "reset" | undefined;
93
+ type?: "button" | "submit" | "reset";
81
94
  onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
82
95
  icon?: string;
83
96
  loader?: string;
84
97
  disabled?: boolean;
85
- buttonStyle?: "solid" | "text" | "line" | "lineInverse" | undefined; // TODO: investigar si podemos hacer esto más elegante y aprovechar el enum buttonStyles
98
+ buttonStyle?: "solid" | "text" | "line" | "lineInverse" | "minimal";
86
99
  className?: string;
87
100
  backIcon?: string;
88
101
  }
@@ -1,7 +1,7 @@
1
1
  import React from "react";
2
2
  import styled from "styled-components";
3
3
 
4
- export const Button = styled.button`
4
+ const Button = styled.button`
5
5
  ${(p) => p.theme.textStyle.uiButton};
6
6
  background-color: ${(p) => p.theme.color.interactive01};
7
7
  border-radius: 4px;
@@ -63,7 +63,7 @@ export const Button = styled.button`
63
63
  }
64
64
  `;
65
65
 
66
- export const TextButton = styled((props) => <Button {...props} />)`
66
+ const TextButton = styled((props) => <Button {...props} />)`
67
67
  background-color: transparent;
68
68
  color: ${(p) => p.theme.color.interactive01};
69
69
  padding: 0 ${(p) => p.theme.spacing.xs};
@@ -92,7 +92,7 @@ export const TextButton = styled((props) => <Button {...props} />)`
92
92
  }
93
93
  `;
94
94
 
95
- export const LineButton = styled((props) => <Button {...props} />)<{ isInverse: boolean | undefined }>`
95
+ const LineButton = styled((props) => <Button {...props} />)<{ isInverse: boolean | undefined }>`
96
96
  background-color: ${(p) => (p.isInverse ? p.theme.color.uiMainMenuBackground : p.theme.color.interactiveBackground)};
97
97
  border: 1px solid ${(p) => (p.isInverse ? p.theme.color.interactiveInverse : p.theme.color.interactive01)};
98
98
  color: ${(p) => (p.isInverse ? p.theme.color.interactiveInverse : p.theme.color.interactive01)};
@@ -123,7 +123,15 @@ export const LineButton = styled((props) => <Button {...props} />)<{ isInverse:
123
123
  }
124
124
  `;
125
125
 
126
- export const Label = styled.span<{ icon?: string; backIcon?: string }>`
126
+ const MinimalButton = styled.button`
127
+ ${(p) => p.theme.textStyle.uiButton};
128
+ background-color: transparent;
129
+ color: ${(p) => p.theme.color.interactive01};
130
+ border: none;
131
+ cursor: pointer;
132
+ `;
133
+
134
+ const Label = styled.span<{ icon?: string; backIcon?: string }>`
127
135
  outline: none;
128
136
  position: relative;
129
137
  z-index: 1;
@@ -133,3 +141,5 @@ export const Label = styled.span<{ icon?: string; backIcon?: string }>`
133
141
  }
134
142
  padding-right: ${(p) => (p.backIcon ? p.theme.spacing.s : `0`)};
135
143
  `;
144
+
145
+ export { Button, TextButton, LineButton, MinimalButton, Label }
@@ -1,64 +1,58 @@
1
- import React, { useState } from "react";
1
+ import React, { useEffect, useState } from "react";
2
2
 
3
- import { structuredData } from "@ax/api";
3
+ import { structuredData, selects } from "@ax/api";
4
4
  import { isReqOk } from "@ax/helpers";
5
5
  import { IGetStructuredDataParams, IDataSource, ISite, IStructuredData } from "@ax/types";
6
- import { Button, CheckField, FieldsBehavior, FloatingMenu, Icon, IconAction, SearchField, Tag } from "@ax/components";
7
- import { IFilter } from "../../Context";
6
+ import { Button, CheckField, FieldsBehavior, IconAction, Select, SearchField, Tag, Tooltip } from "@ax/components";
7
+ import { useCategoryColors } from "@ax/hooks";
8
+
9
+ import { ISource, IFilter } from "../../Context";
8
10
 
9
11
  import * as S from "./style";
10
12
 
11
- const AutoItem = (props: IProps) => {
12
- const { source, canDelete, filter, handleDelete, addFilter, currentSite, structuredDataSite, siteID } = props;
13
+ const AutoItem = (props: IProps): JSX.Element => {
14
+ const {
15
+ source,
16
+ canDelete,
17
+ filter,
18
+ handleDelete,
19
+ addFilter,
20
+ currentSite,
21
+ structuredDataSite,
22
+ filterOperator,
23
+ globalOperator,
24
+ siteID,
25
+ } = props;
13
26
 
14
27
  const initState: IState = {
15
28
  selected: "",
16
29
  options: [],
17
- filter,
18
30
  isOpen: false,
31
+ isAdvancedOpen: false,
32
+ categories: [],
33
+ selectedCategories: [],
19
34
  filteredOptions: [],
20
35
  };
21
36
 
22
37
  const [state, setState] = useState(initState);
23
38
 
24
- const removeItem = (item: string) => {
25
- handleDelete(item);
26
- };
39
+ const catIDs = state.categories.map((cat: ICategory) => cat.label);
40
+ const { categoryColors } = useCategoryColors(catIDs);
27
41
 
28
- const MoreInfoButton = () => (
29
- <S.IconActionWrapper>
30
- <IconAction icon="more" />
31
- </S.IconActionWrapper>
32
- );
42
+ useEffect(() => {
43
+ getCategories();
44
+ // eslint-disable-next-line react-hooks/exhaustive-deps
45
+ }, []);
33
46
 
34
- const actionMenuItem = (item: any, source: string) => {
35
- const handleClick = () => item.action(source);
36
- return (
37
- <S.ActionItem key={item.icon} onClick={handleClick} data-testid="auto-item-action-item">
38
- <Icon name={item.icon} />
39
- <S.ActionText>{item.label}</S.ActionText>
40
- </S.ActionItem>
41
- );
42
- };
47
+ const removeItem = () => handleDelete(source.id);
43
48
 
44
- const editMenu = {
45
- options: [
46
- {
47
- label: "delete",
48
- icon: "delete",
49
- action: removeItem,
50
- },
51
- ],
52
- };
53
-
54
- const ActionMenu = (props: any) => {
55
- const { source } = props;
56
- return (
57
- <FloatingMenu Button={MoreInfoButton}>
58
- <S.ActionMenu>{editMenu.options.map((item: any, i: number) => actionMenuItem(item, source))}</S.ActionMenu>
59
- </FloatingMenu>
60
- );
61
- };
49
+ const menuOptions = [
50
+ {
51
+ label: "delete",
52
+ icon: "delete",
53
+ action: removeItem,
54
+ },
55
+ ];
62
56
 
63
57
  const getIsGlobal = (id: string): boolean => {
64
58
  const data = structuredDataSite && structuredDataSite.find((elem: any) => elem.id === id);
@@ -89,6 +83,32 @@ const AutoItem = (props: IProps) => {
89
83
  return false;
90
84
  };
91
85
 
86
+ const getCategories = async (): Promise<boolean> => {
87
+ try {
88
+ const result = await selects.getSelectItems("categories", source.id);
89
+ if (isReqOk(result.status)) {
90
+ const selectedCategories = getFiltersCategories(filter, result.data);
91
+ setState({ ...state, categories: result.data, selectedCategories });
92
+ return true;
93
+ }
94
+ } catch (e) {
95
+ console.log(e);
96
+ }
97
+ return false;
98
+ };
99
+
100
+ const getFiltersCategories = (filters: any[], categories: ICategory[]) => {
101
+ let filterCategories: ICategory[] = [];
102
+ filters.forEach((filter: any) => {
103
+ const filterCategory = categories.find((cat: ICategory) => cat.label === filter.category);
104
+ filterCategories =
105
+ filterCategory && !filterCategories.includes(filterCategory)
106
+ ? [...filterCategories, filterCategory]
107
+ : filterCategories;
108
+ });
109
+ return filterCategories;
110
+ };
111
+
92
112
  const handleSelectOnChange = (newValue: string | null) => {
93
113
  if (newValue) {
94
114
  getCategoryContent(newValue);
@@ -99,35 +119,92 @@ const AutoItem = (props: IProps) => {
99
119
 
100
120
  const handleCheckChange = (checkValue: any) => {
101
121
  if (checkValue.isChecked) {
102
- const newFilter: any = state.options.filter((f: any) => checkValue.value === f.id);
103
- setState({
104
- ...state,
105
- filter: [...state.filter, { id: newFilter[0].id, label: newFilter[0].content.title, source: source.id }],
106
- });
122
+ const newFilter: any = state.options.find((f: any) => checkValue.value === f.id);
123
+ if (newFilter) {
124
+ const filterCategory = state.categories.find((cat: ICategory) => cat.value === newFilter.structuredData);
125
+ const selectedCategories =
126
+ filterCategory && !state.selectedCategories.includes(filterCategory)
127
+ ? [...state.selectedCategories, filterCategory]
128
+ : state.selectedCategories;
129
+
130
+ setState({
131
+ ...state,
132
+ selectedCategories,
133
+ });
134
+
135
+ const updatedSource: ISource = {
136
+ structuredData: source.id,
137
+ filters: [
138
+ ...filter,
139
+ {
140
+ id: newFilter.id,
141
+ label: newFilter.content.title,
142
+ category: filterCategory?.label || "",
143
+ color: categoryColors[filterCategory?.label || ""],
144
+ },
145
+ ],
146
+ globalOperator: globalOperator,
147
+ filterOperator: filterOperator,
148
+ };
149
+
150
+ addFilter(updatedSource);
151
+ }
107
152
  } else {
108
- const newFilters = state.filter.filter((e) => e.id !== checkValue.value);
109
- setState({ ...state, filter: newFilters });
110
- }
111
- };
153
+ const newFilters = filter.filter((e) => e.id !== checkValue.value);
154
+ const selectedCategories = getFiltersCategories(newFilters, state.categories);
155
+ setState({ ...state, selectedCategories });
112
156
 
113
- const handleButtonClick = () => {
114
- addFilter(state.filter, source.id);
115
- setState({ ...state, isOpen: false });
157
+ const updatedSource: ISource = {
158
+ structuredData: source.id,
159
+ filters: newFilters,
160
+ globalOperator: globalOperator,
161
+ filterOperator: filterOperator,
162
+ };
163
+
164
+ addFilter(updatedSource);
165
+ }
116
166
  };
117
167
 
118
168
  const isChecked = (id: number) => {
119
- const ids = state.filter.map((a) => a.id);
169
+ const ids = filter.map((a) => a.id);
120
170
  return ids.includes(id);
121
171
  };
122
172
 
123
- const toggleFilters = () => {
124
- setState({ ...state, isOpen: !state.isOpen });
125
- };
173
+ const toggleFilters = () => setState({ ...state, isOpen: !state.isOpen });
174
+
175
+ const toggleAdvanced = () => setState({ ...state, isAdvancedOpen: !state.isAdvancedOpen });
126
176
 
127
177
  const handleDeleteTag = (id: number) => {
128
- const newFilters = state.filter.filter((e) => e.id !== id);
129
- setState({ ...state, filter: newFilters });
130
- addFilter(newFilters, source.id);
178
+ const newFilters = filter.filter((e) => e.id !== id);
179
+ const selectedCategories = getFiltersCategories(newFilters, state.categories);
180
+ setState({ ...state, selectedCategories });
181
+ const updatedSource: ISource = {
182
+ structuredData: source.id,
183
+ filters: newFilters,
184
+ globalOperator: globalOperator,
185
+ filterOperator: filterOperator,
186
+ };
187
+ addFilter(updatedSource);
188
+ };
189
+
190
+ const handleFilterOperator = (value: string) => {
191
+ const updatedSource: ISource = {
192
+ structuredData: source.id,
193
+ filters: filter,
194
+ globalOperator: globalOperator,
195
+ filterOperator: value,
196
+ };
197
+ addFilter(updatedSource);
198
+ };
199
+
200
+ const handleGlobalOperator = (value: string) => {
201
+ const updatedSource: ISource = {
202
+ structuredData: source.id,
203
+ filters: filter,
204
+ globalOperator: value,
205
+ filterOperator: filterOperator,
206
+ };
207
+ addFilter(updatedSource);
131
208
  };
132
209
 
133
210
  const handleSearchChange = (query: string) => {
@@ -137,33 +214,104 @@ const AutoItem = (props: IProps) => {
137
214
  setState({ ...state, filteredOptions });
138
215
  };
139
216
 
217
+ const handleClearFilters = () => {
218
+ setState({ ...state, selectedCategories: [] });
219
+ const updatedSource: ISource = {
220
+ structuredData: source.id,
221
+ filters: [],
222
+ };
223
+ addFilter(updatedSource);
224
+ };
225
+
226
+ const operatorOptions = [
227
+ { value: "OR", label: "OR" },
228
+ { value: "AND", label: "AND" },
229
+ ];
230
+
140
231
  return (
141
232
  <S.TypeContainer key={source.id}>
142
- <S.TypeWrapper>
233
+ <S.TypeWrapper data-testid="item-type-wrapper">
143
234
  <S.TypeLabel>Type</S.TypeLabel>
144
235
  <S.TypeName>{source.title}</S.TypeName>
145
- {canDelete && <ActionMenu source={source.id} />}
146
- <S.IconWrapper>
147
- <IconAction icon="filter" onClick={toggleFilters} />
148
- </S.IconWrapper>
236
+ <S.ActionsWrapper>
237
+ <S.IconWrapper>
238
+ <Tooltip content="Filters">
239
+ <IconAction icon="filter" onClick={toggleFilters} active={state.isOpen} />
240
+ </Tooltip>
241
+ </S.IconWrapper>
242
+ <S.IconWrapper>
243
+ <Tooltip content="Advanced settings">
244
+ <IconAction icon="settings" onClick={toggleAdvanced} active={state.isAdvancedOpen} />
245
+ </Tooltip>
246
+ </S.IconWrapper>
247
+ {canDelete && <S.StyledActionMenu icon="more" options={menuOptions} tooltip="Options" />}
248
+ </S.ActionsWrapper>
149
249
  </S.TypeWrapper>
150
- <S.FiltersWrapper>
250
+ <S.FiltersWrapper isActive={state.isAdvancedOpen} isEmpty={filter.length === 0}>
151
251
  {filter &&
152
- filter.map((e: any) => {
153
- const handleClick = () => {
154
- handleDeleteTag(e.id);
155
- };
156
- return <Tag key={e.label} text={e.label} onDeleteAction={handleClick} />;
252
+ filter.map((e: IFilter) => {
253
+ const handleClick = () => handleDeleteTag(e.id);
254
+ return (
255
+ <Tag
256
+ key={e.label}
257
+ text={`${e.category}: ${e.label}`}
258
+ onDeleteAction={handleClick}
259
+ color={e.color || categoryColors[e.category || ""]}
260
+ />
261
+ );
157
262
  })}
263
+ {state.isAdvancedOpen && (
264
+ <S.OptionsBlock>
265
+ Relation between filters{" "}
266
+ <S.SelectWrapper>
267
+ <Select
268
+ name="filterOperator"
269
+ options={operatorOptions}
270
+ onChange={handleFilterOperator}
271
+ value={filterOperator || "OR"}
272
+ type="inline"
273
+ mandatory={true}
274
+ />
275
+ </S.SelectWrapper>
276
+ </S.OptionsBlock>
277
+ )}
158
278
  </S.FiltersWrapper>
279
+ {state.isAdvancedOpen && (
280
+ <S.FiltersWrapper isActive={state.isAdvancedOpen} isEmpty={state.selectedCategories.length === 0}>
281
+ {state.selectedCategories.map((e: ICategory) => {
282
+ return <Tag key={e.label} text={e.label} color={categoryColors[e.label]} rounded={false} />;
283
+ })}
284
+ <S.OptionsBlock>
285
+ Relation between categories{" "}
286
+ <S.SelectWrapper>
287
+ <Select
288
+ name="globalOperator"
289
+ options={operatorOptions}
290
+ onChange={handleGlobalOperator}
291
+ value={globalOperator || "AND"}
292
+ type="inline"
293
+ mandatory={true}
294
+ />
295
+ </S.SelectWrapper>
296
+ </S.OptionsBlock>
297
+ </S.FiltersWrapper>
298
+ )}
299
+ {filter.length > 0 && (
300
+ <S.ClearWrapper>
301
+ <Button type="button" buttonStyle="minimal" onClick={handleClearFilters}>
302
+ Clear All Filters
303
+ </Button>
304
+ </S.ClearWrapper>
305
+ )}
159
306
  <S.ActionsFilterWrapper isOpen={state.isOpen}>
160
307
  <FieldsBehavior
161
308
  title="Create filter by"
162
309
  name="categorySelect"
163
- fieldType="AsyncSelect"
310
+ fieldType="Select"
164
311
  value={state.selected}
165
- entity="categories"
166
312
  onChange={handleSelectOnChange}
313
+ options={state.categories}
314
+ placeholder="Select..."
167
315
  entityId={source.id}
168
316
  site={currentSite}
169
317
  />
@@ -185,13 +333,6 @@ const AutoItem = (props: IProps) => {
185
333
  />
186
334
  ))}
187
335
  </S.ChecksWrapper>
188
- {state.options.length > 0 && (
189
- <S.ButtonWrapper>
190
- <Button type="button" buttonStyle="line" onClick={handleButtonClick}>
191
- Add filter
192
- </Button>
193
- </S.ButtonWrapper>
194
- )}
195
336
  </S.ActionsFilterWrapper>
196
337
  </S.TypeContainer>
197
338
  );
@@ -200,19 +341,28 @@ const AutoItem = (props: IProps) => {
200
341
  interface IState {
201
342
  selected: string;
202
343
  options: any[];
203
- filter: any[];
204
344
  isOpen: boolean;
345
+ isAdvancedOpen: boolean;
346
+ categories: ICategory[];
347
+ selectedCategories: ICategory[];
205
348
  filteredOptions: any[];
206
349
  }
207
350
 
351
+ interface ICategory {
352
+ value: string;
353
+ label: string;
354
+ }
355
+
208
356
  interface IProps {
209
357
  source: IDataSource;
210
358
  canDelete: boolean;
211
359
  filter: IFilter[];
212
360
  currentSite: ISite | null;
213
361
  structuredDataSite: IStructuredData[];
362
+ filterOperator?: string;
363
+ globalOperator?: string;
214
364
  handleDelete: (value: string) => void;
215
- addFilter: (value: any[], source: string) => void;
365
+ addFilter: (source: ISource) => void;
216
366
  siteID?: number | null;
217
367
  }
218
368