@griddo/ax 12.6.0 → 12.6.1
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/config/griddo-config/index.js +1 -0
- package/package.json +3 -4
- package/src/GlobalStore.tsx +91 -35
- package/src/__tests__/modules/ActivityLog/ActivityLog.uc.test.tsx +132 -0
- package/src/__tests__/modules/Analytics/Analytics.uc.test.tsx +168 -0
- package/src/__tests__/modules/GlobalSettings/Robots/Robots.test.tsx +110 -0
- package/src/__tests__/modules/GlobalSettings/Robots/Robots.uc.test.tsx +125 -0
- package/src/__tests__/modules/Redirects/Redirects.uc.test.tsx +164 -0
- package/src/__tests__/modules/Settings/Languages/Languages.uc.test.tsx +213 -0
- package/src/__tests__/modules/Settings/Social/Social.uc.test.tsx +171 -0
- package/src/__tests__/store/activityLog.test.ts +301 -0
- package/src/__tests__/store/analytics.test.ts +134 -0
- package/src/__tests__/store/domains.test.ts +107 -0
- package/src/__tests__/store/languages.test.ts +129 -0
- package/src/__tests__/store/persist.test.ts +47 -0
- package/src/__tests__/store/redirects.test.ts +313 -0
- package/src/__tests__/store/social.test.ts +165 -0
- package/src/components/MainWrapper/index.tsx +14 -2
- package/src/containers/ActivityLog/index.tsx +16 -3
- package/src/containers/ActivityLog/slice.ts +45 -0
- package/src/containers/ActivityLog/{actions.tsx → thunks.ts} +104 -89
- package/src/containers/Analytics/index.tsx +14 -3
- package/src/containers/Analytics/slice.ts +48 -0
- package/src/containers/Analytics/thunks.ts +103 -0
- package/src/containers/Domains/index.tsx +12 -3
- package/src/containers/Domains/slice.ts +37 -0
- package/src/containers/Domains/thunks.ts +60 -0
- package/src/containers/Redirects/constants.tsx +1 -17
- package/src/containers/Redirects/index.tsx +14 -3
- package/src/containers/Redirects/slice.ts +60 -0
- package/src/containers/Redirects/thunks.ts +196 -0
- package/src/containers/Settings/Languages/index.tsx +15 -3
- package/src/containers/Settings/Languages/slice.ts +48 -0
- package/src/containers/Settings/Languages/thunks.ts +83 -0
- package/src/containers/Settings/Social/index.tsx +11 -3
- package/src/containers/Settings/Social/slice.ts +40 -0
- package/src/containers/Settings/Social/thunks.ts +68 -0
- package/src/containers/Sites/actions.tsx +6 -2
- package/src/index.tsx +7 -6
- package/src/modules/ActivityLog/ItemLog/EventItem/index.tsx +13 -27
- package/src/modules/ActivityLog/ItemLogUser/UserItem/EventItem/index.tsx +13 -27
- package/src/modules/ActivityLog/index.tsx +22 -51
- package/src/modules/Analytics/index.tsx +33 -42
- package/src/modules/App/Routing/index.tsx +5 -4
- package/src/modules/FileDrive/index.tsx +1 -1
- package/src/modules/GlobalSettings/Robots/index.tsx +24 -47
- package/src/modules/MediaGallery/index.tsx +1 -1
- package/src/modules/Redirects/RedirectItem/index.tsx +9 -22
- package/src/modules/Redirects/RedirectPanel/index.tsx +13 -14
- package/src/modules/Redirects/index.tsx +26 -69
- package/src/modules/Settings/ContentTypes/DataPacks/index.tsx +4 -3
- package/src/modules/Settings/Languages/LanguagePanel/Form/ConnectedField/index.tsx +11 -18
- package/src/modules/Settings/Languages/LanguagePanel/Form/index.tsx +7 -9
- package/src/modules/Settings/Languages/LanguagePanel/index.tsx +38 -44
- package/src/modules/Settings/Languages/Table/Header/index.tsx +12 -19
- package/src/modules/Settings/Languages/Table/Item/index.tsx +21 -36
- package/src/modules/Settings/Languages/Table/index.tsx +6 -14
- package/src/modules/Settings/Languages/index.tsx +18 -30
- package/src/modules/Settings/SeoAnalyticsSettings/Analytics/index.tsx +42 -52
- package/src/modules/Settings/Social/index.tsx +17 -12
- package/src/modules/StructuredData/StructuredDataList/index.tsx +3 -2
- package/src/store/hooks.ts +37 -0
- package/src/store/index.ts +2 -0
- package/src/types/index.tsx +5 -6
- package/tsconfig.paths.json +1 -0
- package/src/containers/ActivityLog/constants.tsx +0 -6
- package/src/containers/ActivityLog/interfaces.tsx +0 -12
- package/src/containers/ActivityLog/reducer.tsx +0 -24
- package/src/containers/Analytics/actions.tsx +0 -109
- package/src/containers/Analytics/constants.tsx +0 -5
- package/src/containers/Analytics/interfaces.tsx +0 -9
- package/src/containers/Analytics/reducer.tsx +0 -28
- package/src/containers/Domains/actions.tsx +0 -58
- package/src/containers/Domains/constants.tsx +0 -5
- package/src/containers/Domains/interfaces.tsx +0 -9
- package/src/containers/Domains/reducer.tsx +0 -22
- package/src/containers/Redirects/actions.tsx +0 -190
- package/src/containers/Redirects/interfaces.tsx +0 -23
- package/src/containers/Redirects/reducer.tsx +0 -35
- package/src/containers/Settings/Languages/actions.tsx +0 -88
- package/src/containers/Settings/Languages/constants.tsx +0 -6
- package/src/containers/Settings/Languages/reducer.tsx +0 -31
- package/src/containers/Settings/Social/actions.tsx +0 -63
- package/src/containers/Settings/Social/constants.tsx +0 -3
- package/src/containers/Settings/Social/interfaces.tsx +0 -9
- package/src/containers/Settings/Social/reducer.tsx +0 -18
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { redirects } from "@ax/api";
|
|
2
|
+
import { appActions } from "@ax/containers/App";
|
|
3
|
+
import { handleRequest } from "@ax/helpers";
|
|
4
|
+
import type { IRedirect, IRootState } from "@ax/types";
|
|
5
|
+
|
|
6
|
+
import { createAsyncThunk } from "@reduxjs/toolkit";
|
|
7
|
+
import type { Dispatch } from "redux";
|
|
8
|
+
|
|
9
|
+
import { DEFAULT_PARAMS, ERROR_CODE } from "./constants";
|
|
10
|
+
import type { IRedirectsState } from "./slice";
|
|
11
|
+
import { setImports, setRedirects, setTotalImports, setTotalItems } from "./slice";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Read and write side of UC-AX-manage-redirects, as createAsyncThunk.
|
|
15
|
+
*
|
|
16
|
+
* They keep calling `handleRequest` inside for the same reason the Languages pilot does:
|
|
17
|
+
* that helper owns the loading-flag and error-channel conventions of the whole codebase —
|
|
18
|
+
* see docs/RTK-MIGRATION.md §5.
|
|
19
|
+
*
|
|
20
|
+
* This area is where the **callback-as-thunk-argument** mine of §5 finally goes off.
|
|
21
|
+
* `addRedirect` used to take an `errorAction: () => void` that it called on a 409, and
|
|
22
|
+
* createAsyncThunk keeps its argument in `meta.arg` of every action it dispatches — a
|
|
23
|
+
* function there trips the `serializableCheck` that GlobalStore deliberately leaves on.
|
|
24
|
+
* So the 409 now comes back as **data** (`isOriginTaken`) and the screen decides what to
|
|
25
|
+
* open, which is where a decision about modals belonged in the first place.
|
|
26
|
+
*
|
|
27
|
+
* As with the other areas, the legacy `try/catch` whose body was `console.log(e)` is gone:
|
|
28
|
+
* createAsyncThunk turns a throw into a `rejected` action, so the failure is observable.
|
|
29
|
+
*/
|
|
30
|
+
const onError = (response: unknown, dispatch: Dispatch) => appActions.handleError(response)(dispatch);
|
|
31
|
+
|
|
32
|
+
interface IMessageResponse {
|
|
33
|
+
data: { message?: string | string[] };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// The two bulk endpoints answer with one message per offending row, and the screen reports
|
|
37
|
+
// the count instead of the messages. Same wording as before, verb apart.
|
|
38
|
+
const onCountedError = (response: IMessageResponse, dispatch: Dispatch, verb: "delete" | "import") => {
|
|
39
|
+
const { message } = response.data;
|
|
40
|
+
const isMultiple = Array.isArray(message) && message.length > 1;
|
|
41
|
+
const msg = isMultiple ? `The ${verb} action failed due to ${message.length} errors.` : undefined;
|
|
42
|
+
appActions.handleError(response, isMultiple, msg)(dispatch);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Every write reloads the list, and the filter it reloads with depends on scope: the loaded
|
|
46
|
+
// site if there is one, otherwise whatever filter the caller passed. Kept as a pure helper
|
|
47
|
+
// so each thunk dispatches the reload with its own correctly typed `dispatch`.
|
|
48
|
+
const scopedFilter = (getState: () => unknown, filter?: string) => {
|
|
49
|
+
const { currentSiteInfo } = (getState() as IRootState).sites;
|
|
50
|
+
return currentSiteInfo?.id ? `&sites=${currentSiteInfo.id}` : filter;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export interface IRedirectsQuery {
|
|
54
|
+
page: number;
|
|
55
|
+
itemsPerPage: number;
|
|
56
|
+
pagination: boolean;
|
|
57
|
+
query?: string;
|
|
58
|
+
filterBy?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface IGetRedirectsArgs {
|
|
62
|
+
params: IRedirectsQuery;
|
|
63
|
+
filters?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const getRedirects = createAsyncThunk(
|
|
67
|
+
"redirects/getRedirects",
|
|
68
|
+
async ({ params, filters }: IGetRedirectsArgs, { dispatch }) => {
|
|
69
|
+
// Empties the list before asking, as before: the table blanks instead of showing the
|
|
70
|
+
// previous page underneath the spinner.
|
|
71
|
+
dispatch(setRedirects([]));
|
|
72
|
+
|
|
73
|
+
return handleRequest(
|
|
74
|
+
async () => redirects.getRedirects(params, filters),
|
|
75
|
+
{
|
|
76
|
+
handleSuccess: ({ items, totalItems }: { items: IRedirect[]; totalItems: number }) => {
|
|
77
|
+
dispatch(setRedirects(items));
|
|
78
|
+
dispatch(setTotalItems(totalItems));
|
|
79
|
+
},
|
|
80
|
+
handleError: (response: unknown) => onError(response, dispatch),
|
|
81
|
+
},
|
|
82
|
+
[appActions.setIsLoading],
|
|
83
|
+
)(dispatch);
|
|
84
|
+
},
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
interface IRedirectForm {
|
|
88
|
+
from: string;
|
|
89
|
+
to: string | number;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
interface IAddRedirectArgs {
|
|
93
|
+
redirect: IRedirectForm;
|
|
94
|
+
force?: boolean;
|
|
95
|
+
filter?: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface IAddRedirectResult {
|
|
99
|
+
saved: boolean;
|
|
100
|
+
/** The `from` already has a redirect (409). Not a failure — the screen offers to overwrite. */
|
|
101
|
+
isOriginTaken: boolean;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export const addRedirect = createAsyncThunk(
|
|
105
|
+
"redirects/addRedirect",
|
|
106
|
+
async ({ redirect, force, filter }: IAddRedirectArgs, { dispatch, getState }): Promise<IAddRedirectResult> => {
|
|
107
|
+
let isOriginTaken = false;
|
|
108
|
+
|
|
109
|
+
const saved = await handleRequest(
|
|
110
|
+
async () => redirects.createRedirect(redirect, force),
|
|
111
|
+
{
|
|
112
|
+
handleSuccess: () =>
|
|
113
|
+
dispatch(getRedirects({ params: { ...DEFAULT_PARAMS }, filters: scopedFilter(getState, filter) })),
|
|
114
|
+
handleError: (response: { status: number }) => {
|
|
115
|
+
if (response.status === ERROR_CODE.isBeingUsed) {
|
|
116
|
+
isOriginTaken = true;
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
onError(response, dispatch);
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
[],
|
|
123
|
+
)(dispatch);
|
|
124
|
+
|
|
125
|
+
return { saved, isOriginTaken };
|
|
126
|
+
},
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
interface IUpdateRedirectArgs {
|
|
130
|
+
redirect: IRedirectForm;
|
|
131
|
+
redirectID: number;
|
|
132
|
+
filter?: string;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export const updateRedirect = createAsyncThunk(
|
|
136
|
+
"redirects/updateRedirect",
|
|
137
|
+
async ({ redirect, redirectID, filter }: IUpdateRedirectArgs, { dispatch, getState }) =>
|
|
138
|
+
handleRequest(
|
|
139
|
+
async () => redirects.updateRedirect(redirect, redirectID),
|
|
140
|
+
{
|
|
141
|
+
handleSuccess: () =>
|
|
142
|
+
dispatch(getRedirects({ params: { ...DEFAULT_PARAMS }, filters: scopedFilter(getState, filter) })),
|
|
143
|
+
handleError: (response: unknown) => onError(response, dispatch),
|
|
144
|
+
},
|
|
145
|
+
[],
|
|
146
|
+
)(dispatch),
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
interface IDeleteRedirectArgs {
|
|
150
|
+
redirectID: number | number[];
|
|
151
|
+
filter?: string;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export const deleteRedirect = createAsyncThunk(
|
|
155
|
+
"redirects/deleteRedirect",
|
|
156
|
+
async ({ redirectID, filter }: IDeleteRedirectArgs, { dispatch, getState }) =>
|
|
157
|
+
handleRequest(
|
|
158
|
+
async () =>
|
|
159
|
+
Array.isArray(redirectID) ? redirects.deleteRedirectsBulk(redirectID) : redirects.deleteRedirect(redirectID),
|
|
160
|
+
{
|
|
161
|
+
handleSuccess: () =>
|
|
162
|
+
dispatch(getRedirects({ params: { ...DEFAULT_PARAMS }, filters: scopedFilter(getState, filter) })),
|
|
163
|
+
handleError: (response: IMessageResponse) => onCountedError(response, dispatch, "delete"),
|
|
164
|
+
},
|
|
165
|
+
[appActions.setIsSaving],
|
|
166
|
+
)(dispatch),
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
interface IImportRedirectsArgs {
|
|
170
|
+
rows: IRedirectForm[];
|
|
171
|
+
/** First phase: ask the backend to classify the rows without writing anything. */
|
|
172
|
+
check: boolean;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export const importRedirects = createAsyncThunk(
|
|
176
|
+
"redirects/importRedirects",
|
|
177
|
+
async ({ rows, check }: IImportRedirectsArgs, { dispatch }) =>
|
|
178
|
+
handleRequest(
|
|
179
|
+
async () => (check ? redirects.createRedirectsBulkCheck(rows) : redirects.createRedirectsBulk(rows)),
|
|
180
|
+
{
|
|
181
|
+
handleSuccess: (data: { total: number } & NonNullable<IRedirectsState["imports"]>) => {
|
|
182
|
+
if (check) {
|
|
183
|
+
const { total, error, existing, ok } = data;
|
|
184
|
+
dispatch(setImports({ error, existing, ok }));
|
|
185
|
+
dispatch(setTotalImports(total));
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
// Reloads UNFILTERED, unlike every other write. Kept as-is: after importing a
|
|
189
|
+
// batch the user is shown the whole catalogue, not the current site's slice.
|
|
190
|
+
dispatch(getRedirects({ params: { ...DEFAULT_PARAMS } }));
|
|
191
|
+
},
|
|
192
|
+
handleError: (response: IMessageResponse) => onCountedError(response, dispatch, "import"),
|
|
193
|
+
},
|
|
194
|
+
[],
|
|
195
|
+
)(dispatch),
|
|
196
|
+
);
|
|
@@ -1,4 +1,16 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import { resetForm, setForm, updateFormValue } from "./slice";
|
|
2
|
+
import { createSiteLanguage, deleteSiteLanguage, updateSiteLanguage } from "./thunks";
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
const languagesActions = {
|
|
5
|
+
setForm,
|
|
6
|
+
updateFormValue,
|
|
7
|
+
resetForm,
|
|
8
|
+
createSiteLanguage,
|
|
9
|
+
updateSiteLanguage,
|
|
10
|
+
deleteSiteLanguage,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export { languagesActions };
|
|
14
|
+
|
|
15
|
+
export type { ILanguageState } from "./slice";
|
|
16
|
+
export { languagesInitialState, languagesReducer } from "./slice";
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { ISiteLanguage } from "@ax/types";
|
|
2
|
+
|
|
3
|
+
import { createSlice, type PayloadAction } from "@reduxjs/toolkit";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* First area migrated to Redux Toolkit (UC-AX-manage-languages).
|
|
7
|
+
*
|
|
8
|
+
* The slice holds ONLY the language panel's form. The list of a site's languages
|
|
9
|
+
* lives in `sites.currentSiteLanguages` and the loading flags in `app`, which is
|
|
10
|
+
* why this area is so small — see docs/RTK-MIGRATION.md and the use case notes.
|
|
11
|
+
*
|
|
12
|
+
* `updateFormValue` and `resetForm` used to be thunks that read `getState()` to
|
|
13
|
+
* merge onto the current form; as reducers they express the same thing directly,
|
|
14
|
+
* so the public shape (name + argument) is unchanged for callers.
|
|
15
|
+
*/
|
|
16
|
+
export interface ILanguageState {
|
|
17
|
+
form: ISiteLanguage;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const initialState: ILanguageState = {
|
|
21
|
+
form: {
|
|
22
|
+
language: null,
|
|
23
|
+
path: "",
|
|
24
|
+
domain: null,
|
|
25
|
+
isDefault: false,
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const languagesSlice = createSlice({
|
|
30
|
+
name: "languages",
|
|
31
|
+
initialState,
|
|
32
|
+
reducers: {
|
|
33
|
+
setForm(state, action: PayloadAction<ISiteLanguage>) {
|
|
34
|
+
state.form = action.payload;
|
|
35
|
+
},
|
|
36
|
+
updateFormValue(state, action: PayloadAction<Partial<ISiteLanguage>>) {
|
|
37
|
+
state.form = { ...state.form, ...action.payload };
|
|
38
|
+
},
|
|
39
|
+
resetForm(state) {
|
|
40
|
+
state.form = initialState.form;
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export const { setForm, updateFormValue, resetForm } = languagesSlice.actions;
|
|
46
|
+
|
|
47
|
+
export { initialState as languagesInitialState };
|
|
48
|
+
export const languagesReducer = languagesSlice.reducer;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { languages } from "@ax/api";
|
|
2
|
+
import { appActions } from "@ax/containers/App";
|
|
3
|
+
import { sitesActions } from "@ax/containers/Sites";
|
|
4
|
+
import { handleRequest } from "@ax/helpers";
|
|
5
|
+
import type { ISiteLanguage } from "@ax/types";
|
|
6
|
+
|
|
7
|
+
import { createAsyncThunk } from "@reduxjs/toolkit";
|
|
8
|
+
import type { Dispatch } from "redux";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Write side of UC-AX-manage-languages, as createAsyncThunk.
|
|
12
|
+
*
|
|
13
|
+
* They keep calling `handleRequest` inside: that helper is the codebase's
|
|
14
|
+
* hand-rolled createAsyncThunk (152 call-sites) and owns the loading-flag and
|
|
15
|
+
* error-channel conventions. Replacing it is a cross-cutting job, not this
|
|
16
|
+
* area's — see docs/RTK-MIGRATION.md §5.
|
|
17
|
+
*
|
|
18
|
+
* What does change: the legacy versions wrapped everything in a `try/catch` whose
|
|
19
|
+
* body was `console.log(e) // TODO: capturar error bien`, swallowing failures.
|
|
20
|
+
* createAsyncThunk turns a throw into a `rejected` action instead, so the failure
|
|
21
|
+
* is at least observable in devtools rather than lost.
|
|
22
|
+
*
|
|
23
|
+
* Reads live in `sitesActions.getSiteLanguages` (the list belongs to `sites`), so
|
|
24
|
+
* every write refreshes through it, exactly as before.
|
|
25
|
+
*/
|
|
26
|
+
// Called through the namespaces rather than destructured at module scope: a
|
|
27
|
+
// destructured reference is frozen at import time and cannot be spied on, which
|
|
28
|
+
// would make the refresh-after-write behaviour untestable.
|
|
29
|
+
const refreshList = (siteID: number, dispatch: Dispatch) => sitesActions.getSiteLanguages(siteID)(dispatch);
|
|
30
|
+
|
|
31
|
+
const onError = (response: unknown, dispatch: Dispatch) => appActions.handleError(response)(dispatch);
|
|
32
|
+
|
|
33
|
+
interface ISiteLanguageArgs {
|
|
34
|
+
siteID: number;
|
|
35
|
+
data: ISiteLanguage;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface IDeleteSiteLanguageArgs {
|
|
39
|
+
siteID: number;
|
|
40
|
+
languageID: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const createSiteLanguage = createAsyncThunk(
|
|
44
|
+
"languages/createSiteLanguage",
|
|
45
|
+
async ({ siteID, data }: ISiteLanguageArgs, { dispatch }) =>
|
|
46
|
+
handleRequest(
|
|
47
|
+
async () => languages.createSiteLanguage(siteID, data),
|
|
48
|
+
{
|
|
49
|
+
handleSuccess: () => refreshList(siteID, dispatch),
|
|
50
|
+
handleError: (response: unknown) => onError(response, dispatch),
|
|
51
|
+
},
|
|
52
|
+
[appActions.setIsSaving],
|
|
53
|
+
)(dispatch),
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
export const updateSiteLanguage = createAsyncThunk(
|
|
57
|
+
"languages/updateSiteLanguage",
|
|
58
|
+
async ({ siteID, data }: ISiteLanguageArgs, { dispatch }) =>
|
|
59
|
+
handleRequest(
|
|
60
|
+
async () => languages.updateSiteLanguage(siteID, data),
|
|
61
|
+
{
|
|
62
|
+
handleSuccess: () => refreshList(siteID, dispatch),
|
|
63
|
+
handleError: (response: unknown) => onError(response, dispatch),
|
|
64
|
+
},
|
|
65
|
+
[appActions.setIsSaving],
|
|
66
|
+
)(dispatch),
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
export const deleteSiteLanguage = createAsyncThunk(
|
|
70
|
+
"languages/deleteSiteLanguage",
|
|
71
|
+
// setIsLoading, not setIsSaving — kept as-is to preserve behaviour: the list
|
|
72
|
+
// view only renders <Loading /> off app.isLoading, so deleting shows the
|
|
73
|
+
// spinner while adding/editing does not. Inconsistency noted in the use case.
|
|
74
|
+
async ({ siteID, languageID }: IDeleteSiteLanguageArgs, { dispatch }) =>
|
|
75
|
+
handleRequest(
|
|
76
|
+
async () => languages.deleteSiteLanguage(siteID, languageID),
|
|
77
|
+
{
|
|
78
|
+
handleSuccess: () => refreshList(siteID, dispatch),
|
|
79
|
+
handleError: (response: unknown) => onError(response, dispatch),
|
|
80
|
+
},
|
|
81
|
+
[appActions.setIsLoading],
|
|
82
|
+
)(dispatch),
|
|
83
|
+
);
|
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import { setSocial } from "./slice";
|
|
2
|
+
import { getSocial, saveSocial } from "./thunks";
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
const socialActions = {
|
|
5
|
+
setSocial,
|
|
6
|
+
getSocial,
|
|
7
|
+
saveSocial,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export { socialActions };
|
|
11
|
+
|
|
12
|
+
export { socialInitialState, socialReducer } from "./slice";
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ISocialState } from "@ax/types";
|
|
2
|
+
|
|
3
|
+
import { createSlice, type PayloadAction } from "@reduxjs/toolkit";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The `social` area holds one site's social profile URLs (and the two things filed
|
|
7
|
+
* alongside them, newsletter and podcast) as a flat `{ network: url }` map —
|
|
8
|
+
* UC-AX-configure-social-networks. No types, order or icons: the screen is a plain
|
|
9
|
+
* form of URL text fields saved in a single PUT, and the set of fields comes from the
|
|
10
|
+
* instance config plus a hardcoded list, not from the state.
|
|
11
|
+
*
|
|
12
|
+
* `ISocialState` stays in `@ax/types` rather than moving next to the slice as
|
|
13
|
+
* `ILanguageState` and `IAnalyticsState` did: seven preview components use it as a
|
|
14
|
+
* prop type, so it is a shared domain type that happens to also be this area's state.
|
|
15
|
+
*
|
|
16
|
+
* STAYS in redux-persist, against the rule of ADR 0002: six page-editor screens read
|
|
17
|
+
* `state.social` to feed the preview canvas and none of them fetches — only
|
|
18
|
+
* `sites.setSiteInfo` writes it outside this screen, and that does not re-run on a hard
|
|
19
|
+
* refresh. The persist was their loader. Leaves in waves 2-4 with those consumers.
|
|
20
|
+
*/
|
|
21
|
+
export const initialState: ISocialState = {};
|
|
22
|
+
|
|
23
|
+
const socialSlice = createSlice({
|
|
24
|
+
name: "social",
|
|
25
|
+
initialState,
|
|
26
|
+
reducers: {
|
|
27
|
+
// REPLACES, it does not merge — the opposite of its `analytics` sibling. The
|
|
28
|
+
// legacy reducer returned `{ ...payload }` and the screen leans on it: since
|
|
29
|
+
// "removing" a network means emptying its field and saving, a key the server
|
|
30
|
+
// drops has to drop out of the store too.
|
|
31
|
+
setSocial(_state, action: PayloadAction<ISocialState>) {
|
|
32
|
+
return { ...action.payload };
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
export const { setSocial } = socialSlice.actions;
|
|
38
|
+
|
|
39
|
+
export { initialState as socialInitialState };
|
|
40
|
+
export const socialReducer = socialSlice.reducer;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { social } from "@ax/api";
|
|
2
|
+
import { appActions } from "@ax/containers/App";
|
|
3
|
+
import { handleRequest } from "@ax/helpers";
|
|
4
|
+
import type { IRootState, ISocialState } from "@ax/types";
|
|
5
|
+
|
|
6
|
+
import { createAsyncThunk } from "@reduxjs/toolkit";
|
|
7
|
+
import type { Dispatch } from "redux";
|
|
8
|
+
|
|
9
|
+
import { setSocial } from "./slice";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Read and write side of UC-AX-configure-social-networks, as createAsyncThunk.
|
|
13
|
+
*
|
|
14
|
+
* They keep calling `handleRequest` inside for the same reason the Languages pilot
|
|
15
|
+
* does: that helper owns the loading-flag and error-channel conventions of the whole
|
|
16
|
+
* codebase, and replacing it is a cross-cutting job — see docs/RTK-MIGRATION.md §5.
|
|
17
|
+
*
|
|
18
|
+
* Both keep reading the site from `sites.currentSiteInfo` instead of taking it as an
|
|
19
|
+
* argument, exactly as the legacy thunks did. This area has a single scope — whatever
|
|
20
|
+
* site is loaded — so there is nothing to make explicit the way Analytics had to with
|
|
21
|
+
* its global scope, and no call site pays the single-argument rule here.
|
|
22
|
+
*
|
|
23
|
+
* What does change: the legacy versions wrapped everything in a `try/catch` whose
|
|
24
|
+
* body was `console.log(e)`, swallowing failures. createAsyncThunk turns a throw into
|
|
25
|
+
* a `rejected` action instead, so the failure is observable.
|
|
26
|
+
*/
|
|
27
|
+
// Called through the namespace rather than destructured at module scope: a
|
|
28
|
+
// destructured reference is frozen at import time and cannot be spied on.
|
|
29
|
+
const onError = (response: unknown, dispatch: Dispatch) => appActions.handleError(response)(dispatch);
|
|
30
|
+
|
|
31
|
+
// `currentSiteInfo` is typed `ISite | null`, which the legacy thunks never had to face
|
|
32
|
+
// because they read it through an untyped `getState` — on a null site they built the
|
|
33
|
+
// url off `undefined` and threw, and their `try/catch` swallowed it. The screen only
|
|
34
|
+
// exists inside a loaded site, so the guard below is unreachable in practice; it is
|
|
35
|
+
// there so the impossible case cannot turn into a request to `/site/undefined/socials`.
|
|
36
|
+
const siteOf = (getState: () => unknown) => (getState() as IRootState).sites.currentSiteInfo;
|
|
37
|
+
|
|
38
|
+
export const getSocial = createAsyncThunk<boolean, void>("social/getSocial", async (_arg, { dispatch, getState }) => {
|
|
39
|
+
const site = siteOf(getState);
|
|
40
|
+
if (!site) return false;
|
|
41
|
+
|
|
42
|
+
return handleRequest(
|
|
43
|
+
async () => social.getSocial(site.id),
|
|
44
|
+
{
|
|
45
|
+
handleSuccess: (data: ISocialState) => dispatch(setSocial(data)),
|
|
46
|
+
// No error channel on the read side, as before: the failure only reaches the
|
|
47
|
+
// console and the form is left empty. Wart documented in the use case.
|
|
48
|
+
handleError: () => console.log("Error en getSocial"),
|
|
49
|
+
},
|
|
50
|
+
[appActions.setIsLoading],
|
|
51
|
+
)(dispatch);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
export const saveSocial = createAsyncThunk("social/saveSocial", async (form: ISocialState, { dispatch, getState }) => {
|
|
55
|
+
const site = siteOf(getState);
|
|
56
|
+
if (!site) return false;
|
|
57
|
+
|
|
58
|
+
return handleRequest(
|
|
59
|
+
// The whole map goes out in one PUT, emptied fields included — that is what
|
|
60
|
+
// makes clearing a field remove the network.
|
|
61
|
+
async () => social.updateSocial(site.id, form),
|
|
62
|
+
{
|
|
63
|
+
handleSuccess: (data: ISocialState) => dispatch(setSocial(data)),
|
|
64
|
+
handleError: (response: unknown) => onError(response, dispatch),
|
|
65
|
+
},
|
|
66
|
+
[appActions.setIsSaving],
|
|
67
|
+
)(dispatch);
|
|
68
|
+
});
|
|
@@ -9,6 +9,7 @@ import { socialActions } from "@ax/containers/Settings/Social";
|
|
|
9
9
|
import { structuredDataActions } from "@ax/containers/StructuredData";
|
|
10
10
|
import { usersActions } from "@ax/containers/Users";
|
|
11
11
|
import { getDefaultTheme, getThemeElements, handleRequest, isReqOk, sortBy } from "@ax/helpers";
|
|
12
|
+
import type { AppDispatch } from "@ax/store";
|
|
12
13
|
import type {
|
|
13
14
|
IGetGlobalPagesParams,
|
|
14
15
|
IGetSitePagesParams,
|
|
@@ -357,7 +358,7 @@ function setSiteInfo(currentSiteInfo: ISite): (dispatch: Dispatch, getState: ()
|
|
|
357
358
|
|
|
358
359
|
// get analytics
|
|
359
360
|
const analyticsResponse: any = await analytics.getAnalytics(currentSiteInfo.id);
|
|
360
|
-
if (isReqOk(
|
|
361
|
+
if (isReqOk(analyticsResponse.status)) {
|
|
361
362
|
dispatch(analyticsActions.setAnalytics(analyticsResponse.data));
|
|
362
363
|
} else {
|
|
363
364
|
console.log("Error en sites getAnalytics");
|
|
@@ -569,7 +570,10 @@ function resetSiteValues(siteID: number): (dispatch: Dispatch) => void {
|
|
|
569
570
|
resetMenuValues(dispatch);
|
|
570
571
|
dispatch(setCurrentSitePages([]));
|
|
571
572
|
dispatch(setTotalItems(0));
|
|
572
|
-
|
|
573
|
+
// `analytics` is already on RTK while `sites` is not (wave 3), so its thunk is
|
|
574
|
+
// dispatched, not invoked by hand: a createAsyncThunk expects the full
|
|
575
|
+
// (dispatch, getState, extra) triple. The cast is the seam between the two.
|
|
576
|
+
(dispatch as AppDispatch)(getAnalytics(siteID));
|
|
573
577
|
dispatch(setCurrentSearch(""));
|
|
574
578
|
dispatch(setThemeElements(null));
|
|
575
579
|
};
|
package/src/index.tsx
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import ReactDOM from "react-dom/client";
|
|
3
2
|
import { Provider } from "react-redux";
|
|
3
|
+
import { Router } from "react-router-dom";
|
|
4
|
+
|
|
5
|
+
import ReactDOM from "react-dom/client";
|
|
4
6
|
import { PersistGate } from "redux-persist/integration/react";
|
|
5
|
-
import history from "./routes/history";
|
|
6
|
-
import { ConnectedRouter } from "connected-react-router";
|
|
7
7
|
|
|
8
8
|
import { GlobalStore } from "./GlobalStore";
|
|
9
9
|
import App from "./modules/App";
|
|
10
|
+
import history from "./routes/history";
|
|
10
11
|
|
|
11
12
|
export class Main {
|
|
12
13
|
public static start(): void {
|
|
13
14
|
const globalStore = new GlobalStore();
|
|
14
|
-
const configStore = globalStore.configureStore(
|
|
15
|
+
const configStore = globalStore.configureStore();
|
|
15
16
|
|
|
16
17
|
const container = document.getElementById("root");
|
|
17
18
|
if (!container) throw new Error("Failed to find the root element");
|
|
@@ -19,11 +20,11 @@ export class Main {
|
|
|
19
20
|
|
|
20
21
|
root.render(
|
|
21
22
|
<Provider store={configStore.store}>
|
|
22
|
-
<
|
|
23
|
+
<Router history={history}>
|
|
23
24
|
<PersistGate loading={undefined} persistor={configStore.persistor}>
|
|
24
25
|
<App />
|
|
25
26
|
</PersistGate>
|
|
26
|
-
</
|
|
27
|
+
</Router>
|
|
27
28
|
</Provider>,
|
|
28
29
|
);
|
|
29
30
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type React from "react";
|
|
2
2
|
import { useState } from "react";
|
|
3
|
-
import { connect } from "react-redux";
|
|
4
3
|
|
|
5
4
|
import { Avatar, RestoreModal, Tag, TruncatedTooltip } from "@ax/components";
|
|
6
5
|
import { activityLogActions } from "@ax/containers/ActivityLog";
|
|
@@ -8,9 +7,9 @@ import { navigationActions } from "@ax/containers/Navigation";
|
|
|
8
7
|
import { pageEditorActions } from "@ax/containers/PageEditor";
|
|
9
8
|
import { structuredDataActions } from "@ax/containers/StructuredData";
|
|
10
9
|
import { useModal } from "@ax/hooks";
|
|
11
|
-
import
|
|
10
|
+
import { useAppDispatch, useAppSelector } from "@ax/store";
|
|
12
11
|
|
|
13
|
-
import type { LogActivityDTO
|
|
12
|
+
import type { LogActivityDTO } from "@griddo/api-types";
|
|
14
13
|
|
|
15
14
|
import { eventKey } from "../../constants";
|
|
16
15
|
import DetailModal from "../../DetailModal";
|
|
@@ -19,9 +18,12 @@ import { getContentTypeSchemaVersion, getPageSchemaVersion } from "../../utils";
|
|
|
19
18
|
import * as S from "./style";
|
|
20
19
|
|
|
21
20
|
const EventItem = (props: IEventItemProps) => {
|
|
22
|
-
const { item,
|
|
21
|
+
const { item, getLog } = props;
|
|
23
22
|
const { user, created, site, contentType, content, eventType } = item;
|
|
24
23
|
|
|
24
|
+
const dispatch = useAppDispatch();
|
|
25
|
+
const schemaVersion = useAppSelector((state) => state.structuredData.schemaVersion);
|
|
26
|
+
|
|
25
27
|
const { isOpen: isDetailOpen, toggleModal: toggleDetailModal } = useModal();
|
|
26
28
|
const { isOpen: isRestoreOpen, toggleModal: toggleRestoreModal } = useModal();
|
|
27
29
|
const [schemaChange, setSchemaChange] = useState(false);
|
|
@@ -32,14 +34,14 @@ const EventItem = (props: IEventItemProps) => {
|
|
|
32
34
|
switch (type) {
|
|
33
35
|
case eventKey.DELETED_PAGE:
|
|
34
36
|
case eventKey.DELETED_CONTENT_TYPE_PAGE:
|
|
35
|
-
return await restorePage(id);
|
|
37
|
+
return await dispatch(pageEditorActions.restorePage(id));
|
|
36
38
|
case eventKey.DELETED_HEADER:
|
|
37
|
-
return await restoreNavigation(id, "header");
|
|
39
|
+
return await dispatch(navigationActions.restoreNavigation(id, "header"));
|
|
38
40
|
case eventKey.DELETED_FOOTER:
|
|
39
|
-
return await restoreNavigation(id, "footer");
|
|
41
|
+
return await dispatch(navigationActions.restoreNavigation(id, "footer"));
|
|
40
42
|
case eventKey.DELETED_CATEGORY:
|
|
41
43
|
case eventKey.DELETED_CONTENT_TYPE:
|
|
42
|
-
return await
|
|
44
|
+
return await dispatch(structuredDataActions.restoreStructuredDataContent(id));
|
|
43
45
|
default:
|
|
44
46
|
return false;
|
|
45
47
|
}
|
|
@@ -83,12 +85,12 @@ const EventItem = (props: IEventItemProps) => {
|
|
|
83
85
|
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
84
86
|
e.stopPropagation();
|
|
85
87
|
const siteID = site?.id || null;
|
|
86
|
-
goToLogContent(contentType, content, siteID);
|
|
88
|
+
dispatch(activityLogActions.goToLogContent({ contentType, content, site: siteID }));
|
|
87
89
|
};
|
|
88
90
|
|
|
89
91
|
const handleKeyDown = () => {
|
|
90
92
|
const siteID = site?.id || null;
|
|
91
|
-
goToLogContent(contentType, content, siteID);
|
|
93
|
+
dispatch(activityLogActions.goToLogContent({ contentType, content, site: siteID }));
|
|
92
94
|
};
|
|
93
95
|
|
|
94
96
|
return (
|
|
@@ -152,23 +154,7 @@ const EventItem = (props: IEventItemProps) => {
|
|
|
152
154
|
|
|
153
155
|
interface IEventItemProps {
|
|
154
156
|
item: LogActivityDTO;
|
|
155
|
-
schemaVersion: string;
|
|
156
|
-
restorePage(id: number): Promise<boolean>;
|
|
157
|
-
restoreNavigation(navID: number, type: string): Promise<boolean>;
|
|
158
|
-
restoreStructuredData(dataID: number): Promise<boolean>;
|
|
159
|
-
goToLogContent(contentType: LogContentTypeDTO | null, content: LogContentDTO | null, site: number | null): void;
|
|
160
157
|
getLog(): void;
|
|
161
158
|
}
|
|
162
159
|
|
|
163
|
-
|
|
164
|
-
schemaVersion: state.structuredData.schemaVersion,
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
const mapDispatchToProps = {
|
|
168
|
-
restorePage: pageEditorActions.restorePage,
|
|
169
|
-
restoreNavigation: navigationActions.restoreNavigation,
|
|
170
|
-
restoreStructuredData: structuredDataActions.restoreStructuredDataContent,
|
|
171
|
-
goToLogContent: activityLogActions.goToLogContent,
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
export default connect(mapStateToProps, mapDispatchToProps)(EventItem);
|
|
160
|
+
export default EventItem;
|