@griddo/ax 12.10.5 → 12.11.1-rc.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.
Files changed (33) hide show
  1. package/README.md +0 -2
  2. package/config/webpack.config.js +2 -1
  3. package/config/webpackSchemas.config.js +7 -4
  4. package/package.json +31 -56
  5. package/src/__tests__/components/Fields/ConditionalField/ConditionalField.test.tsx +17 -0
  6. package/src/__tests__/components/Fields/RadioField/RadioField.test.tsx +66 -0
  7. package/src/__tests__/components/Fields/RadioGroup/RadioGroup.test.tsx +76 -0
  8. package/src/__tests__/modules/Redirects/BulkHeader/TableHeader.test.tsx +123 -0
  9. package/src/__tests__/modules/Redirects/RedirectItem/RedirectItem.test.tsx +163 -0
  10. package/src/__tests__/modules/Redirects/RedirectPanel/RedirectPanel.test.tsx +134 -0
  11. package/src/__tests__/modules/Redirects/Redirects/Redirects.create.test.tsx +69 -0
  12. package/src/__tests__/modules/Redirects/Redirects/Redirects.edit.test.tsx +91 -0
  13. package/src/__tests__/modules/Redirects/Redirects/Redirects.filter.test.tsx +131 -0
  14. package/src/components/Fields/ConditionalField/style.tsx +1 -1
  15. package/src/components/Fields/RadioField/index.tsx +20 -3
  16. package/src/components/Fields/RadioField/style.tsx +18 -1
  17. package/src/components/Fields/RadioGroup/index.tsx +1 -0
  18. package/src/containers/Redirects/actions.tsx +15 -3
  19. package/src/global.d.ts +0 -1
  20. package/src/modules/ActivityLog/DownloadModal/index.tsx +2 -1
  21. package/src/modules/ActivityLog/index.tsx +2 -2
  22. package/src/modules/Redirects/BulkHeader/TableHeader/index.tsx +5 -0
  23. package/src/modules/Redirects/BulkHeader/TableHeader/style.tsx +5 -1
  24. package/src/modules/Redirects/RedirectItem/index.tsx +11 -1
  25. package/src/modules/Redirects/RedirectItem/style.tsx +29 -1
  26. package/src/modules/Redirects/RedirectPanel/index.tsx +40 -3
  27. package/src/modules/Redirects/TypeFilter/index.tsx +44 -0
  28. package/src/modules/Redirects/TypeFilter/style.tsx +27 -0
  29. package/src/modules/Redirects/hooks.tsx +12 -4
  30. package/src/modules/Redirects/index.tsx +22 -14
  31. package/src/modules/Redirects/utils.tsx +10 -1
  32. package/src/types/index.tsx +3 -0
  33. package/tsconfig.json +1 -9
@@ -28,6 +28,7 @@ const RadioGroup = (props: IRadioGroupProps): JSX.Element => {
28
28
  checked={checked}
29
29
  value={item.value}
30
30
  title={item.title}
31
+ description={item.description}
31
32
  name={name}
32
33
  error={item.error}
33
34
  disabled={item.disabled || disabled}
@@ -1,6 +1,8 @@
1
1
  import { redirects } from "@ax/api";
2
2
  import { appActions } from "@ax/containers/App";
3
3
  import { handleRequest } from "@ax/helpers";
4
+ import type { RedirectType } from "@ax/types";
5
+
4
6
  import {
5
7
  DEFAULT_PARAMS,
6
8
  ERROR_CODE,
@@ -11,8 +13,18 @@ import {
11
13
  } from "./constants";
12
14
  import type { ISetImports, ISetRedirects, ISetTotalImports, ISetTotalItems } from "./interfaces";
13
15
 
16
+ // El contrato del tipo de redirección con la API no está cerrado (sc-62031) y
17
+ // un código HTTP en BD es naturalmente un entero. `RadioGroup` compara con
18
+ // `===`, así que un 301 numérico no casaría con la opción "301" y el panel se
19
+ // abriría sin nada marcado. Se normaliza aquí, en la única puerta de entrada de
20
+ // las redirecciones al store, para que listado y panel vean siempre lo mismo.
21
+ const normalizeRedirectType = (redirect: any) =>
22
+ redirect?.type === null || redirect?.type === undefined || redirect?.type === ""
23
+ ? redirect
24
+ : { ...redirect, type: String(redirect.type) };
25
+
14
26
  function setRedirects(redirects: any[]): ISetRedirects {
15
- return { type: SET_REDIRECTS, payload: { redirects } };
27
+ return { type: SET_REDIRECTS, payload: { redirects: redirects.map(normalizeRedirectType) } };
16
28
  }
17
29
 
18
30
  function setTotalItems(totalItems: number): ISetTotalItems {
@@ -50,7 +62,7 @@ function getRedirects(params: any, filters?: string): (dispatch: any) => Promise
50
62
  }
51
63
 
52
64
  function addRedirect(
53
- redirect: { from: string; to: string | number },
65
+ redirect: { from: string; to: string | number; type?: RedirectType },
54
66
  errorAction: () => void,
55
67
  force?: boolean,
56
68
  filter?: string,
@@ -84,7 +96,7 @@ function addRedirect(
84
96
  }
85
97
 
86
98
  function updateRedirect(
87
- redirect: { from: string; to: string | number },
99
+ redirect: { from: string; to: string | number; type?: RedirectType },
88
100
  redirectID: number,
89
101
  filter?: string,
90
102
  ): (dispatch: any, getState: any) => Promise<void> {
package/src/global.d.ts CHANGED
@@ -3,7 +3,6 @@ declare module "components";
3
3
  declare module "froala-editor";
4
4
  declare module "react-froala-wysiwyg";
5
5
  declare module "lodash.isequal";
6
- declare module "is-wsl";
7
6
  declare module "react-draft-wysiwyg";
8
7
  declare module "history";
9
8
  declare module "draftjs-to-html";
@@ -1,5 +1,6 @@
1
1
  import type React from "react";
2
2
 
3
+ import type { ExportFormat } from "@griddo/api-types";
3
4
  import type { IModal } from "@ax/types";
4
5
  import { FieldsBehavior, Modal } from "@ax/components";
5
6
  import type { IDownloadFormState } from "..";
@@ -14,7 +15,7 @@ const DownloadModal = (props: IDownloadModalProps) => {
14
15
  const setEndDate = (endDate: string) => setForm((state) => ({ ...state, endDate }));
15
16
  const setStartTime = (startTime: string) => setForm((state) => ({ ...state, startTime }));
16
17
  const setMaxEvents = (maxEvents: number) => setForm((state) => ({ ...state, maxEvents }));
17
- const setFormat = (format: string) => setForm((state) => ({ ...state, format }));
18
+ const setFormat = (format: ExportFormat) => setForm((state) => ({ ...state, format }));
18
19
 
19
20
  const secondaryModalAction = { title: "Cancel", onClick: toggleModal };
20
21
 
@@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useRef, useState } from "react";
2
2
  import { connect } from "react-redux";
3
3
  import { format, subDays } from "date-fns";
4
4
 
5
- import type { LogActivityExportRequest, LogActivityPaginationRequest } from "@griddo/api-types";
5
+ import type { ExportFormat, LogActivityExportRequest, LogActivityPaginationRequest } from "@griddo/api-types";
6
6
  import type { ILogItemsByDay, ILogItemsByUser, IQueryValue, IRootState, IStructuredData } from "@ax/types";
7
7
  import { EmptyState, ErrorToast, FilterTagsBar, MainWrapper, SearchTagsBar, Select, TableList } from "@ax/components";
8
8
  import { activityLogActions } from "@ax/containers/ActivityLog";
@@ -290,7 +290,7 @@ export interface IDownloadFormState {
290
290
  dateRange: string;
291
291
  startTime: string | null;
292
292
  maxEvents: number | null;
293
- format: string;
293
+ format: ExportFormat;
294
294
  startDate: string | null;
295
295
  endDate: string | null;
296
296
  }
@@ -3,6 +3,8 @@ import React from "react";
3
3
  import { CheckField, SiteFilter, TableCounter, DateFilter, Tooltip } from "@ax/components";
4
4
  import type { IQueryValue } from "@ax/types";
5
5
 
6
+ import TypeFilter from "../../TypeFilter";
7
+
6
8
  import * as S from "./style";
7
9
 
8
10
  const TableHeader = (props: IProps): JSX.Element => {
@@ -35,6 +37,9 @@ const TableHeader = (props: IProps): JSX.Element => {
35
37
  />
36
38
  </Tooltip>
37
39
  </S.CheckHeader>
40
+ <S.TypeHeader>
41
+ <TypeFilter filterItems={filterItems} value={filterValues.type} />
42
+ </S.TypeHeader>
38
43
  {!isSiteItem && (
39
44
  <S.SiteHeader>
40
45
  <SiteFilter filterItems={filterItems} value={filterValues} pointer="sites" center={false} />
@@ -16,6 +16,10 @@ const CheckHeader = styled(Header)`
16
16
  width: 32px;
17
17
  `;
18
18
 
19
+ const TypeHeader = styled(Header)`
20
+ width: 96px;
21
+ `;
22
+
19
23
  const SiteHeader = styled(Header)`
20
24
  width: 200px;
21
25
  `;
@@ -30,4 +34,4 @@ const ActionsHeader = styled(Header)`
30
34
  justify-content: flex-end;
31
35
  `;
32
36
 
33
- export { TableHeader, CheckHeader, SiteHeader, UrlHeader, ActionsHeader };
37
+ export { TableHeader, CheckHeader, TypeHeader, SiteHeader, UrlHeader, ActionsHeader };
@@ -10,6 +10,7 @@ import { format } from "date-fns";
10
10
 
11
11
  import { DeleteModal } from "../atoms";
12
12
  import RedirectPanel from "../RedirectPanel";
13
+ import { DEFAULT_REDIRECT_TYPE, REDIRECT_TYPE_COLORS } from "../utils";
13
14
 
14
15
  import * as S from "./style";
15
16
 
@@ -36,7 +37,11 @@ const RedirectItem = (props: IRedirectItemProps): JSX.Element => {
36
37
  const toggleSecondaryPanel = () => setIsOpenedSecond(!isOpenedSecond);
37
38
 
38
39
  const handleClick = () => {
39
- const mappedRedirect = { ...redirect, to: { ...redirect.to, linkTo: redirect.to.pageId, href: redirect.to.url } };
40
+ const mappedRedirect = {
41
+ ...redirect,
42
+ type: redirect.type ?? DEFAULT_REDIRECT_TYPE,
43
+ to: { ...redirect.to, linkTo: redirect.to.pageId, href: redirect.to.url },
44
+ };
40
45
  setFormValues(mappedRedirect);
41
46
  setIsOpenedSecond(false);
42
47
  toggleModal("edit");
@@ -94,6 +99,11 @@ const RedirectItem = (props: IRedirectItemProps): JSX.Element => {
94
99
  onChange={handleOnChange}
95
100
  />
96
101
  </S.CheckCell>
102
+ <S.TypeCell role="cell" onClick={handleClick} data-testid="redirect-type">
103
+ {redirect.type && (
104
+ <S.TypeTag type="square" text={redirect.type} color={REDIRECT_TYPE_COLORS[redirect.type]} />
105
+ )}
106
+ </S.TypeCell>
97
107
  {!isSiteItem && (
98
108
  <S.SiteCell role="cell" onClick={handleClick}>
99
109
  {redirect.site?.siteName && <ElementsTooltip elements={[redirect.site.siteName]} maxChar={20} />}
@@ -1,6 +1,7 @@
1
1
  import styled from "styled-components";
2
2
 
3
3
  import { Cell, Row } from "@ax/components/TableList/TableItem/style";
4
+ import Tag from "@ax/components/Tag";
4
5
  import { ActionMenu } from "@ax/components";
5
6
 
6
7
  const CheckCell = styled(Cell)`
@@ -11,6 +12,22 @@ const CheckCell = styled(Cell)`
11
12
  }
12
13
  `;
13
14
 
15
+ // `Cell` es `flex-flow: column wrap`, que por defecto estira al hijo al ancho
16
+ // del content box; el chip debe ceñirse a su texto.
17
+ const TypeCell = styled(Cell)`
18
+ width: 96px;
19
+ align-items: flex-start;
20
+ `;
21
+
22
+ // `Tag` pinta el texto casi blanco en cuanto recibe `color`, ilegible sobre
23
+ // estos pasteles. El `&&` duplica la especificidad para ganarle a la regla de
24
+ // TagSquare siempre, y no solo cuando el orden de inyección acompaña.
25
+ const TypeTag = styled(Tag)`
26
+ && {
27
+ color: ${(p) => p.theme.color.textMediumEmphasis};
28
+ }
29
+ `;
30
+
14
31
  const SiteCell = styled(Cell)`
15
32
  width: 200px;
16
33
  `;
@@ -54,4 +71,15 @@ const ModalContent = styled.div`
54
71
  }
55
72
  `;
56
73
 
57
- export { CheckCell, SiteCell, ActionsCell, UrlCell, StyledActionMenu, ItemRow, ModalContent, DateCell };
74
+ export {
75
+ CheckCell,
76
+ TypeCell,
77
+ TypeTag,
78
+ SiteCell,
79
+ ActionsCell,
80
+ UrlCell,
81
+ StyledActionMenu,
82
+ ItemRow,
83
+ ModalContent,
84
+ DateCell,
85
+ };
@@ -1,12 +1,27 @@
1
1
  import React from "react";
2
2
  import { connect } from "react-redux";
3
3
 
4
- import type { IRedirect } from "@ax/types";
4
+ import type { IRedirect, RedirectType } from "@ax/types";
5
5
  import { Button, FieldsBehavior, FloatingPanel } from "@ax/components";
6
6
  import { redirectsActions } from "@ax/containers/Redirects";
7
7
 
8
8
  import * as S from "./style";
9
9
 
10
+ const REDIRECTION_TYPE_OPTIONS = [
11
+ {
12
+ name: "permanent",
13
+ value: "301",
14
+ title: "301 - Permanent",
15
+ description: "Keeps SEO. Use for definitive redirects.",
16
+ },
17
+ {
18
+ name: "temporary",
19
+ value: "302",
20
+ title: "302 - Temporary",
21
+ description: "Doesn’t keep SEO. Use for short-lived changes.",
22
+ },
23
+ ];
24
+
10
25
  const RedirectPanel = (props: IProps): JSX.Element => {
11
26
  const {
12
27
  isOpen,
@@ -30,11 +45,18 @@ const RedirectPanel = (props: IProps): JSX.Element => {
30
45
  setFormValues({ ...formValues, to: toValue });
31
46
  };
32
47
 
48
+ const handleType = (newValue: RedirectType) => setFormValues({ ...formValues, type: newValue });
49
+
33
50
  const addItemAction = () => addRedirect();
34
51
 
35
52
  const editItemAction = async () => {
36
53
  const toPage = formValues.to.pageId ? formValues.to.pageId : formValues.to.url;
37
- formValues.id && (await updateRedirect({ from: formValues.from, to: toPage }, formValues.id, currentFilter));
54
+ formValues.id &&
55
+ (await updateRedirect(
56
+ { from: formValues.from, to: toPage, type: formValues.type },
57
+ formValues.id,
58
+ currentFilter,
59
+ ));
38
60
  toggleModal();
39
61
  };
40
62
 
@@ -81,6 +103,17 @@ const RedirectPanel = (props: IProps): JSX.Element => {
81
103
  autoComplete="redirect-new"
82
104
  helptext="Select an internal page or paste an external url."
83
105
  />
106
+ {/* El panel de alta sigue montado mientras se edita una fila. Con el mismo
107
+ `name` y sin <form>, el navegador trata los radios de los dos paneles
108
+ como un solo grupo y el `checked` se lo queda el panel cerrado. */}
109
+ <FieldsBehavior
110
+ title="Redirection type"
111
+ name={redirect ? `redirectionType-${redirect.id}` : "redirectionType"}
112
+ fieldType="RadioGroup"
113
+ value={formValues.type}
114
+ options={REDIRECTION_TYPE_OPTIONS}
115
+ onChange={handleType}
116
+ />
84
117
  <S.Footer>
85
118
  {redirect ? (
86
119
  <Button className="button" type="button" onClick={editButton.action} disabled={editButton.disabled}>
@@ -113,7 +146,11 @@ interface ICategoryPanelProps {
113
146
 
114
147
  interface IDispatchProps {
115
148
  addRedirect(force?: boolean): void;
116
- updateRedirect(redirect: { from: string; to: string | number }, redirectID: number, filter?: string): Promise<void>;
149
+ updateRedirect(
150
+ redirect: { from: string; to: string | number; type?: RedirectType },
151
+ redirectID: number,
152
+ filter?: string,
153
+ ): Promise<void>;
117
154
  }
118
155
 
119
156
  type IProps = ICategoryPanelProps & IDispatchProps;
@@ -0,0 +1,44 @@
1
+ import { FloatingMenu, Icon, ListItem, ListTitle } from "@ax/components";
2
+ import type { IQueryValue, RedirectType } from "@ax/types";
3
+
4
+ import * as S from "./style";
5
+
6
+ const REDIRECT_TYPES: RedirectType[] = ["301", "302"];
7
+
8
+ const TypeFilter = (props: ITypeFilterProps): JSX.Element => {
9
+ const { filterItems, value } = props;
10
+
11
+ const selected = value?.length ? value[0].value : undefined;
12
+
13
+ // El menú no ofrece un "All": volver a marcar el tipo activo es lo que lo
14
+ // quita, y la barra de filtros seleccionados conserva su "Clear All".
15
+ const chooseType = (type: RedirectType) =>
16
+ filterItems("type", selected === type ? [] : [{ value: type, label: type }]);
17
+
18
+ const Header = () => (
19
+ <S.Type isActive={!!selected} data-testid="redirect-type-filter">
20
+ Type
21
+ <S.IconsWrapper>
22
+ {selected ? <Icon name="Filter" size="16" /> : <Icon name="DownArrow" size="16" />}
23
+ </S.IconsWrapper>
24
+ </S.Type>
25
+ );
26
+
27
+ return (
28
+ <FloatingMenu Button={Header} position="left">
29
+ <ListTitle>Filter by type</ListTitle>
30
+ {REDIRECT_TYPES.map((type) => (
31
+ <ListItem key={type} isSelected={selected === type} onClick={() => chooseType(type)}>
32
+ {type}
33
+ </ListItem>
34
+ ))}
35
+ </FloatingMenu>
36
+ );
37
+ };
38
+
39
+ export interface ITypeFilterProps {
40
+ filterItems(pointer: string, filter: IQueryValue[]): void;
41
+ value?: IQueryValue[];
42
+ }
43
+
44
+ export default TypeFilter;
@@ -0,0 +1,27 @@
1
+ import styled from "styled-components";
2
+
3
+ // Va dentro de la celda `TypeHeader`, que ya pone el ancho y el padding de la
4
+ // columna: aquí solo el estado del filtro.
5
+ const Type = styled.div<{ isActive: boolean }>`
6
+ ${(p) => p.theme.textStyle.uiS};
7
+ box-sizing: border-box;
8
+ display: flex;
9
+ flex-direction: row;
10
+ width: 100%;
11
+ position: relative;
12
+ color: ${(p) => (p.isActive ? p.theme.color.textHighEmphasis : p.theme.color.textMediumEmphasis)};
13
+ &:hover {
14
+ color: ${(p) => p.theme.color.interactive01};
15
+ }
16
+ `;
17
+
18
+ const IconsWrapper = styled.div`
19
+ display: flex;
20
+ align-items: center;
21
+ flex-direction: row;
22
+ svg {
23
+ margin-left: 4px;
24
+ }
25
+ `;
26
+
27
+ export { Type, IconsWrapper };
@@ -18,14 +18,17 @@ const useSortedListStatus = () => {
18
18
  const useFilterQuery = (site: ISite | null) => {
19
19
  const initialQueryValues: Record<string, IQueryValue[]> = {
20
20
  sites: site ? [{ value: site.id, label: site.name }] : [{ value: "all", label: "All" }],
21
+ type: [],
21
22
  order: [],
22
23
  };
23
24
 
25
+ const initialFilterQuery = `&sites=${site?.id || "global"}`;
26
+
24
27
  const [query, setQuery] = useState(initialQueryValues);
25
- const [currentFilterQuery, setCurrentFilterQuery] = useState(`&sites=${site?.id || "global"}`);
28
+ const [currentFilterQuery, setCurrentFilterQuery] = useState(initialFilterQuery);
26
29
 
27
30
  const setFilterQuery = (filterValues: any) => {
28
- const { sites, order } = filterValues;
31
+ const { sites, type, order } = filterValues;
29
32
  let filterQuery = "";
30
33
 
31
34
  const currentQuery = (pointer: string, values: IQueryValue[]): string => {
@@ -40,6 +43,10 @@ const useFilterQuery = (site: ISite | null) => {
40
43
  filterQuery = currentQuery("sites", sites);
41
44
  }
42
45
 
46
+ if (type) {
47
+ filterQuery = currentQuery("type", type);
48
+ }
49
+
43
50
  if (order) {
44
51
  filterQuery = currentQuery("order", order);
45
52
  }
@@ -48,10 +55,11 @@ const useFilterQuery = (site: ISite | null) => {
48
55
  };
49
56
 
50
57
  const setFiltersSelection = (pointer: string, filter: IQueryValue[], isAscendent?: boolean) => {
51
- const { sites, order } = query;
58
+ const { sites, type, order } = query;
52
59
  const orderMethod = isAscendent ? "asc" : "desc";
53
60
  const filterValues: Record<string, IQueryValue[]> = {
54
61
  sites: pointer === "sites" ? filter : sites,
62
+ type: pointer === "type" ? filter : type,
55
63
  order: pointer === "order" ? [{ value: `${filter[0].value}-${orderMethod}`, label: filter[0].label }] : order,
56
64
  };
57
65
 
@@ -61,7 +69,7 @@ const useFilterQuery = (site: ISite | null) => {
61
69
 
62
70
  const resetFilterQuery = () => {
63
71
  setQuery(initialQueryValues);
64
- setCurrentFilterQuery("");
72
+ setCurrentFilterQuery(initialFilterQuery);
65
73
  };
66
74
 
67
75
  return {
@@ -14,14 +14,14 @@ import {
14
14
  import { appActions } from "@ax/containers/App";
15
15
  import { redirectsActions } from "@ax/containers/Redirects";
16
16
  import { useBulkSelection, useModals, useToast } from "@ax/hooks";
17
- import type { IEmptyStateProps, INavItem, IQueryValue, IRedirect, IRootState, ISite } from "@ax/types";
17
+ import type { IEmptyStateProps, INavItem, IQueryValue, IRedirect, IRootState, ISite, RedirectType } from "@ax/types";
18
18
 
19
19
  import { DeleteModal, ImportCheckModal, ImportModal, OverwriteModal } from "./atoms";
20
20
  import BulkHeader from "./BulkHeader";
21
21
  import { useFilterQuery, useSortedListStatus } from "./hooks";
22
22
  import RedirectItem from "./RedirectItem";
23
23
  import RedirectPanel from "./RedirectPanel";
24
- import { getSortedListStatus } from "./utils";
24
+ import { DEFAULT_REDIRECT_TYPE, getSortedListStatus } from "./utils";
25
25
 
26
26
  import * as S from "./style";
27
27
 
@@ -61,12 +61,13 @@ const Redirects = (props: IProps): JSX.Element => {
61
61
  const [isEmpty, setIsEmpty] = useState(false);
62
62
  const [emptyStateProps, setEmptyStateProps] = useState<IEmptyStateProps>({});
63
63
 
64
- const initState = {
64
+ const initState: IRedirect = {
65
65
  from: "",
66
66
  to: {
67
67
  pageId: undefined,
68
68
  url: "",
69
69
  },
70
+ type: DEFAULT_REDIRECT_TYPE,
70
71
  };
71
72
 
72
73
  const [formValues, setFormValues] = useState<IRedirect>(initState);
@@ -202,7 +203,12 @@ const Redirects = (props: IProps): JSX.Element => {
202
203
 
203
204
  const handleAddRedirect = async (force?: boolean) => {
204
205
  const toPage = formValues.to.pageId ? formValues.to.pageId : formValues.to.url;
205
- await addRedirect({ from: formValues.from, to: toPage }, () => toggleModal("overwrite"), force, filterQuery);
206
+ await addRedirect(
207
+ { from: formValues.from, to: toPage, type: formValues.type },
208
+ () => toggleModal("overwrite"),
209
+ force,
210
+ filterQuery,
211
+ );
206
212
  isOpen("edit") && toggleModal("edit");
207
213
  };
208
214
 
@@ -268,7 +274,8 @@ const Redirects = (props: IProps): JSX.Element => {
268
274
  <S.TitleWrapper>
269
275
  <S.Title>URL Redirects Manager</S.Title>
270
276
  <S.Description>
271
- You can create 301 redirects to direct users and search engines from an old URL to a new one.
277
+ You can create redirects to direct users and search engines from an old URL to a new one, and choose
278
+ whether each one is permanent (301) or temporary (302).
272
279
  </S.Description>
273
280
  </S.TitleWrapper>
274
281
  <S.TableWrapper>
@@ -282,14 +289,15 @@ const Redirects = (props: IProps): JSX.Element => {
282
289
  <>
283
290
  <S.SearchTags>
284
291
  <SearchTagsBar query={searchQuery} setQuery={handleSearch} />
285
- {!currentSite && (
286
- <FilterTagsBar
287
- filters={filterValues}
288
- setFilters={setFiltersSelection}
289
- resetFilters={resetFilterQuery}
290
- labels={{ sites: "Site" }}
291
- />
292
- )}
292
+ {/* El filtro de tipo existe en los dos ámbitos, así que la barra ya no
293
+ depende del ámbito. En el de sitio se etiqueta solo el tipo: el sitio
294
+ lo fija la ruta y ofrecer su tag invitaría a quitar lo que no se puede. */}
295
+ <FilterTagsBar
296
+ filters={filterValues}
297
+ setFilters={setFiltersSelection}
298
+ resetFilters={resetFilterQuery}
299
+ labels={currentSite ? { type: "Type" } : { sites: "Site", type: "Type" }}
300
+ />
293
301
  </S.SearchTags>
294
302
  {isEmpty ? (
295
303
  <S.EmptyWrapper>
@@ -399,7 +407,7 @@ interface IDispatchProps {
399
407
  getRedirects(params: any, filters: string): void;
400
408
  deleteRedirect(redirectID: number[]): Promise<boolean>;
401
409
  addRedirect(
402
- redirect: { from: string; to: string | number },
410
+ redirect: { from: string; to: string | number; type?: RedirectType },
403
411
  errorAction: () => void,
404
412
  force?: boolean,
405
413
  filter?: string,
@@ -1,3 +1,5 @@
1
+ import type { RedirectType } from "@ax/types";
2
+
1
3
  const getSortedListStatus = (
2
4
  orderPointer: string,
3
5
  isAscending: boolean,
@@ -10,4 +12,11 @@ const getSortedListStatus = (
10
12
  return sortedListStatus;
11
13
  };
12
14
 
13
- export { getSortedListStatus };
15
+ const DEFAULT_REDIRECT_TYPE: RedirectType = "301";
16
+
17
+ const REDIRECT_TYPE_COLORS: Record<RedirectType, string> = {
18
+ "301": "#C9EFFF",
19
+ "302": "#DBB9FF",
20
+ };
21
+
22
+ export { getSortedListStatus, DEFAULT_REDIRECT_TYPE, REDIRECT_TYPE_COLORS };
@@ -914,10 +914,13 @@ export interface IDomainRobot {
914
914
  computedContent: string;
915
915
  }
916
916
 
917
+ export type RedirectType = "301" | "302";
918
+
917
919
  export interface IRedirect {
918
920
  id?: number;
919
921
  from: string;
920
922
  to: { pageId?: number; url: string; linkTo?: number; href?: string };
923
+ type?: RedirectType;
921
924
  site?: { siteId: number | null; siteName: string | null; siteUrl: string | null };
922
925
  domain?: string;
923
926
  date?: string;
package/tsconfig.json CHANGED
@@ -15,15 +15,7 @@
15
15
  "useUnknownInCatchVariables": false,
16
16
  "noEmit": true,
17
17
  "jsx": "react-jsx",
18
- "types": [
19
- "vitest/globals",
20
- "react",
21
- "reflect-metadata",
22
- "node",
23
- "webpack-env",
24
- "react-dom",
25
- "@testing-library/jest-dom"
26
- ]
18
+ "types": ["vitest/globals", "react", "node", "react-dom", "@testing-library/jest-dom"]
27
19
  },
28
20
  "include": ["src"],
29
21
  "extends": "./tsconfig.paths.json"