@griddo/ax 12.6.1 → 12.6.2

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 (91) hide show
  1. package/config/griddo-config/index.js +0 -1
  2. package/package.json +4 -3
  3. package/src/GlobalStore.tsx +35 -91
  4. package/src/__tests__/hooks/heartbeat.test.tsx +88 -0
  5. package/src/components/MainWrapper/index.tsx +2 -14
  6. package/src/containers/ActivityLog/{thunks.ts → actions.tsx} +89 -104
  7. package/src/containers/ActivityLog/constants.tsx +6 -0
  8. package/src/containers/ActivityLog/index.tsx +3 -16
  9. package/src/containers/ActivityLog/interfaces.tsx +12 -0
  10. package/src/containers/ActivityLog/reducer.tsx +24 -0
  11. package/src/containers/Analytics/actions.tsx +109 -0
  12. package/src/containers/Analytics/constants.tsx +5 -0
  13. package/src/containers/Analytics/index.tsx +3 -14
  14. package/src/containers/Analytics/interfaces.tsx +9 -0
  15. package/src/containers/Analytics/reducer.tsx +28 -0
  16. package/src/containers/Domains/actions.tsx +58 -0
  17. package/src/containers/Domains/constants.tsx +5 -0
  18. package/src/containers/Domains/index.tsx +3 -12
  19. package/src/containers/Domains/interfaces.tsx +9 -0
  20. package/src/containers/Domains/reducer.tsx +22 -0
  21. package/src/containers/Redirects/actions.tsx +190 -0
  22. package/src/containers/Redirects/constants.tsx +17 -1
  23. package/src/containers/Redirects/index.tsx +3 -14
  24. package/src/containers/Redirects/interfaces.tsx +23 -0
  25. package/src/containers/Redirects/reducer.tsx +35 -0
  26. package/src/containers/Settings/Languages/actions.tsx +88 -0
  27. package/src/containers/Settings/Languages/constants.tsx +6 -0
  28. package/src/containers/Settings/Languages/index.tsx +3 -15
  29. package/src/containers/Settings/Languages/reducer.tsx +31 -0
  30. package/src/containers/Settings/Social/actions.tsx +63 -0
  31. package/src/containers/Settings/Social/constants.tsx +3 -0
  32. package/src/containers/Settings/Social/index.tsx +3 -11
  33. package/src/containers/Settings/Social/interfaces.tsx +9 -0
  34. package/src/containers/Settings/Social/reducer.tsx +18 -0
  35. package/src/containers/Sites/actions.tsx +2 -6
  36. package/src/hooks/heartbeat.ts +32 -0
  37. package/src/hooks/index.tsx +2 -0
  38. package/src/index.tsx +6 -7
  39. package/src/modules/ActivityLog/ItemLog/EventItem/index.tsx +27 -13
  40. package/src/modules/ActivityLog/ItemLogUser/UserItem/EventItem/index.tsx +27 -13
  41. package/src/modules/ActivityLog/index.tsx +51 -22
  42. package/src/modules/Analytics/index.tsx +42 -33
  43. package/src/modules/App/Routing/index.tsx +4 -5
  44. package/src/modules/FileDrive/index.tsx +1 -1
  45. package/src/modules/GlobalEditor/index.tsx +6 -8
  46. package/src/modules/GlobalSettings/Robots/index.tsx +47 -24
  47. package/src/modules/MediaGallery/index.tsx +1 -1
  48. package/src/modules/PageEditor/index.tsx +5 -9
  49. package/src/modules/Redirects/RedirectItem/index.tsx +22 -9
  50. package/src/modules/Redirects/RedirectPanel/index.tsx +14 -13
  51. package/src/modules/Redirects/index.tsx +69 -26
  52. package/src/modules/Settings/ContentTypes/DataPacks/index.tsx +3 -4
  53. package/src/modules/Settings/Languages/LanguagePanel/Form/ConnectedField/index.tsx +18 -11
  54. package/src/modules/Settings/Languages/LanguagePanel/Form/index.tsx +9 -7
  55. package/src/modules/Settings/Languages/LanguagePanel/index.tsx +44 -38
  56. package/src/modules/Settings/Languages/Table/Header/index.tsx +19 -12
  57. package/src/modules/Settings/Languages/Table/Item/index.tsx +36 -21
  58. package/src/modules/Settings/Languages/Table/index.tsx +14 -6
  59. package/src/modules/Settings/Languages/index.tsx +30 -18
  60. package/src/modules/Settings/SeoAnalyticsSettings/Analytics/index.tsx +52 -42
  61. package/src/modules/Settings/Social/index.tsx +12 -17
  62. package/src/modules/StructuredData/StructuredDataList/index.tsx +2 -3
  63. package/src/types/index.tsx +6 -5
  64. package/tsconfig.paths.json +0 -1
  65. package/src/__tests__/modules/ActivityLog/ActivityLog.uc.test.tsx +0 -132
  66. package/src/__tests__/modules/Analytics/Analytics.uc.test.tsx +0 -168
  67. package/src/__tests__/modules/GlobalSettings/Robots/Robots.test.tsx +0 -110
  68. package/src/__tests__/modules/GlobalSettings/Robots/Robots.uc.test.tsx +0 -125
  69. package/src/__tests__/modules/Redirects/Redirects.uc.test.tsx +0 -164
  70. package/src/__tests__/modules/Settings/Languages/Languages.uc.test.tsx +0 -213
  71. package/src/__tests__/modules/Settings/Social/Social.uc.test.tsx +0 -171
  72. package/src/__tests__/store/activityLog.test.ts +0 -301
  73. package/src/__tests__/store/analytics.test.ts +0 -134
  74. package/src/__tests__/store/domains.test.ts +0 -107
  75. package/src/__tests__/store/languages.test.ts +0 -129
  76. package/src/__tests__/store/persist.test.ts +0 -47
  77. package/src/__tests__/store/redirects.test.ts +0 -313
  78. package/src/__tests__/store/social.test.ts +0 -165
  79. package/src/containers/ActivityLog/slice.ts +0 -45
  80. package/src/containers/Analytics/slice.ts +0 -48
  81. package/src/containers/Analytics/thunks.ts +0 -103
  82. package/src/containers/Domains/slice.ts +0 -37
  83. package/src/containers/Domains/thunks.ts +0 -60
  84. package/src/containers/Redirects/slice.ts +0 -60
  85. package/src/containers/Redirects/thunks.ts +0 -196
  86. package/src/containers/Settings/Languages/slice.ts +0 -48
  87. package/src/containers/Settings/Languages/thunks.ts +0 -83
  88. package/src/containers/Settings/Social/slice.ts +0 -40
  89. package/src/containers/Settings/Social/thunks.ts +0 -68
  90. package/src/store/hooks.ts +0 -37
  91. package/src/store/index.ts +0 -2
@@ -1,165 +0,0 @@
1
- import { social } from "@ax/api";
2
- import { appActions } from "@ax/containers/App";
3
- import { socialActions, socialReducer } from "@ax/containers/Settings";
4
- import { type ISitesState, sitesInitialState, sitesReducer } from "@ax/containers/Sites/reducer";
5
- import type { ISite, ISocialState } from "@ax/types";
6
-
7
- import { configureStore, type Reducer, type UnknownAction } from "@reduxjs/toolkit";
8
- import type { MockInstance } from "vitest";
9
-
10
- /**
11
- * Characterisation tests for the `social` area — the per-site social profile URLs
12
- * behind `/sites/settings/social` (UC-AX-configure-social-networks).
13
- *
14
- * Written against the PUBLIC surface (the store and the exported action creators),
15
- * never against reducer internals or action type constants, so this file must keep
16
- * passing unchanged once the area becomes a slice. If it needs an edit, the shape
17
- * changed and that is a finding, not an adjustment.
18
- *
19
- * Two behaviours here are load-bearing and easy to lose in a rewrite:
20
- *
21
- * - `setSocial` REPLACES the map, it does not merge — the opposite of its
22
- * `analytics` sibling. The legacy reducer returned `{ ...payload }`, and the
23
- * screen depends on it: "removing" a network means emptying its field and
24
- * saving, so a key the server drops has to drop out of the store too.
25
- * - the site is read from `sites.currentSiteInfo`, not passed in, which is why
26
- * that reducer is mounted below. It is the seam this area has with `sites`.
27
- *
28
- * Going to the slice needed exactly two edits, and both are type-level or known:
29
- *
30
- * - the two `expect(saved)` assertions gained `.unwrap()`. `dispatch(thunk())` used
31
- * to resolve to `handleRequest`'s boolean; a createAsyncThunk resolves to the
32
- * fulfilled action and hands the value over through `.unwrap()`. Same finding as
33
- * Domains — docs/RTK-MIGRATION.md §5.
34
- * - the casts that let a redux 4 reducer and action creator into an RTK store went
35
- * away on their own (§8.3). `sites` still needs its own, being still legacy.
36
- *
37
- * Every request, resulting state and error-channel assertion below came over from the
38
- * legacy run untouched. No argument churn: both thunks already took exactly one
39
- * argument, so createAsyncThunk's single-argument rule cost nothing here.
40
- */
41
-
42
- const SITE_ID = 42;
43
-
44
- const SOCIALS: ISocialState = {
45
- instagram: "https://instagram.com/griddo",
46
- twitter: "https://x.com/griddo",
47
- };
48
-
49
- // Mirrors how GlobalStore mounts this area, minus persistence. `sites` is still
50
- // legacy, so it needs the cast GlobalStore uses — RTK types reducers against redux 5's
51
- // UnknownAction (docs/RTK-MIGRATION.md §8.3).
52
- const makeStore = () =>
53
- configureStore({
54
- reducer: {
55
- social: socialReducer,
56
- sites: sitesReducer as Reducer<ISitesState, UnknownAction>,
57
- },
58
- preloadedState: {
59
- sites: { ...sitesInitialState, currentSiteInfo: { id: SITE_ID } as ISite },
60
- },
61
- });
62
-
63
- type Store = ReturnType<typeof makeStore>;
64
- const socialsOf = (store: Store) => store.getState().social;
65
-
66
- const ok = (data: ISocialState) => ({ status: 200, data });
67
-
68
- describe("[UC-AX-configure-social-networks] social state", () => {
69
- let store: Store;
70
- let handleError: MockInstance;
71
-
72
- beforeEach(() => {
73
- store = makeStore();
74
- handleError = vi.spyOn(appActions, "handleError").mockReturnValue((() => undefined) as never);
75
- });
76
-
77
- afterEach(() => {
78
- vi.restoreAllMocks();
79
- });
80
-
81
- it("AC2 · starts with no networks configured", () => {
82
- expect(socialsOf(store)).toEqual({});
83
- });
84
-
85
- it("AC2 · setSocial replaces the whole map instead of merging onto it", () => {
86
- store.dispatch(socialActions.setSocial(SOCIALS));
87
-
88
- store.dispatch(socialActions.setSocial({ instagram: SOCIALS.instagram }));
89
-
90
- // `twitter` is gone, not kept: the payload is the server's whole map.
91
- expect(socialsOf(store)).toEqual({ instagram: SOCIALS.instagram });
92
- });
93
-
94
- it("AC2 · loading asks the current site for its networks and stores them", async () => {
95
- const get = vi.spyOn(social, "getSocial").mockResolvedValue(ok(SOCIALS) as never);
96
-
97
- await store.dispatch(socialActions.getSocial());
98
-
99
- expect(get).toHaveBeenCalledWith(SITE_ID);
100
- expect(socialsOf(store)).toEqual(SOCIALS);
101
- });
102
-
103
- it("AC2 · a failed load leaves the map untouched and logs instead of showing a toast", async () => {
104
- store.dispatch(socialActions.setSocial(SOCIALS));
105
- vi.spyOn(social, "getSocial").mockResolvedValue({ status: 500, data: {} } as never);
106
- // Asserted rather than merely allowed: the read side's ONLY error channel is this
107
- // console.log, so spying on it both pins the wart and keeps it out of the suite output.
108
- const log = vi.spyOn(console, "log").mockImplementation(() => undefined);
109
-
110
- await store.dispatch(socialActions.getSocial());
111
-
112
- // The failure reaches the console and nothing else, so the screen keeps whatever it had.
113
- // Documented as a wart in the UC: a failed load is invisible to the user.
114
- expect(log).toHaveBeenCalledWith("Error en getSocial");
115
- expect(handleError).not.toHaveBeenCalled();
116
- expect(socialsOf(store)).toEqual(SOCIALS);
117
- });
118
- });
119
-
120
- describe("[UC-AX-configure-social-networks] social writes", () => {
121
- let store: Store;
122
- let handleError: MockInstance;
123
-
124
- beforeEach(() => {
125
- store = makeStore();
126
- handleError = vi.spyOn(appActions, "handleError").mockReturnValue((() => undefined) as never);
127
- });
128
-
129
- afterEach(() => {
130
- vi.restoreAllMocks();
131
- });
132
-
133
- it("AC6 · saving puts the whole form to the current site and takes the response as the new state", async () => {
134
- const update = vi.spyOn(social, "updateSocial").mockResolvedValue(ok(SOCIALS) as never);
135
-
136
- const saved = await store.dispatch(socialActions.saveSocial(SOCIALS)).unwrap();
137
-
138
- expect(update).toHaveBeenCalledWith(SITE_ID, SOCIALS);
139
- expect(saved).toBe(true);
140
- expect(socialsOf(store)).toEqual(SOCIALS);
141
- });
142
-
143
- it("AC6 · an emptied network goes out as an empty string — that is how one is removed", async () => {
144
- const form = { ...SOCIALS, twitter: "" };
145
- const update = vi.spyOn(social, "updateSocial").mockResolvedValue(ok(form) as never);
146
-
147
- await store.dispatch(socialActions.saveSocial(form));
148
-
149
- expect(update).toHaveBeenCalledWith(SITE_ID, form);
150
- expect(socialsOf(store)).toEqual(form);
151
- });
152
-
153
- it("AC6 · a failed save routes through the error channel and reports it did not save", async () => {
154
- const response = { status: 400, data: { message: "Invalid URL" } };
155
- vi.spyOn(social, "updateSocial").mockResolvedValue(response as never);
156
-
157
- const saved = await store.dispatch(socialActions.saveSocial(SOCIALS)).unwrap();
158
-
159
- expect(handleError).toHaveBeenCalledWith(response);
160
- // The screen keys `resetDirty()` off this boolean, so losing it means the form
161
- // silently forgets unsaved changes.
162
- expect(saved).toBe(false);
163
- expect(socialsOf(store)).toEqual({});
164
- });
165
- });
@@ -1,45 +0,0 @@
1
- import type { ILogItemsByDay, ILogItemsByUser } from "@ax/types";
2
-
3
- import { createSlice, type PayloadAction } from "@reduxjs/toolkit";
4
-
5
- /**
6
- * The `activityLog` area holds ONE page of the audit trail plus how it is grouped
7
- * (UC-AX-view-activity-log). There is a single slot for the page: switching between the
8
- * timeline and the grouped-by-user view overwrites it, which is why the screen refetches
9
- * whenever the mode changes.
10
- *
11
- * `activityLog` is a union because the two endpoints answer with differently shaped groups —
12
- * by day or by user — and the view picks its renderer off `listMode`, not off the data.
13
- *
14
- * Out of redux-persist since this migration (ADR 0002).
15
- */
16
- export interface IActivityLogState {
17
- activityLog: ILogItemsByDay | ILogItemsByUser;
18
- listMode: string;
19
- }
20
-
21
- export const initialState: IActivityLogState = {
22
- activityLog: { totalItems: 0, items: [] },
23
- listMode: "default",
24
- };
25
-
26
- const activityLogSlice = createSlice({
27
- name: "activityLog",
28
- initialState,
29
- // The legacy reducer had both action types share one `return { ...state, ...payload }`
30
- // branch, which worked because each payload carried exactly one key. One reducer per field
31
- // says the same thing without the indirection.
32
- reducers: {
33
- setActivityLog(state, action: PayloadAction<ILogItemsByDay | ILogItemsByUser>) {
34
- state.activityLog = action.payload;
35
- },
36
- setListMode(state, action: PayloadAction<string>) {
37
- state.listMode = action.payload;
38
- },
39
- },
40
- });
41
-
42
- export const { setActivityLog, setListMode } = activityLogSlice.actions;
43
-
44
- export { initialState as activityLogInitialState };
45
- export const activityLogReducer = activityLogSlice.reducer;
@@ -1,48 +0,0 @@
1
- import type { IAnalytics, IDimension, IDimensionsGroup } from "@ax/types";
2
-
3
- import { createSlice, type PayloadAction } from "@reduxjs/toolkit";
4
-
5
- /**
6
- * The `analytics` area holds the tracking script and the Data Layer definition
7
- * (dimensions and their groups) of whatever scope is loaded — global or one site
8
- * (UC-AX-configure-seo-analytics). There is a single slot: loading a site
9
- * overwrites what global left, which is why the two settings screens reload on mount.
10
- *
11
- * STAYS in redux-persist, against the rule of ADR 0002: `AnalyticsField` (the page
12
- * editor's SEO/analytics field) reads `groups` from here and never fetches, so the
13
- * persist is what feeds it after a hard refresh. It decides which of two different
14
- * fields to render, so an empty `groups` is not a flash but the wrong editor. Leaves
15
- * when PageEditor migrates (wave 4).
16
- */
17
- export interface IAnalyticsState {
18
- scriptCode: string;
19
- siteScriptCodeExists: boolean;
20
- dimensions: IDimension[];
21
- groups: IDimensionsGroup[];
22
- }
23
-
24
- export const initialState: IAnalyticsState = {
25
- scriptCode: "",
26
- siteScriptCodeExists: false,
27
- dimensions: [],
28
- groups: [],
29
- };
30
-
31
- const analyticsSlice = createSlice({
32
- name: "analytics",
33
- initialState,
34
- reducers: {
35
- // MERGES, it does not replace: the legacy reducer spread the payload onto the
36
- // state and `Sites/actions.tsx` dispatches this with whatever the site's
37
- // metrics endpoint returned, which need not carry every field. Typed as a
38
- // partial to say so out loud.
39
- setAnalytics(state, action: PayloadAction<Partial<IAnalytics>>) {
40
- Object.assign(state, action.payload);
41
- },
42
- },
43
- });
44
-
45
- export const { setAnalytics } = analyticsSlice.actions;
46
-
47
- export { initialState as analyticsInitialState };
48
- export const analyticsReducer = analyticsSlice.reducer;
@@ -1,103 +0,0 @@
1
- import { analytics } from "@ax/api";
2
- import { appActions } from "@ax/containers/App";
3
- import { handleRequest } from "@ax/helpers";
4
- import type { IAnalytics } from "@ax/types";
5
-
6
- import { createAsyncThunk } from "@reduxjs/toolkit";
7
- import type { Dispatch } from "redux";
8
-
9
- import { setAnalytics } from "./slice";
10
-
11
- /**
12
- * Read and write side of UC-AX-configure-seo-analytics, as createAsyncThunk. They
13
- * keep calling `handleRequest` inside — see the Languages pilot and §5 of
14
- * docs/RTK-MIGRATION.md.
15
- *
16
- * `siteId` is `null` for the global scope, which the API spells `"global"`. The
17
- * legacy signatures made the argument optional; createAsyncThunk takes exactly one
18
- * argument, so the scope is now always explicit at the call site. That is the whole
19
- * of the churn this migration pushes onto the callers.
20
- */
21
- const onError = (response: unknown, dispatch: Dispatch, isMultiple = false, message?: string) =>
22
- appActions.handleError(response, isMultiple, message)(dispatch);
23
-
24
- const scopeOf = (siteId: number | null) => siteId ?? "global";
25
-
26
- export const getAnalytics = createAsyncThunk("analytics/getAnalytics", async (siteId: number | null, { dispatch }) =>
27
- handleRequest(
28
- async () => analytics.getAnalytics(scopeOf(siteId)),
29
- {
30
- handleSuccess: (data: Partial<IAnalytics>) => dispatch(setAnalytics(data)),
31
- handleError: (response: unknown) => onError(response, dispatch),
32
- },
33
- [],
34
- )(dispatch),
35
- );
36
-
37
- interface IUpdateAnalyticsArgs {
38
- analytics: IAnalytics;
39
- siteId: number | null;
40
- }
41
-
42
- export const updateAnalytics = createAsyncThunk(
43
- "analytics/updateAnalytics",
44
- async ({ analytics: state, siteId }: IUpdateAnalyticsArgs, { dispatch }) => {
45
- const { scriptCode, dimensions, groups } = state;
46
- const scope = scopeOf(siteId);
47
-
48
- return handleRequest(
49
- // The three writes go out in PARALLEL and `handleRequest` treats the array
50
- // of responses as a whole: any one of them failing fails the save.
51
- async () =>
52
- Promise.all([
53
- analytics.updateScriptCode(scope, { scriptCode }),
54
- analytics.createDimensions(scope, { dimensions }),
55
- analytics.createDimensionsGroups(scope, { groups }),
56
- ]),
57
- {
58
- // Not awaited, as the legacy version did not await it either: the button
59
- // stops saying "Saving" before the reload lands.
60
- handleSuccess: () => dispatch(getAnalytics(siteId)),
61
- handleError: (response: { data: { message?: string | string[] } }) => {
62
- const { message } = response.data;
63
- const isMultiple = Array.isArray(message) && message.length > 1;
64
- const msg = isMultiple ? `The action failed due to ${message.length} errors.` : undefined;
65
- onError(response, dispatch, isMultiple, msg);
66
- },
67
- },
68
- [appActions.setIsSaving],
69
- )(dispatch);
70
- },
71
- );
72
-
73
- interface IUpdateScriptCodeArgs {
74
- scriptCode: string;
75
- siteId: number | null;
76
- }
77
-
78
- export const updateScriptCode = createAsyncThunk(
79
- "analytics/updateScriptCode",
80
- async ({ scriptCode, siteId }: IUpdateScriptCodeArgs, { dispatch }) =>
81
- handleRequest(
82
- async () => analytics.updateScriptCode(scopeOf(siteId), { scriptCode }),
83
- {
84
- handleSuccess: () => dispatch(getAnalytics(siteId)),
85
- handleError: (response: unknown) => onError(response, dispatch),
86
- },
87
- [appActions.setIsSaving],
88
- )(dispatch),
89
- );
90
-
91
- export const deleteScriptCode = createAsyncThunk(
92
- "analytics/deleteScriptCode",
93
- // No loading flag, as before: resetting to the global defaults shows no spinner.
94
- async (siteId: number | null, { dispatch }) =>
95
- handleRequest(
96
- async () => analytics.deleteScriptCode(scopeOf(siteId)),
97
- {
98
- handleSuccess: () => dispatch(getAnalytics(siteId)),
99
- handleError: (response: unknown) => onError(response, dispatch),
100
- },
101
- [],
102
- )(dispatch),
103
- );
@@ -1,37 +0,0 @@
1
- import type { IDomainRobot } from "@ax/types";
2
-
3
- import { createSlice, type PayloadAction } from "@reduxjs/toolkit";
4
-
5
- /**
6
- * The `domains` area holds the robots.txt of every domain (UC-AX-configure-seo-analytics,
7
- * `/settings/robots`). Despite the name it is not a domain manager: the list of
8
- * domains a site has is read through `sites`, and this slice only carries what the
9
- * robots.txt editor renders.
10
- *
11
- * Out of redux-persist since this migration (ADR 0002): the screen calls
12
- * `getDomainsRobots()` on mount, so the persisted copy was never read.
13
- */
14
- export interface IDomainsState {
15
- robots: IDomainRobot[];
16
- }
17
-
18
- export const initialState: IDomainsState = {
19
- robots: [],
20
- };
21
-
22
- const domainsSlice = createSlice({
23
- name: "domains",
24
- initialState,
25
- reducers: {
26
- // The legacy action wrapped the list in `{ robots }` to spread it onto the
27
- // state; as a reducer the payload is just the list.
28
- setDomainsRobots(state, action: PayloadAction<IDomainRobot[]>) {
29
- state.robots = action.payload;
30
- },
31
- },
32
- });
33
-
34
- export const { setDomainsRobots } = domainsSlice.actions;
35
-
36
- export { initialState as domainsInitialState };
37
- export const domainsReducer = domainsSlice.reducer;
@@ -1,60 +0,0 @@
1
- import { domains } from "@ax/api";
2
- import { appActions } from "@ax/containers/App";
3
- import { handleRequest } from "@ax/helpers";
4
- import type { IDomainRobot } from "@ax/types";
5
-
6
- import { createAsyncThunk } from "@reduxjs/toolkit";
7
- import type { Dispatch } from "redux";
8
-
9
- import { setDomainsRobots } from "./slice";
10
-
11
- /**
12
- * Read and write side of the robots.txt editor, as createAsyncThunk.
13
- *
14
- * They keep calling `handleRequest` inside for the same reason the Languages
15
- * pilot does: that helper owns the loading-flag and error-channel conventions of
16
- * the whole codebase (152 call-sites) and replacing it is a cross-cutting job,
17
- * not this area's — see docs/RTK-MIGRATION.md §5.
18
- *
19
- * What does change: the legacy versions wrapped everything in a `try/catch` whose
20
- * body was `console.log(e)`, swallowing failures. createAsyncThunk turns a throw
21
- * into a `rejected` action instead, so the failure is observable.
22
- */
23
- // Called through the namespace rather than destructured at module scope: a
24
- // destructured reference is frozen at import time and cannot be spied on, which
25
- // would make the reload-after-save behaviour untestable.
26
- const onError = (response: unknown, dispatch: Dispatch, isMultiple = false, message?: string) =>
27
- appActions.handleError(response, isMultiple, message)(dispatch);
28
-
29
- export const getDomainsRobots = createAsyncThunk<boolean, void>(
30
- "domains/getDomainsRobots",
31
- async (_arg, { dispatch }) =>
32
- handleRequest(
33
- async () => domains.getDomainsRobots(),
34
- {
35
- handleSuccess: (robots: IDomainRobot[]) => dispatch(setDomainsRobots(robots)),
36
- handleError: (response: unknown) => onError(response, dispatch),
37
- },
38
- [appActions.setIsLoading],
39
- )(dispatch),
40
- );
41
-
42
- export const updateDomainsRobots = createAsyncThunk(
43
- "domains/updateDomainsRobots",
44
- async (robots: IDomainRobot[], { dispatch }) =>
45
- handleRequest(
46
- async () => domains.updateDomainsRobots(robots),
47
- {
48
- // Reloading from the server is what makes `computedContent` (the
49
- // inherited rules, read-only) reflect the content just saved.
50
- handleSuccess: () => dispatch(getDomainsRobots()),
51
- handleError: (response: { data: { message?: string | string[] } }) => {
52
- const { message } = response.data;
53
- const isMultiple = Array.isArray(message) && message.length > 1;
54
- const msg = isMultiple ? `The action failed due to ${message.length} errors.` : undefined;
55
- onError(response, dispatch, isMultiple, msg);
56
- },
57
- },
58
- [appActions.setIsSaving],
59
- )(dispatch),
60
- );
@@ -1,60 +0,0 @@
1
- import type { IRedirect } from "@ax/types";
2
-
3
- import { createSlice, type PayloadAction } from "@reduxjs/toolkit";
4
-
5
- /**
6
- * The `redirects` area holds ONE page of the redirect catalogue plus the result of the
7
- * last CSV import check (UC-AX-manage-redirects). Paging, searching and filtering live in
8
- * the view, so the list is whatever page was asked for last and `totalItems` is what the
9
- * pager counts with.
10
- *
11
- * The four setters stay internal to the area: nothing outside dispatches them — the legacy
12
- * `actions.tsx` did not export them either — so the barrel keeps them private and the
13
- * public surface is just the five thunks.
14
- *
15
- * Out of redux-persist since this migration (ADR 0002).
16
- */
17
- export interface IRedirectsState {
18
- redirects: IRedirect[];
19
- totalItems: number;
20
- totalImports: number;
21
- imports: null | {
22
- error: IRedirect[];
23
- existing: IRedirect[];
24
- ok: IRedirect[];
25
- };
26
- }
27
-
28
- export const initialState: IRedirectsState = {
29
- redirects: [],
30
- totalItems: 0,
31
- totalImports: 0,
32
- imports: null,
33
- };
34
-
35
- const redirectsSlice = createSlice({
36
- name: "redirects",
37
- initialState,
38
- // The legacy reducer had all four action types share one `return { ...state, ...payload }`
39
- // branch, which worked because each payload carried exactly one key. One reducer per
40
- // field says the same thing without the indirection.
41
- reducers: {
42
- setRedirects(state, action: PayloadAction<IRedirect[]>) {
43
- state.redirects = action.payload;
44
- },
45
- setTotalItems(state, action: PayloadAction<number>) {
46
- state.totalItems = action.payload;
47
- },
48
- setImports(state, action: PayloadAction<IRedirectsState["imports"]>) {
49
- state.imports = action.payload;
50
- },
51
- setTotalImports(state, action: PayloadAction<number>) {
52
- state.totalImports = action.payload;
53
- },
54
- },
55
- });
56
-
57
- export const { setRedirects, setTotalItems, setImports, setTotalImports } = redirectsSlice.actions;
58
-
59
- export { initialState as redirectsInitialState };
60
- export const redirectsReducer = redirectsSlice.reducer;