@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.
- package/README.md +0 -2
- package/config/webpack.config.js +2 -1
- package/config/webpackSchemas.config.js +7 -4
- package/package.json +31 -56
- package/src/__tests__/components/Fields/ConditionalField/ConditionalField.test.tsx +17 -0
- package/src/__tests__/components/Fields/RadioField/RadioField.test.tsx +66 -0
- package/src/__tests__/components/Fields/RadioGroup/RadioGroup.test.tsx +76 -0
- package/src/__tests__/modules/Redirects/BulkHeader/TableHeader.test.tsx +123 -0
- package/src/__tests__/modules/Redirects/RedirectItem/RedirectItem.test.tsx +163 -0
- package/src/__tests__/modules/Redirects/RedirectPanel/RedirectPanel.test.tsx +134 -0
- package/src/__tests__/modules/Redirects/Redirects/Redirects.create.test.tsx +69 -0
- package/src/__tests__/modules/Redirects/Redirects/Redirects.edit.test.tsx +91 -0
- package/src/__tests__/modules/Redirects/Redirects/Redirects.filter.test.tsx +131 -0
- package/src/components/Fields/ConditionalField/style.tsx +1 -1
- package/src/components/Fields/RadioField/index.tsx +20 -3
- package/src/components/Fields/RadioField/style.tsx +18 -1
- package/src/components/Fields/RadioGroup/index.tsx +1 -0
- package/src/containers/Redirects/actions.tsx +15 -3
- package/src/global.d.ts +0 -1
- package/src/modules/ActivityLog/DownloadModal/index.tsx +2 -1
- package/src/modules/ActivityLog/index.tsx +2 -2
- package/src/modules/Redirects/BulkHeader/TableHeader/index.tsx +5 -0
- package/src/modules/Redirects/BulkHeader/TableHeader/style.tsx +5 -1
- package/src/modules/Redirects/RedirectItem/index.tsx +11 -1
- package/src/modules/Redirects/RedirectItem/style.tsx +29 -1
- package/src/modules/Redirects/RedirectPanel/index.tsx +40 -3
- package/src/modules/Redirects/TypeFilter/index.tsx +44 -0
- package/src/modules/Redirects/TypeFilter/style.tsx +27 -0
- package/src/modules/Redirects/hooks.tsx +12 -4
- package/src/modules/Redirects/index.tsx +22 -14
- package/src/modules/Redirects/utils.tsx +10 -1
- package/src/types/index.tsx +3 -0
- package/tsconfig.json +1 -9
|
@@ -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
|
@@ -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:
|
|
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:
|
|
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 = {
|
|
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 {
|
|
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 &&
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
{
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
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 };
|
package/src/types/index.tsx
CHANGED
|
@@ -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"
|