@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.
Files changed (86) hide show
  1. package/config/griddo-config/index.js +1 -0
  2. package/package.json +3 -4
  3. package/src/GlobalStore.tsx +91 -35
  4. package/src/__tests__/modules/ActivityLog/ActivityLog.uc.test.tsx +132 -0
  5. package/src/__tests__/modules/Analytics/Analytics.uc.test.tsx +168 -0
  6. package/src/__tests__/modules/GlobalSettings/Robots/Robots.test.tsx +110 -0
  7. package/src/__tests__/modules/GlobalSettings/Robots/Robots.uc.test.tsx +125 -0
  8. package/src/__tests__/modules/Redirects/Redirects.uc.test.tsx +164 -0
  9. package/src/__tests__/modules/Settings/Languages/Languages.uc.test.tsx +213 -0
  10. package/src/__tests__/modules/Settings/Social/Social.uc.test.tsx +171 -0
  11. package/src/__tests__/store/activityLog.test.ts +301 -0
  12. package/src/__tests__/store/analytics.test.ts +134 -0
  13. package/src/__tests__/store/domains.test.ts +107 -0
  14. package/src/__tests__/store/languages.test.ts +129 -0
  15. package/src/__tests__/store/persist.test.ts +47 -0
  16. package/src/__tests__/store/redirects.test.ts +313 -0
  17. package/src/__tests__/store/social.test.ts +165 -0
  18. package/src/components/MainWrapper/index.tsx +14 -2
  19. package/src/containers/ActivityLog/index.tsx +16 -3
  20. package/src/containers/ActivityLog/slice.ts +45 -0
  21. package/src/containers/ActivityLog/{actions.tsx → thunks.ts} +104 -89
  22. package/src/containers/Analytics/index.tsx +14 -3
  23. package/src/containers/Analytics/slice.ts +48 -0
  24. package/src/containers/Analytics/thunks.ts +103 -0
  25. package/src/containers/Domains/index.tsx +12 -3
  26. package/src/containers/Domains/slice.ts +37 -0
  27. package/src/containers/Domains/thunks.ts +60 -0
  28. package/src/containers/Redirects/constants.tsx +1 -17
  29. package/src/containers/Redirects/index.tsx +14 -3
  30. package/src/containers/Redirects/slice.ts +60 -0
  31. package/src/containers/Redirects/thunks.ts +196 -0
  32. package/src/containers/Settings/Languages/index.tsx +15 -3
  33. package/src/containers/Settings/Languages/slice.ts +48 -0
  34. package/src/containers/Settings/Languages/thunks.ts +83 -0
  35. package/src/containers/Settings/Social/index.tsx +11 -3
  36. package/src/containers/Settings/Social/slice.ts +40 -0
  37. package/src/containers/Settings/Social/thunks.ts +68 -0
  38. package/src/containers/Sites/actions.tsx +6 -2
  39. package/src/index.tsx +7 -6
  40. package/src/modules/ActivityLog/ItemLog/EventItem/index.tsx +13 -27
  41. package/src/modules/ActivityLog/ItemLogUser/UserItem/EventItem/index.tsx +13 -27
  42. package/src/modules/ActivityLog/index.tsx +22 -51
  43. package/src/modules/Analytics/index.tsx +33 -42
  44. package/src/modules/App/Routing/index.tsx +5 -4
  45. package/src/modules/FileDrive/index.tsx +1 -1
  46. package/src/modules/GlobalSettings/Robots/index.tsx +24 -47
  47. package/src/modules/MediaGallery/index.tsx +1 -1
  48. package/src/modules/Redirects/RedirectItem/index.tsx +9 -22
  49. package/src/modules/Redirects/RedirectPanel/index.tsx +13 -14
  50. package/src/modules/Redirects/index.tsx +26 -69
  51. package/src/modules/Settings/ContentTypes/DataPacks/index.tsx +4 -3
  52. package/src/modules/Settings/Languages/LanguagePanel/Form/ConnectedField/index.tsx +11 -18
  53. package/src/modules/Settings/Languages/LanguagePanel/Form/index.tsx +7 -9
  54. package/src/modules/Settings/Languages/LanguagePanel/index.tsx +38 -44
  55. package/src/modules/Settings/Languages/Table/Header/index.tsx +12 -19
  56. package/src/modules/Settings/Languages/Table/Item/index.tsx +21 -36
  57. package/src/modules/Settings/Languages/Table/index.tsx +6 -14
  58. package/src/modules/Settings/Languages/index.tsx +18 -30
  59. package/src/modules/Settings/SeoAnalyticsSettings/Analytics/index.tsx +42 -52
  60. package/src/modules/Settings/Social/index.tsx +17 -12
  61. package/src/modules/StructuredData/StructuredDataList/index.tsx +3 -2
  62. package/src/store/hooks.ts +37 -0
  63. package/src/store/index.ts +2 -0
  64. package/src/types/index.tsx +5 -6
  65. package/tsconfig.paths.json +1 -0
  66. package/src/containers/ActivityLog/constants.tsx +0 -6
  67. package/src/containers/ActivityLog/interfaces.tsx +0 -12
  68. package/src/containers/ActivityLog/reducer.tsx +0 -24
  69. package/src/containers/Analytics/actions.tsx +0 -109
  70. package/src/containers/Analytics/constants.tsx +0 -5
  71. package/src/containers/Analytics/interfaces.tsx +0 -9
  72. package/src/containers/Analytics/reducer.tsx +0 -28
  73. package/src/containers/Domains/actions.tsx +0 -58
  74. package/src/containers/Domains/constants.tsx +0 -5
  75. package/src/containers/Domains/interfaces.tsx +0 -9
  76. package/src/containers/Domains/reducer.tsx +0 -22
  77. package/src/containers/Redirects/actions.tsx +0 -190
  78. package/src/containers/Redirects/interfaces.tsx +0 -23
  79. package/src/containers/Redirects/reducer.tsx +0 -35
  80. package/src/containers/Settings/Languages/actions.tsx +0 -88
  81. package/src/containers/Settings/Languages/constants.tsx +0 -6
  82. package/src/containers/Settings/Languages/reducer.tsx +0 -31
  83. package/src/containers/Settings/Social/actions.tsx +0 -63
  84. package/src/containers/Settings/Social/constants.tsx +0 -3
  85. package/src/containers/Settings/Social/interfaces.tsx +0 -9
  86. 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 * as languagesActions from "./actions";
2
- import { languagesReducer } from "./reducer";
1
+ import { resetForm, setForm, updateFormValue } from "./slice";
2
+ import { createSiteLanguage, deleteSiteLanguage, updateSiteLanguage } from "./thunks";
3
3
 
4
- export { languagesActions, languagesReducer };
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 * as socialActions from "./actions";
2
- import { socialReducer } from "./reducer";
1
+ import { setSocial } from "./slice";
2
+ import { getSocial, saveSocial } from "./thunks";
3
3
 
4
- export { socialActions, socialReducer };
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(socialResponse.status)) {
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
- getAnalytics(siteID)(dispatch);
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(history);
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
- <ConnectedRouter history={history}>
23
+ <Router history={history}>
23
24
  <PersistGate loading={undefined} persistor={configStore.persistor}>
24
25
  <App />
25
26
  </PersistGate>
26
- </ConnectedRouter>
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 type { IRootState } from "@ax/types";
10
+ import { useAppDispatch, useAppSelector } from "@ax/store";
12
11
 
13
- import type { LogActivityDTO, LogContentDTO, LogContentTypeDTO } from "@griddo/api-types";
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, schemaVersion, restorePage, restoreNavigation, restoreStructuredData, goToLogContent, getLog } = props;
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 restoreStructuredData(id);
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
- const mapStateToProps = (state: IRootState) => ({
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;