@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.
- package/config/griddo-config/index.js +0 -1
- package/package.json +4 -3
- package/src/GlobalStore.tsx +35 -91
- package/src/__tests__/hooks/heartbeat.test.tsx +88 -0
- package/src/components/MainWrapper/index.tsx +2 -14
- package/src/containers/ActivityLog/{thunks.ts → actions.tsx} +89 -104
- package/src/containers/ActivityLog/constants.tsx +6 -0
- package/src/containers/ActivityLog/index.tsx +3 -16
- package/src/containers/ActivityLog/interfaces.tsx +12 -0
- package/src/containers/ActivityLog/reducer.tsx +24 -0
- package/src/containers/Analytics/actions.tsx +109 -0
- package/src/containers/Analytics/constants.tsx +5 -0
- package/src/containers/Analytics/index.tsx +3 -14
- package/src/containers/Analytics/interfaces.tsx +9 -0
- package/src/containers/Analytics/reducer.tsx +28 -0
- package/src/containers/Domains/actions.tsx +58 -0
- package/src/containers/Domains/constants.tsx +5 -0
- package/src/containers/Domains/index.tsx +3 -12
- package/src/containers/Domains/interfaces.tsx +9 -0
- package/src/containers/Domains/reducer.tsx +22 -0
- package/src/containers/Redirects/actions.tsx +190 -0
- package/src/containers/Redirects/constants.tsx +17 -1
- package/src/containers/Redirects/index.tsx +3 -14
- package/src/containers/Redirects/interfaces.tsx +23 -0
- package/src/containers/Redirects/reducer.tsx +35 -0
- package/src/containers/Settings/Languages/actions.tsx +88 -0
- package/src/containers/Settings/Languages/constants.tsx +6 -0
- package/src/containers/Settings/Languages/index.tsx +3 -15
- package/src/containers/Settings/Languages/reducer.tsx +31 -0
- package/src/containers/Settings/Social/actions.tsx +63 -0
- package/src/containers/Settings/Social/constants.tsx +3 -0
- package/src/containers/Settings/Social/index.tsx +3 -11
- package/src/containers/Settings/Social/interfaces.tsx +9 -0
- package/src/containers/Settings/Social/reducer.tsx +18 -0
- package/src/containers/Sites/actions.tsx +2 -6
- package/src/hooks/heartbeat.ts +32 -0
- package/src/hooks/index.tsx +2 -0
- package/src/index.tsx +6 -7
- package/src/modules/ActivityLog/ItemLog/EventItem/index.tsx +27 -13
- package/src/modules/ActivityLog/ItemLogUser/UserItem/EventItem/index.tsx +27 -13
- package/src/modules/ActivityLog/index.tsx +51 -22
- package/src/modules/Analytics/index.tsx +42 -33
- package/src/modules/App/Routing/index.tsx +4 -5
- package/src/modules/FileDrive/index.tsx +1 -1
- package/src/modules/GlobalEditor/index.tsx +6 -8
- package/src/modules/GlobalSettings/Robots/index.tsx +47 -24
- package/src/modules/MediaGallery/index.tsx +1 -1
- package/src/modules/PageEditor/index.tsx +5 -9
- package/src/modules/Redirects/RedirectItem/index.tsx +22 -9
- package/src/modules/Redirects/RedirectPanel/index.tsx +14 -13
- package/src/modules/Redirects/index.tsx +69 -26
- package/src/modules/Settings/ContentTypes/DataPacks/index.tsx +3 -4
- package/src/modules/Settings/Languages/LanguagePanel/Form/ConnectedField/index.tsx +18 -11
- package/src/modules/Settings/Languages/LanguagePanel/Form/index.tsx +9 -7
- package/src/modules/Settings/Languages/LanguagePanel/index.tsx +44 -38
- package/src/modules/Settings/Languages/Table/Header/index.tsx +19 -12
- package/src/modules/Settings/Languages/Table/Item/index.tsx +36 -21
- package/src/modules/Settings/Languages/Table/index.tsx +14 -6
- package/src/modules/Settings/Languages/index.tsx +30 -18
- package/src/modules/Settings/SeoAnalyticsSettings/Analytics/index.tsx +52 -42
- package/src/modules/Settings/Social/index.tsx +12 -17
- package/src/modules/StructuredData/StructuredDataList/index.tsx +2 -3
- package/src/types/index.tsx +6 -5
- package/tsconfig.paths.json +0 -1
- package/src/__tests__/modules/ActivityLog/ActivityLog.uc.test.tsx +0 -132
- package/src/__tests__/modules/Analytics/Analytics.uc.test.tsx +0 -168
- package/src/__tests__/modules/GlobalSettings/Robots/Robots.test.tsx +0 -110
- package/src/__tests__/modules/GlobalSettings/Robots/Robots.uc.test.tsx +0 -125
- package/src/__tests__/modules/Redirects/Redirects.uc.test.tsx +0 -164
- package/src/__tests__/modules/Settings/Languages/Languages.uc.test.tsx +0 -213
- package/src/__tests__/modules/Settings/Social/Social.uc.test.tsx +0 -171
- package/src/__tests__/store/activityLog.test.ts +0 -301
- package/src/__tests__/store/analytics.test.ts +0 -134
- package/src/__tests__/store/domains.test.ts +0 -107
- package/src/__tests__/store/languages.test.ts +0 -129
- package/src/__tests__/store/persist.test.ts +0 -47
- package/src/__tests__/store/redirects.test.ts +0 -313
- package/src/__tests__/store/social.test.ts +0 -165
- package/src/containers/ActivityLog/slice.ts +0 -45
- package/src/containers/Analytics/slice.ts +0 -48
- package/src/containers/Analytics/thunks.ts +0 -103
- package/src/containers/Domains/slice.ts +0 -37
- package/src/containers/Domains/thunks.ts +0 -60
- package/src/containers/Redirects/slice.ts +0 -60
- package/src/containers/Redirects/thunks.ts +0 -196
- package/src/containers/Settings/Languages/slice.ts +0 -48
- package/src/containers/Settings/Languages/thunks.ts +0 -83
- package/src/containers/Settings/Social/slice.ts +0 -40
- package/src/containers/Settings/Social/thunks.ts +0 -68
- package/src/store/hooks.ts +0 -37
- 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;
|