@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,76 +1,61 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { connect } from "react-redux";
|
|
2
3
|
|
|
3
4
|
import { Loading, MainWrapper, Nav } from "@ax/components";
|
|
4
5
|
import { analyticsActions } from "@ax/containers/Analytics";
|
|
5
6
|
import { appActions } from "@ax/containers/App";
|
|
6
7
|
import { RouteLeavingGuard } from "@ax/guards";
|
|
7
8
|
import { useModal } from "@ax/hooks";
|
|
8
|
-
import {
|
|
9
|
-
import type { INavItem } from "@ax/types";
|
|
9
|
+
import type { IAnalytics, INavItem, IRootState, ISite } from "@ax/types";
|
|
10
10
|
|
|
11
11
|
import { Dimensions, Groups, ResetModal, ScriptCode, Warning } from "./atoms";
|
|
12
12
|
|
|
13
13
|
import * as S from "./style";
|
|
14
14
|
|
|
15
15
|
const Analytics = (props: IProps): JSX.Element => {
|
|
16
|
-
const {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
16
|
+
const {
|
|
17
|
+
navItems,
|
|
18
|
+
currentNavItem,
|
|
19
|
+
isSaving,
|
|
20
|
+
isLoading,
|
|
21
|
+
analytics,
|
|
22
|
+
getAnalytics,
|
|
23
|
+
setHistoryPush,
|
|
24
|
+
site,
|
|
25
|
+
updateScriptCode,
|
|
26
|
+
deleteScriptCode,
|
|
27
|
+
} = props;
|
|
23
28
|
|
|
24
29
|
const [showWarning, setShowWarning] = useState(false);
|
|
25
30
|
const [scriptCode, setScriptCode] = useState(analytics.scriptCode);
|
|
26
31
|
const [isDirty, setIsDirty] = useState(false);
|
|
27
|
-
const [hasLoaded, setHasLoaded] = useState(false);
|
|
28
32
|
const { isOpen, toggleModal } = useModal();
|
|
29
33
|
|
|
30
|
-
// biome-ignore lint/correctness/useExhaustiveDependencies:
|
|
34
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: TODO: fix this
|
|
31
35
|
useEffect(() => {
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
setHasLoaded(await dispatch(analyticsActions.getAnalytics(site?.id ?? null)).unwrap());
|
|
35
|
-
} catch {
|
|
36
|
-
setHasLoaded(false);
|
|
37
|
-
}
|
|
38
|
-
};
|
|
39
|
-
load();
|
|
36
|
+
const handleGetAnalytics = async () => await getAnalytics(site?.id);
|
|
37
|
+
handleGetAnalytics();
|
|
40
38
|
}, []);
|
|
41
39
|
|
|
42
|
-
// The Warning ("this site has no script code of its own, edit the global one") is a
|
|
43
|
-
// claim about the server, and `siteScriptCodeExists` is `false` both when the site has
|
|
44
|
-
// no override and when nothing has been fetched yet. Making the claim before the GET
|
|
45
|
-
// answers told a site that HAS its own script that it had none — and `getAnalytics`
|
|
46
|
-
// sets no loading flag, so nothing covered the gap. Hence the guard: until the GET
|
|
47
|
-
// succeeds the editor is the neutral state, and on failure the error modal is already up.
|
|
48
|
-
useEffect(() => {
|
|
49
|
-
if (!hasLoaded) return;
|
|
50
|
-
setShowWarning(!analytics.siteScriptCodeExists);
|
|
51
|
-
}, [hasLoaded, analytics.siteScriptCodeExists]);
|
|
52
|
-
|
|
53
40
|
useEffect(() => {
|
|
54
41
|
setScriptCode(analytics.scriptCode);
|
|
42
|
+
setShowWarning(!analytics.siteScriptCodeExists);
|
|
55
43
|
setIsDirty(false);
|
|
56
|
-
}, [analytics.scriptCode]);
|
|
44
|
+
}, [analytics.scriptCode, analytics.siteScriptCodeExists]);
|
|
57
45
|
|
|
58
46
|
useEffect(() => {
|
|
59
47
|
setIsDirty(scriptCode !== analytics.scriptCode);
|
|
60
48
|
}, [scriptCode, analytics.scriptCode]);
|
|
61
49
|
|
|
62
50
|
const handleSave = async () => {
|
|
63
|
-
|
|
64
|
-
// resolves to the fulfilled action, which would be truthy even on failure.
|
|
65
|
-
const isSaved =
|
|
66
|
-
!!site && (await dispatch(analyticsActions.updateScriptCode({ scriptCode, siteId: site.id })).unwrap());
|
|
51
|
+
const isSaved = !!site && (await updateScriptCode(scriptCode, site.id));
|
|
67
52
|
if (isSaved) setIsDirty(false);
|
|
68
53
|
};
|
|
69
54
|
|
|
70
|
-
const setRoute = (path: string) =>
|
|
55
|
+
const setRoute = (path: string) => setHistoryPush(path);
|
|
71
56
|
|
|
72
57
|
const handleReset = async () => {
|
|
73
|
-
await
|
|
58
|
+
await deleteScriptCode(site?.id);
|
|
74
59
|
isOpen && toggleModal();
|
|
75
60
|
setShowWarning(true);
|
|
76
61
|
setScriptCode(analytics.scriptCode);
|
|
@@ -121,7 +106,6 @@ const Analytics = (props: IProps): JSX.Element => {
|
|
|
121
106
|
<MainWrapper
|
|
122
107
|
backLink={false}
|
|
123
108
|
title="Analytics Settings"
|
|
124
|
-
screen="analytics-site"
|
|
125
109
|
rightButton={rightButtonProps}
|
|
126
110
|
rightLineButton={secondaryButtonProps}
|
|
127
111
|
>
|
|
@@ -135,10 +119,36 @@ const Analytics = (props: IProps): JSX.Element => {
|
|
|
135
119
|
);
|
|
136
120
|
};
|
|
137
121
|
|
|
138
|
-
|
|
122
|
+
const mapStateToProps = (state: IRootState) => ({
|
|
123
|
+
isSaving: state.app.isSaving,
|
|
124
|
+
isLoading: state.app.isLoading,
|
|
125
|
+
analytics: state.analytics,
|
|
126
|
+
site: state.sites.currentSiteInfo,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const mapDispatchToProps = {
|
|
130
|
+
setHistoryPush: appActions.setHistoryPush,
|
|
131
|
+
getAnalytics: analyticsActions.getAnalytics,
|
|
132
|
+
updateScriptCode: analyticsActions.updateScriptCode,
|
|
133
|
+
deleteScriptCode: analyticsActions.deleteScriptCode,
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
interface IAnalyticsProps {
|
|
139
137
|
navItems: INavItem[];
|
|
140
138
|
currentNavItem: INavItem;
|
|
139
|
+
isSaving: boolean;
|
|
140
|
+
isLoading: boolean;
|
|
141
|
+
analytics: IAnalytics;
|
|
142
|
+
site: ISite | null;
|
|
141
143
|
}
|
|
142
144
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
+
interface IDispatchProps {
|
|
146
|
+
getAnalytics(siteId?: number | null): Promise<void>;
|
|
147
|
+
setHistoryPush(path: string, isEditor?: boolean): void;
|
|
148
|
+
updateScriptCode(scriptCode: string, siteId?: number | null): Promise<boolean>;
|
|
149
|
+
deleteScriptCode(siteId?: number | null): Promise<boolean>;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
type IProps = IAnalyticsProps & IDispatchProps;
|
|
153
|
+
|
|
154
|
+
export default connect(mapStateToProps, mapDispatchToProps)(Analytics);
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
//
|
|
28
28
|
|
|
29
29
|
import { useEffect, useState } from "react";
|
|
30
|
+
import { useDispatch, useSelector } from "react-redux";
|
|
30
31
|
|
|
31
32
|
import { ErrorToast, FieldsBehavior, MainWrapper, Nav } from "@ax/components";
|
|
32
33
|
import { appActions } from "@ax/containers/App";
|
|
@@ -34,10 +35,11 @@ import { socialActions } from "@ax/containers/Settings/Social";
|
|
|
34
35
|
import { RouteLeavingGuard } from "@ax/guards";
|
|
35
36
|
import { capitalize } from "@ax/helpers";
|
|
36
37
|
import { useIsDirty } from "@ax/hooks";
|
|
37
|
-
import {
|
|
38
|
-
import type { INavItem } from "@ax/types";
|
|
38
|
+
import type { INavItem, IRootState } from "@ax/types";
|
|
39
39
|
|
|
40
40
|
import { config } from "components";
|
|
41
|
+
import type { AnyAction } from "redux";
|
|
42
|
+
import type { ThunkDispatch } from "redux-thunk";
|
|
41
43
|
|
|
42
44
|
import * as S from "./style";
|
|
43
45
|
|
|
@@ -62,12 +64,12 @@ const hardcodedSocials = [
|
|
|
62
64
|
|
|
63
65
|
const Social = (props: IProps): JSX.Element => {
|
|
64
66
|
const { navItems, currentNavItem } = props;
|
|
65
|
-
const dispatch =
|
|
66
|
-
const isSaving =
|
|
67
|
-
const socialInfo =
|
|
67
|
+
const dispatch = useDispatch<ThunkDispatch<IRootState, undefined, AnyAction>>();
|
|
68
|
+
const isSaving = useSelector((state: IRootState) => state.app.isSaving);
|
|
69
|
+
const socialInfo = useSelector((state: IRootState) => state.social);
|
|
68
70
|
const [socialForm, setSocialForm] = useState<Record<string, string>>({});
|
|
69
71
|
const [serverFields, setServerFields] = useState<Set<string>>(new Set());
|
|
70
|
-
const { isDirty, resetDirty } = useIsDirty(socialForm);
|
|
72
|
+
const { isDirty, resetDirty, setIsDirty } = useIsDirty(socialForm);
|
|
71
73
|
|
|
72
74
|
const socialFields = [
|
|
73
75
|
...instanceSocials.map((name) => ({ name, title: `${capitalize(name)} URL` })),
|
|
@@ -86,21 +88,14 @@ const Social = (props: IProps): JSX.Element => {
|
|
|
86
88
|
resetDirty();
|
|
87
89
|
}, [dispatch]);
|
|
88
90
|
|
|
89
|
-
// `resetDirty()`, not `setIsDirty(false)`: it also moves useIsDirty's baseline to the
|
|
90
|
-
// form being hydrated here. Leaving the redux-persist (ADR 0002) means `social` is
|
|
91
|
-
// empty on the first render and arrives with the GET, and useIsDirty snapshots the
|
|
92
|
-
// baseline during render — so the empty→loaded hop read as an edit and the screen
|
|
93
|
-
// landed dirty, arming the leave guard before the user touched anything.
|
|
94
91
|
useEffect(() => {
|
|
95
92
|
setSocialForm({ ...socialInfo });
|
|
96
93
|
setServerFields(new Set(Object.keys(socialInfo).filter((k) => !!socialInfo[k])));
|
|
97
|
-
|
|
98
|
-
}, [socialInfo,
|
|
94
|
+
setIsDirty(false);
|
|
95
|
+
}, [socialInfo, setIsDirty]);
|
|
99
96
|
|
|
100
97
|
const saveForm = async () => {
|
|
101
|
-
|
|
102
|
-
// resolves to the fulfilled action, which would be truthy even on failure.
|
|
103
|
-
const isSaved = await dispatch(socialActions.saveSocial(socialForm)).unwrap();
|
|
98
|
+
const isSaved = await dispatch(socialActions.saveSocial(socialForm));
|
|
104
99
|
if (isSaved) resetDirty();
|
|
105
100
|
};
|
|
106
101
|
|
|
@@ -127,7 +122,7 @@ const Social = (props: IProps): JSX.Element => {
|
|
|
127
122
|
return (
|
|
128
123
|
<>
|
|
129
124
|
<RouteLeavingGuard when={isDirty} action={setRoute} text={modalText} />
|
|
130
|
-
<MainWrapper backLink={false} title="Social" rightButton={rightButtonProps}
|
|
125
|
+
<MainWrapper backLink={false} title="Social" rightButton={rightButtonProps}>
|
|
131
126
|
<S.Wrapper>
|
|
132
127
|
<Nav current={currentNavItem} items={navItems} onClick={handleMenuClick} />
|
|
133
128
|
<S.ContentWrapper>
|
|
@@ -315,8 +315,7 @@ const StructuredDataList = (props: IProps): JSX.Element => {
|
|
|
315
315
|
// biome-ignore lint/correctness/useExhaustiveDependencies: TODO: fix this
|
|
316
316
|
useEffect(() => {
|
|
317
317
|
checkUserSession();
|
|
318
|
-
|
|
319
|
-
getAnalytics(null);
|
|
318
|
+
getAnalytics();
|
|
320
319
|
}, []);
|
|
321
320
|
|
|
322
321
|
// biome-ignore lint/correctness/useExhaustiveDependencies: TODO: fix this
|
|
@@ -812,7 +811,7 @@ interface IDispatchProps {
|
|
|
812
811
|
deleteBulk(ids: number[]): Promise<boolean>;
|
|
813
812
|
restorePage(id: number | number[]): Promise<boolean>;
|
|
814
813
|
resetPageEditor(): Promise<void>;
|
|
815
|
-
getAnalytics(siteId
|
|
814
|
+
getAnalytics(siteId?: number | null): Promise<void>;
|
|
816
815
|
setCurrentDataID(id: number | null): void;
|
|
817
816
|
resetCurrentSiteErrorPages: () => Promise<void>;
|
|
818
817
|
setContentFilters(contentFilters: Record<string, Record<string, IQueryValue[]>> | null): void;
|
package/src/types/index.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { IActivityLogState } from "@ax/containers/ActivityLog/
|
|
2
|
-
import type { IAnalyticsState } from "@ax/containers/Analytics/
|
|
1
|
+
import type { IActivityLogState } from "@ax/containers/ActivityLog/reducer";
|
|
2
|
+
import type { IAnalyticsState } from "@ax/containers/Analytics/reducer";
|
|
3
3
|
import type { IAppState } from "@ax/containers/App/reducer";
|
|
4
|
-
import type { IDomainsState } from "@ax/containers/Domains/
|
|
4
|
+
import type { IDomainsState } from "@ax/containers/Domains/reducer";
|
|
5
5
|
import type { IFileDriveState } from "@ax/containers/FileDrive/reducer";
|
|
6
6
|
import type { IFormsState } from "@ax/containers/Forms/reducer";
|
|
7
7
|
import type { IGalleryState } from "@ax/containers/Gallery/reducer";
|
|
@@ -9,9 +9,9 @@ import type { IIntegrationsState } from "@ax/containers/Integrations/reducer";
|
|
|
9
9
|
import type { INavigationState } from "@ax/containers/Navigation/Defaults/reducer";
|
|
10
10
|
import type { IMenuState } from "@ax/containers/Navigation/Menu/reducer";
|
|
11
11
|
import type { IPageEditorState } from "@ax/containers/PageEditor/reducer";
|
|
12
|
-
import type { IRedirectsState } from "@ax/containers/Redirects/
|
|
12
|
+
import type { IRedirectsState } from "@ax/containers/Redirects/reducer";
|
|
13
13
|
import type { IDataPacksState } from "@ax/containers/Settings/DataPacks/reducer";
|
|
14
|
-
import type { ILanguageState } from "@ax/containers/Settings/Languages/
|
|
14
|
+
import type { ILanguageState } from "@ax/containers/Settings/Languages/reducer";
|
|
15
15
|
import type { ISitesState } from "@ax/containers/Sites/reducer";
|
|
16
16
|
import type { IStructuredDataState } from "@ax/containers/StructuredData/reducer";
|
|
17
17
|
import type { IUsersState } from "@ax/containers/Users/reducer";
|
|
@@ -261,6 +261,7 @@ export interface IHeadingField {
|
|
|
261
261
|
}
|
|
262
262
|
|
|
263
263
|
export interface IRootState {
|
|
264
|
+
router: any;
|
|
264
265
|
app: IAppState;
|
|
265
266
|
sites: ISitesState;
|
|
266
267
|
pageEditor: IPageEditorState;
|
package/tsconfig.paths.json
CHANGED
|
@@ -17,7 +17,6 @@
|
|
|
17
17
|
"@ax/routes/*": ["./src/routes/*"],
|
|
18
18
|
"@ax/types": ["./src/types"],
|
|
19
19
|
"@ax/schemas": ["./src/schemas"],
|
|
20
|
-
"@ax/store": ["./src/store"],
|
|
21
20
|
"@ax/themes/*": ["./src/themes/*"],
|
|
22
21
|
"@ax/constants": ["./src/constants"],
|
|
23
22
|
"@ax/constants/*": ["./src/constants/*"]
|
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
import { activityLogReducer } from "@ax/containers/ActivityLog";
|
|
2
|
-
import { appReducer, type IAppState } from "@ax/containers/App/reducer";
|
|
3
|
-
import {
|
|
4
|
-
type IStructuredDataState,
|
|
5
|
-
structuredDataReducer,
|
|
6
|
-
} from "@ax/containers/StructuredData/reducer";
|
|
7
|
-
import { parseTheme } from "@ax/helpers";
|
|
8
|
-
import ActivityLog from "@ax/modules/ActivityLog";
|
|
9
|
-
import history from "@ax/routes/history";
|
|
10
|
-
import globalTheme from "@ax/themes/theme.json";
|
|
11
|
-
|
|
12
|
-
import { configureStore, type Reducer, type UnknownAction } from "@reduxjs/toolkit";
|
|
13
|
-
import axios from "axios";
|
|
14
|
-
import { Router } from "react-router-dom";
|
|
15
|
-
import { ThemeProvider } from "styled-components";
|
|
16
|
-
import type { MockedFunction } from "vitest";
|
|
17
|
-
|
|
18
|
-
import { act, cleanup, render, screen } from "../../../../config/tests/test-utils";
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* End-to-end test of the activity log on the real store — view, thunks, api layer and slice,
|
|
22
|
-
* with only axios mocked (see the `## Profile` section of docs/gainup/README.md for why a
|
|
23
|
-
* `redux-mock-store` test does not count as AC coverage).
|
|
24
|
-
*
|
|
25
|
-
* It covers the two things the store tests cannot see: that landing on the screen really does
|
|
26
|
-
* ask the grouped-by-day endpoint for a hundred rows and paint them (AC1), and the empty state
|
|
27
|
-
* (AC5), which is view logic living nowhere else.
|
|
28
|
-
*
|
|
29
|
-
* AC2 — switching to "Grouped by user" — is verified in `src/__tests__/store/activityLog.test.ts`
|
|
30
|
-
* instead: the mode selector is a react-select, and driving its menu open in jsdom would test
|
|
31
|
-
* the dropdown rather than the use case. The endpoint switch and the persisted `listMode` are
|
|
32
|
-
* the parts that matter and they are pinned there.
|
|
33
|
-
*/
|
|
34
|
-
vi.mock("axios");
|
|
35
|
-
const mockedAxios = axios as MockedFunction<typeof axios>;
|
|
36
|
-
|
|
37
|
-
const LOG_PAGE = {
|
|
38
|
-
totalItems: 1,
|
|
39
|
-
items: [
|
|
40
|
-
{
|
|
41
|
-
date: "10 Aug 2026",
|
|
42
|
-
items: [
|
|
43
|
-
{
|
|
44
|
-
id: 1,
|
|
45
|
-
user: { id: 3, name: "Ada Lovelace", image: null, email: "ada@example.edu", role: "admin" },
|
|
46
|
-
created: { hour: "09:41", date: "10 Aug 2026" },
|
|
47
|
-
site: null,
|
|
48
|
-
content: null,
|
|
49
|
-
contentType: null,
|
|
50
|
-
eventType: { id: 1, name: "Created page", internal_key: "CREATED_PAGE" },
|
|
51
|
-
ip: "10.0.0.1",
|
|
52
|
-
},
|
|
53
|
-
],
|
|
54
|
-
},
|
|
55
|
-
],
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
const makeStore = () =>
|
|
59
|
-
configureStore({
|
|
60
|
-
// Only what this screen reads. `app` and `structuredData` are still legacy, so they need
|
|
61
|
-
// the cast GlobalStore uses (docs/RTK-MIGRATION.md §8.3).
|
|
62
|
-
reducer: {
|
|
63
|
-
activityLog: activityLogReducer,
|
|
64
|
-
app: appReducer as Reducer<IAppState, UnknownAction>,
|
|
65
|
-
structuredData: structuredDataReducer as Reducer<IStructuredDataState, UnknownAction>,
|
|
66
|
-
},
|
|
67
|
-
middleware: (getDefault) => getDefault({ serializableCheck: false, immutableCheck: false }),
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
const answer = (data: unknown) => ({ status: 200, data, statusText: "OK", headers: {}, config: {} });
|
|
71
|
-
|
|
72
|
-
const logCalls = () =>
|
|
73
|
-
mockedAxios.mock.calls
|
|
74
|
-
.map(([config]) => config as { url?: string; method?: string })
|
|
75
|
-
.filter((config) => (config?.url ?? "").includes("/logs/activity-grouped-day"));
|
|
76
|
-
|
|
77
|
-
const renderScreen = async (store: ReturnType<typeof makeStore>) => {
|
|
78
|
-
await act(async () => {
|
|
79
|
-
render(
|
|
80
|
-
<Router history={history}>
|
|
81
|
-
<ThemeProvider theme={parseTheme(globalTheme)}>
|
|
82
|
-
<ActivityLog />
|
|
83
|
-
</ThemeProvider>
|
|
84
|
-
</Router>,
|
|
85
|
-
{ store },
|
|
86
|
-
);
|
|
87
|
-
});
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
// The screen's filters and the structured-data refresh fetch their own lists on mount, so only
|
|
91
|
-
// the log endpoint answers with a log page.
|
|
92
|
-
const respondWith = (page: unknown) => {
|
|
93
|
-
mockedAxios.mockImplementation((async (config: { url?: string }) => {
|
|
94
|
-
const url = config?.url ?? "";
|
|
95
|
-
if (url.includes("/logs/activity-grouped-day")) return answer(page);
|
|
96
|
-
// The filter option lists want a bare array, not a paginated envelope.
|
|
97
|
-
return answer([]);
|
|
98
|
-
}) as never);
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
afterEach(() => {
|
|
102
|
-
vi.restoreAllMocks();
|
|
103
|
-
cleanup();
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
describe("[UC-AX-view-activity-log] the activity log screen end to end", () => {
|
|
107
|
-
it("AC1 · landing on the screen asks the grouped-by-day endpoint for a hundred rows and paints them", async () => {
|
|
108
|
-
respondWith(LOG_PAGE);
|
|
109
|
-
const store = makeStore();
|
|
110
|
-
|
|
111
|
-
await renderScreen(store);
|
|
112
|
-
|
|
113
|
-
const [request] = logCalls();
|
|
114
|
-
expect(request.url).toContain("page=1");
|
|
115
|
-
expect(request.url).toContain("itemsPerPage=100");
|
|
116
|
-
// Default ordering is DESC, which the endpoint expresses by the absence of `order`.
|
|
117
|
-
expect(request.url).not.toContain("order=");
|
|
118
|
-
expect(store.getState().activityLog.activityLog.totalItems).toBe(1);
|
|
119
|
-
expect(screen.getByText("Ada Lovelace")).toBeTruthy();
|
|
120
|
-
expect(screen.getByText("09:41")).toBeTruthy();
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it("AC5 · an empty log shows the default empty state instead of a table", async () => {
|
|
124
|
-
respondWith({ totalItems: 0, items: [] });
|
|
125
|
-
|
|
126
|
-
await renderScreen(makeStore());
|
|
127
|
-
|
|
128
|
-
expect(logCalls()).toHaveLength(1);
|
|
129
|
-
expect(screen.getByText(/You will start to see the User Activity Log/)).toBeTruthy();
|
|
130
|
-
expect(screen.queryByText("Ada Lovelace")).toBeNull();
|
|
131
|
-
});
|
|
132
|
-
});
|
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
import { Router } from "react-router-dom";
|
|
2
|
-
|
|
3
|
-
// From the slice, not the `@ax/containers/Analytics` barrel: `Sites/actions` destructures
|
|
4
|
-
// `analyticsActions` at module scope, so pulling the barrel in first from a test that also
|
|
5
|
-
// mounts a screen loses the race with that cycle and the destructure sees `undefined`.
|
|
6
|
-
import { analyticsReducer } from "@ax/containers/Analytics/slice";
|
|
7
|
-
import { appInitialState, appReducer, type IAppState } from "@ax/containers/App/reducer";
|
|
8
|
-
import { type ISitesState, sitesInitialState, sitesReducer } from "@ax/containers/Sites/reducer";
|
|
9
|
-
import { parseTheme } from "@ax/helpers";
|
|
10
|
-
import GlobalAnalytics from "@ax/modules/Analytics";
|
|
11
|
-
import SiteAnalytics from "@ax/modules/Settings/SeoAnalyticsSettings/Analytics";
|
|
12
|
-
import history from "@ax/routes/history";
|
|
13
|
-
import globalTheme from "@ax/themes/theme.json";
|
|
14
|
-
import type { IAnalytics, INavItem, ISite } from "@ax/types";
|
|
15
|
-
|
|
16
|
-
import { configureStore, type Reducer, type UnknownAction } from "@reduxjs/toolkit";
|
|
17
|
-
import axios from "axios";
|
|
18
|
-
import { ThemeProvider } from "styled-components";
|
|
19
|
-
import type { MockedFunction } from "vitest";
|
|
20
|
-
|
|
21
|
-
import { act, cleanup, render, screen } from "../../../../config/tests/test-utils";
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* End-to-end tests of the two screens of the `analytics` area on the real store — view,
|
|
25
|
-
* thunks, api layer and slice, with only axios mocked (see the `## Profile` section of
|
|
26
|
-
* docs/gainup/README.md for why a `redux-mock-store` test does not count as AC coverage).
|
|
27
|
-
*
|
|
28
|
-
* They cover the half of AC5 and AC6 that lives only in the view, and both were written
|
|
29
|
-
* against bugs the review of this migration found: the global screen landed dirty, so Save
|
|
30
|
-
* was enabled and the leave guard armed on a screen the user had only looked at; and the
|
|
31
|
-
* site screen claimed "this site has no script code of its own" for as long as the GET was
|
|
32
|
-
* in flight, which is a statement about the server made before asking it.
|
|
33
|
-
*
|
|
34
|
-
* The in-flight window is the whole point of the second one, so its mock hands back a
|
|
35
|
-
* promise this file resolves by hand: awaiting the render would skip the state under test.
|
|
36
|
-
*/
|
|
37
|
-
vi.mock("axios");
|
|
38
|
-
const mockedAxios = axios as MockedFunction<typeof axios>;
|
|
39
|
-
|
|
40
|
-
const SITE_ID = 7;
|
|
41
|
-
const ANALYTICS: IAnalytics = {
|
|
42
|
-
scriptCode: "<script>site-ga</script>",
|
|
43
|
-
siteScriptCodeExists: true,
|
|
44
|
-
dimensions: [],
|
|
45
|
-
groups: [],
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
const mockComponent = () => null;
|
|
49
|
-
const NAV_ITEMS: INavItem[] = [{ title: "Analytics", path: "/sites/settings/analytics", component: mockComponent }];
|
|
50
|
-
|
|
51
|
-
const okResponse = (data: unknown) => ({ status: 200, data, statusText: "OK", headers: {}, config: {} });
|
|
52
|
-
|
|
53
|
-
const makeStore = () =>
|
|
54
|
-
configureStore({
|
|
55
|
-
// Only what these screens read. `app` and `sites` are still legacy, so they need the
|
|
56
|
-
// cast GlobalStore uses (docs/RTK-MIGRATION.md §8.3).
|
|
57
|
-
reducer: {
|
|
58
|
-
app: appReducer as Reducer<IAppState, UnknownAction>,
|
|
59
|
-
analytics: analyticsReducer,
|
|
60
|
-
sites: sitesReducer as Reducer<ISitesState, UnknownAction>,
|
|
61
|
-
},
|
|
62
|
-
middleware: (getDefault) => getDefault({ serializableCheck: false, immutableCheck: false }),
|
|
63
|
-
preloadedState: {
|
|
64
|
-
// `appInitialState.isLoading` is `true` and `getAnalytics` passes no loading flag,
|
|
65
|
-
// so a bare store would spin forever. `false` is the state any screen is actually
|
|
66
|
-
// reached in: the login and the boot fetches have already released the counter.
|
|
67
|
-
app: { ...appInitialState, isLoading: false },
|
|
68
|
-
sites: { ...sitesInitialState, currentSiteInfo: { id: SITE_ID } as ISite },
|
|
69
|
-
},
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
const metricsCalls = (method: string) =>
|
|
73
|
-
mockedAxios.mock.calls
|
|
74
|
-
.map(([config]) => config as { url?: string; method?: string })
|
|
75
|
-
.filter((config) => config?.method === method && !!config?.url?.includes("/metrics"));
|
|
76
|
-
|
|
77
|
-
const renderScreen = async (store: ReturnType<typeof makeStore>, ui: JSX.Element) => {
|
|
78
|
-
await act(async () => {
|
|
79
|
-
render(
|
|
80
|
-
<Router history={history}>
|
|
81
|
-
<ThemeProvider theme={parseTheme(globalTheme)}>{ui}</ThemeProvider>
|
|
82
|
-
</Router>,
|
|
83
|
-
{ store },
|
|
84
|
-
);
|
|
85
|
-
});
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
const siteScreen = <SiteAnalytics navItems={NAV_ITEMS} currentNavItem={NAV_ITEMS[0]} />;
|
|
89
|
-
|
|
90
|
-
const button = (name: string) => screen.getByRole("button", { name });
|
|
91
|
-
// The Warning panel's own button — it is what tells the site it has no override.
|
|
92
|
-
const warning = () => screen.queryByRole("button", { name: "Define Site Analytics" });
|
|
93
|
-
|
|
94
|
-
beforeEach(() => {
|
|
95
|
-
mockedAxios.mockImplementation((async () => okResponse(ANALYTICS)) as never);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
afterEach(() => {
|
|
99
|
-
vi.restoreAllMocks();
|
|
100
|
-
cleanup();
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
describe("[UC-AX-configure-seo-analytics] the global Analytics screen end to end", () => {
|
|
104
|
-
it("AC6 · lands clean on the data it just fetched, so Save stays disabled", async () => {
|
|
105
|
-
const store = makeStore();
|
|
106
|
-
|
|
107
|
-
await renderScreen(store, <GlobalAnalytics />);
|
|
108
|
-
|
|
109
|
-
expect(metricsCalls("GET")).toHaveLength(1);
|
|
110
|
-
expect(store.getState().analytics.scriptCode).toBe(ANALYTICS.scriptCode);
|
|
111
|
-
// The empty→loaded hop is not an edit. useIsDirty snapshots its baseline during
|
|
112
|
-
// render, so without the resetDirty() in the hydration effect this reads "Save"
|
|
113
|
-
// enabled — and the leave guard fires on the way out of a screen nobody touched.
|
|
114
|
-
expect(button("Saved").hasAttribute("disabled")).toBe(true);
|
|
115
|
-
});
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
describe("[UC-AX-configure-seo-analytics] the site Analytics screen end to end", () => {
|
|
119
|
-
it("AC5 · does not claim the site has no script code of its own until the GET answers", async () => {
|
|
120
|
-
const store = makeStore();
|
|
121
|
-
let answerGet = () => {};
|
|
122
|
-
mockedAxios.mockImplementation(
|
|
123
|
-
(() =>
|
|
124
|
-
new Promise((resolve) => {
|
|
125
|
-
answerGet = () => resolve(okResponse(ANALYTICS));
|
|
126
|
-
})) as never,
|
|
127
|
-
);
|
|
128
|
-
|
|
129
|
-
await renderScreen(store, siteScreen);
|
|
130
|
-
|
|
131
|
-
// In flight: `siteScriptCodeExists` is still its initial `false`, which means "not
|
|
132
|
-
// asked yet", not "no override". Deciding here is what showed the Warning to a site
|
|
133
|
-
// that does have its own script.
|
|
134
|
-
expect(warning()).toBeNull();
|
|
135
|
-
|
|
136
|
-
await act(async () => {
|
|
137
|
-
answerGet();
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
expect(warning()).toBeNull();
|
|
141
|
-
expect(screen.getByDisplayValue(ANALYTICS.scriptCode)).toBeTruthy();
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
it("AC5 · shows the Warning once the server answers that the site has no script code", async () => {
|
|
145
|
-
const store = makeStore();
|
|
146
|
-
mockedAxios.mockImplementation((async () =>
|
|
147
|
-
okResponse({ ...ANALYTICS, siteScriptCodeExists: false, scriptCode: "" })) as never);
|
|
148
|
-
|
|
149
|
-
await renderScreen(store, siteScreen);
|
|
150
|
-
|
|
151
|
-
expect(warning()).toBeTruthy();
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
it("AC5 · a failed GET leaves the editor, not a claim about the site's script code", async () => {
|
|
155
|
-
const store = makeStore();
|
|
156
|
-
mockedAxios.mockImplementation((async () => ({
|
|
157
|
-
status: 500,
|
|
158
|
-
data: { message: "Server error" },
|
|
159
|
-
statusText: "Server Error",
|
|
160
|
-
headers: {},
|
|
161
|
-
config: {},
|
|
162
|
-
})) as never);
|
|
163
|
-
|
|
164
|
-
await renderScreen(store, siteScreen);
|
|
165
|
-
|
|
166
|
-
expect(warning()).toBeNull();
|
|
167
|
-
});
|
|
168
|
-
});
|