@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
@@ -87,7 +87,6 @@ const griddoAxAliases = {
87
87
  "@ax/routes": path.resolve(__dirname, "../../src/routes"),
88
88
  "@ax/types": path.resolve(__dirname, "../../src/types"),
89
89
  "@ax/schemas": path.resolve(__dirname, "../../src/schemas"),
90
- "@ax/store": path.resolve(__dirname, "../../src/store"),
91
90
  "@ax/locales": path.resolve(__dirname, "../../src/locales"),
92
91
  "@ax/constants": path.resolve(__dirname, "../../src/constants"),
93
92
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@griddo/ax",
3
3
  "description": "Griddo Author Experience",
4
- "version": "12.6.1",
4
+ "version": "12.6.2",
5
5
  "authors": [
6
6
  "Álvaro Sánchez' <alvaro.sanches@secuoyas.com>",
7
7
  "Diego M. Béjar <diego.bejar@secuoyas.com>",
@@ -46,7 +46,6 @@
46
46
  "@dnd-kit/sortable": "10.0.0",
47
47
  "@griddo/api-types": "^1.0.12",
48
48
  "@pmmmwh/react-refresh-webpack-plugin": "0.5.16",
49
- "@reduxjs/toolkit": "2.9.0",
50
49
  "@styled-system/prop-types": "5.1.5",
51
50
  "@styled-system/theme-get": "5.1.2",
52
51
  "@svgr/webpack": "8.1.0",
@@ -78,6 +77,7 @@
78
77
  "babel-preset-react-app": "10.1.0",
79
78
  "case-sensitive-paths-webpack-plugin": "2.4.0",
80
79
  "compress.js": "1.2.2",
80
+ "connected-react-router": "6.9.3",
81
81
  "css-loader": "6.11.0",
82
82
  "css-minimizer-webpack-plugin": "5.0.1",
83
83
  "date-fns": "2.30.0",
@@ -126,6 +126,7 @@
126
126
  "react-textarea-autosize": "8.5.9",
127
127
  "react-timezone-map-select": "1.2.25",
128
128
  "redux": "4.2.1",
129
+ "redux-devtools-extension": "2.13.9",
129
130
  "redux-persist": "6.0.0",
130
131
  "reflect-metadata": "0.1.13",
131
132
  "resolve": "1.22.10",
@@ -200,5 +201,5 @@
200
201
  "publishConfig": {
201
202
  "access": "public"
202
203
  },
203
- "gitHead": "e0eec02188240c74c8fdacfa7a04fbafdc38c597"
204
+ "gitHead": "1e6a3ad08eb17a4c57f80fffb65193a82a577969"
204
205
  }
@@ -1,78 +1,35 @@
1
- import type { IRootState } from "@ax/types";
2
-
3
- import { configureStore } from "@reduxjs/toolkit";
4
- import { type Action, combineReducers, type Reducer } from "redux";
5
- import {
6
- FLUSH,
7
- PAUSE,
8
- PERSIST,
9
- type PersistConfig,
10
- type PersistedState,
11
- type Persistor,
12
- PURGE,
13
- persistReducer,
14
- persistStore,
15
- REGISTER,
16
- REHYDRATE,
17
- } from "redux-persist";
1
+ import { createStore, type Reducer, combineReducers, applyMiddleware, type Action, type StoreEnhancer } from "redux";
2
+ import { persistReducer, persistStore, type PersistConfig, type Persistor } from "redux-persist";
18
3
  import storage from "redux-persist/lib/storage"; // defaults to localStorage for web
4
+ import { connectRouter, routerMiddleware } from "connected-react-router";
5
+ import { composeWithDevTools } from "redux-devtools-extension";
6
+ import thunk from "redux-thunk";
19
7
 
20
- import { activityLogInitialState, activityLogReducer } from "./containers/ActivityLog/slice";
21
- import { analyticsInitialState, analyticsReducer } from "./containers/Analytics/slice";
8
+ import { appReducer, appInitialState } from "./containers/App/reducer";
9
+ import { sitesReducer, sitesInitialState } from "./containers/Sites/reducer";
10
+ import { pageEditorReducer, pageEditorInitialState } from "./containers/PageEditor/reducer";
11
+ import { menuReducer, menuInitialState } from "./containers/Navigation/Menu/reducer";
12
+ import { navigationReducer, navigationInitialState } from "./containers/Navigation/Defaults/reducer";
13
+ import { structuredDataReducer, structuredDataInitialState } from "./containers/StructuredData/reducer";
14
+ import { languagesReducer, languagesInitialState } from "./containers/Settings/Languages/reducer";
15
+ import { dataPacksReducer, dataPacksInitialState } from "./containers/Settings/DataPacks/reducer";
16
+ import { socialReducer, socialInitialState } from "./containers/Settings/Social/reducer";
17
+ import { usersReducer, usersInitialState } from "./containers/Users/reducer";
18
+ import { galleryReducer, galleryInitialState } from "./containers/Gallery/reducer";
19
+ import { domainsReducer, domainsInitialState } from "./containers/Domains/reducer";
20
+ import { redirectsReducer, redirectsInitialState } from "./containers/Redirects/reducer";
21
+ import { analyticsReducer, analyticsInitialState } from "./containers/Analytics/reducer";
22
+ import { integrationsReducer, integrationsInitialState } from "./containers/Integrations/reducer";
23
+ import { fileDriveReducer, fileDriveInitialState } from "./containers/FileDrive/reducer";
24
+ import { formsReducer, formsInitialState } from "./containers/Forms/reducer";
25
+ import { activityLogReducer, activityLogInitialState } from "./containers/ActivityLog/reducer";
22
26
  import { LOGOUT } from "./containers/App/constants";
23
- import { appInitialState, appReducer } from "./containers/App/reducer";
24
- import { domainsInitialState, domainsReducer } from "./containers/Domains/slice";
25
- import { fileDriveInitialState, fileDriveReducer } from "./containers/FileDrive/reducer";
26
- import { formsInitialState, formsReducer } from "./containers/Forms/reducer";
27
- import { galleryInitialState, galleryReducer } from "./containers/Gallery/reducer";
28
- import { integrationsInitialState, integrationsReducer } from "./containers/Integrations/reducer";
29
- import { navigationInitialState, navigationReducer } from "./containers/Navigation/Defaults/reducer";
30
- import { menuInitialState, menuReducer } from "./containers/Navigation/Menu/reducer";
31
- import { pageEditorInitialState, pageEditorReducer } from "./containers/PageEditor/reducer";
32
- import { redirectsInitialState, redirectsReducer } from "./containers/Redirects/slice";
33
- import { dataPacksInitialState, dataPacksReducer } from "./containers/Settings/DataPacks/reducer";
34
- import { languagesInitialState, languagesReducer } from "./containers/Settings/Languages/slice";
35
- import { socialInitialState, socialReducer } from "./containers/Settings/Social/slice";
36
- import { sitesInitialState, sitesReducer } from "./containers/Sites/reducer";
37
- import { structuredDataInitialState, structuredDataReducer } from "./containers/StructuredData/reducer";
38
- import { usersInitialState, usersReducer } from "./containers/Users/reducer";
39
-
40
- // Every area migrated to RTK leaves redux-persist (docs/adr/0002): its state is a
41
- // copy of the server's that the screen reloads on mount, so persisting it only
42
- // meant starting up with stale data. `app` stays in — it carries the session.
43
- //
44
- // The test is that EVERY consumer reloads it, not just its settings screen, so
45
- // `social` and `analytics` stay in for now: six page-editor screens read `state.social`
46
- // and `AnalyticsField` reads `state.analytics`, and none of them fetches — the persist
47
- // was their only loader on a hard refresh. They leave in waves 2-4 with those consumers
48
- // (docs/RTK-MIGRATION.md §5).
49
- const blacklist = ["menu", "app", "languages", "domains", "redirects", "activityLog"];
50
-
51
- /**
52
- * redux-persist applies the blacklist only when WRITING: `createPersistoid` filters the
53
- * keys it stages, but `getStoredState` returns every key the previous releases wrote and
54
- * `autoMergeLevel1` hard-sets them all (`newState[key] = inboundState[key]`). So adding
55
- * an area to the blacklist does not stop it from being rehydrated once — it only stops
56
- * the next write.
57
- *
58
- * Without this, the first boot after the deploy restores the areas that just left the
59
- * persist from a stale blob, which is exactly what ADR 0002 removes: state from another
60
- * site, in a screen that then PUTs it back. Deleting the keys from the inbound state is
61
- * enough (the first write afterwards replaces the whole blob), and it needs no `version`
62
- * bump — `persistReducer` calls `migrate` on every rehydrate.
63
- */
64
- export const dropBlacklistedAreas = async (state: PersistedState): Promise<PersistedState> => {
65
- if (!state) return state;
66
27
 
67
- const kept = { ...state } as Record<string, unknown>;
68
- for (const area of blacklist) {
69
- delete kept[area];
70
- }
71
- return kept as PersistedState;
72
- };
28
+ import type { IRootState } from "@ax/types";
73
29
 
30
+ const blacklist = ["router", "menu", "app", "languages"];
74
31
  export class GlobalStore {
75
- private persistConfig: PersistConfig<any> = { key: "root", storage, blacklist, migrate: dropBlacklistedAreas };
32
+ private persistConfig: PersistConfig<any> = { key: "root", storage, blacklist };
76
33
 
77
34
  private authPersistConfig = {
78
35
  key: "app",
@@ -80,8 +37,11 @@ export class GlobalStore {
80
37
  blacklist: ["error", "isLoggingIn", "isRehydrated", "isSaving", "loadingCount", "user"],
81
38
  };
82
39
 
83
- public configureStore() {
40
+ public configureStore(history: History) {
41
+ let middleware: StoreEnhancer = applyMiddleware(thunk, routerMiddleware(history));
42
+
84
43
  const globalReducer: Reducer<any> = combineReducers({
44
+ router: connectRouter(history),
85
45
  app: persistReducer(this.authPersistConfig, appReducer as Reducer<any, Action<any>>),
86
46
  sites: sitesReducer as Reducer<any, Action<any>>,
87
47
  pageEditor: pageEditorReducer as Reducer<any, Action<any>>,
@@ -105,6 +65,7 @@ export class GlobalStore {
105
65
  const rootReducer = (state: IRootState | undefined, action: any) => {
106
66
  if (action.type === LOGOUT) {
107
67
  state = {
68
+ router: state?.router,
108
69
  app: state?.app || appInitialState,
109
70
  sites: sitesInitialState,
110
71
  pageEditor: pageEditorInitialState,
@@ -129,27 +90,10 @@ export class GlobalStore {
129
90
  return globalReducer(state, action);
130
91
  };
131
92
 
132
- // configureStore already wires redux-thunk and the devtools extension (in
133
- // non-production), which is why redux-devtools-extension is gone from the deps.
134
- const store = configureStore({
135
- reducer: persistReducer(this.persistConfig, rootReducer),
136
- middleware: (getDefaultMiddleware) =>
137
- getDefaultMiddleware({
138
- // redux-persist dispatches these with non-serializable payloads by design.
139
- // Kept ON otherwise: it only logs, and its warnings are the worklist for
140
- // the offenders documented in docs/RTK-MIGRATION.md §5 (5 Date instances in
141
- // state, app.error.btnAction holding a function).
142
- serializableCheck: {
143
- ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
144
- },
145
- // OFF on purpose. The three editors (PageEditor, Forms, Navigation/Defaults)
146
- // mutate live store references and re-dispatch the regenerated tree
147
- // (docs/RTK-MIGRATION.md §2.1), so this would log on every edit — and it
148
- // deep-scans the whole tree per action, which is costly on a state this big.
149
- // Re-enable per area as each editor is sanitised (waves 2-4).
150
- immutableCheck: false,
151
- }),
152
- });
93
+ if (process.env.NODE_ENV !== "production") {
94
+ middleware = composeWithDevTools(middleware);
95
+ }
96
+ const store = createStore(persistReducer(this.persistConfig, rootReducer), middleware);
153
97
  const persistor: Persistor = persistStore(store);
154
98
  return { store, persistor };
155
99
  }
@@ -0,0 +1,88 @@
1
+ import "@testing-library/jest-dom";
2
+
3
+ import { useEditingHeartbeat } from "@ax/hooks";
4
+
5
+ import { renderHook } from "@testing-library/react";
6
+
7
+ const HEARTBEAT = 30000;
8
+
9
+ const ORIGINAL = 213;
10
+ const TRANSLATION = 214;
11
+
12
+ describe("useEditingHeartbeat hook", () => {
13
+ beforeEach(() => {
14
+ vi.useFakeTimers();
15
+ });
16
+
17
+ afterEach(() => {
18
+ vi.useRealTimers();
19
+ });
20
+
21
+ const renderHeartbeat = (pageID: number | null, sendPagePing: (id: number) => void) =>
22
+ renderHook(({ pageID }) => useEditingHeartbeat(pageID, sendPagePing), { initialProps: { pageID } });
23
+
24
+ it("locks the page on mount, without waiting for the first interval", () => {
25
+ const sendPagePing = vi.fn();
26
+
27
+ renderHeartbeat(ORIGINAL, sendPagePing);
28
+
29
+ expect(sendPagePing).toHaveBeenCalledTimes(1);
30
+ expect(sendPagePing).toHaveBeenCalledWith(ORIGINAL);
31
+ });
32
+
33
+ it("renews the lock every 30 seconds", () => {
34
+ const sendPagePing = vi.fn();
35
+
36
+ renderHeartbeat(ORIGINAL, sendPagePing);
37
+ vi.advanceTimersByTime(HEARTBEAT * 2);
38
+
39
+ // t+0 (mount) + t+30s + t+60s
40
+ expect(sendPagePing).toHaveBeenCalledTimes(3);
41
+ expect(sendPagePing.mock.calls.every(([id]) => id === ORIGINAL)).toBe(true);
42
+ });
43
+
44
+ // sc-118275: creating a translation from inside the editor changes pageID in the store without
45
+ // remounting. The heartbeat used to be pinned to the id it captured on mount, so it kept
46
+ // renewing the lock of the original page and left the translation unlocked.
47
+ it("follows the page when its id changes under an editor that never remounts", () => {
48
+ const sendPagePing = vi.fn();
49
+
50
+ const { rerender } = renderHeartbeat(ORIGINAL, sendPagePing);
51
+ vi.advanceTimersByTime(HEARTBEAT);
52
+ sendPagePing.mockClear();
53
+
54
+ rerender({ pageID: TRANSLATION });
55
+
56
+ // No 30 second gap: the page actually being edited is locked as soon as it exists.
57
+ expect(sendPagePing).toHaveBeenCalledTimes(1);
58
+ expect(sendPagePing).toHaveBeenCalledWith(TRANSLATION);
59
+
60
+ vi.advanceTimersByTime(HEARTBEAT * 2);
61
+
62
+ expect(sendPagePing).toHaveBeenCalledTimes(3);
63
+ // The original is left alone so its lock expires; renewing it would block the rest of the
64
+ // team on a page nobody is touching.
65
+ expect(sendPagePing).not.toHaveBeenCalledWith(ORIGINAL);
66
+ });
67
+
68
+ it("does not ping while there is no page yet", () => {
69
+ const sendPagePing = vi.fn();
70
+
71
+ renderHeartbeat(null, sendPagePing);
72
+ vi.advanceTimersByTime(HEARTBEAT * 2);
73
+
74
+ expect(sendPagePing).not.toHaveBeenCalled();
75
+ });
76
+
77
+ it("stops renewing the lock once the editor is closed", () => {
78
+ const sendPagePing = vi.fn();
79
+
80
+ const { unmount } = renderHeartbeat(ORIGINAL, sendPagePing);
81
+ unmount();
82
+ sendPagePing.mockClear();
83
+
84
+ vi.advanceTimersByTime(HEARTBEAT * 2);
85
+
86
+ expect(sendPagePing).not.toHaveBeenCalled();
87
+ });
88
+ });
@@ -6,12 +6,12 @@ import AppBar, { type IAppBarProps } from "./AppBar";
6
6
  import * as S from "./style";
7
7
 
8
8
  const MainWrapper = (props: IWrapperProps): JSX.Element => {
9
- const { children, fixedAppBar, fullWidth, hasAnimation, screen } = props;
9
+ const { children, fixedAppBar, fullWidth, hasAnimation } = props;
10
10
 
11
11
  const { isOnline } = useNetworkStatus();
12
12
 
13
13
  return (
14
- <S.Wrapper fixedAppBar={fixedAppBar} data-testid="main-wrapper" data-screen={screen}>
14
+ <S.Wrapper fixedAppBar={fixedAppBar} data-testid="main-wrapper">
15
15
  {hasAnimation && <S.BackgroundAnimation />}
16
16
  <AppBar {...props} />
17
17
  <S.Main fullWidth={fullWidth} data-testid="main-component" id="main-component">
@@ -30,18 +30,6 @@ const MainWrapper = (props: IWrapperProps): JSX.Element => {
30
30
  export interface IWrapperProps extends IAppBarProps {
31
31
  children?: any[] | JSX.Element;
32
32
  fullWidth?: boolean;
33
- /**
34
- * Stable per-screen identifier for e2e, emitted as `data-screen`. Separate from
35
- * `data-testid="main-wrapper"`, which is the same on all 32 screens and is already
36
- * consumed by tests here and in griddo-qa's `identifiers.ts`.
37
- *
38
- * It exists because nothing else identifies a screen: `title` is display copy and
39
- * is not even unique — Robots and Redirects are both "SEO Settings", and the global
40
- * and the site Analytics screens are both "Analytics Settings". The e2e specs had
41
- * to assert on `location.pathname` with exact equality, and `/settings/analytics`
42
- * is a substring of `/sites/settings/analytics`.
43
- */
44
- screen?: string;
45
33
  }
46
34
 
47
35
  export default MainWrapper;
@@ -1,15 +1,5 @@
1
- import { logs } from "@ax/api";
2
- import { appActions } from "@ax/containers/App";
3
- import { formsActions } from "@ax/containers/Forms";
4
- import { integrationsActions } from "@ax/containers/Integrations";
5
- import { navigationActions } from "@ax/containers/Navigation";
6
- import { pageEditorActions } from "@ax/containers/PageEditor";
7
- import { sitesActions } from "@ax/containers/Sites";
8
- import { structuredDataActions } from "@ax/containers/StructuredData";
9
- import { usersActions } from "@ax/containers/Users";
10
- import { handleRequest } from "@ax/helpers";
11
- import type { AppDispatch } from "@ax/store";
12
- import type { IRootState } from "@ax/types";
1
+ import type { Dispatch } from "redux";
2
+ import { format } from "date-fns";
13
3
 
14
4
  import type {
15
5
  LogActivityExportRequest,
@@ -20,106 +10,99 @@ import type {
20
10
  LogContentTypeDTO,
21
11
  PaginationResponse,
22
12
  } from "@griddo/api-types";
23
- import { createAsyncThunk } from "@reduxjs/toolkit";
24
- import { format } from "date-fns";
25
- import type { Dispatch } from "redux";
13
+ import { logs } from "@ax/api";
14
+ import { appActions } from "@ax/containers/App";
15
+ import { usersActions } from "@ax/containers/Users";
16
+ import { pageEditorActions } from "@ax/containers/PageEditor";
17
+ import { sitesActions } from "@ax/containers/Sites";
18
+ import { integrationsActions } from "@ax/containers/Integrations";
19
+ import { formsActions } from "@ax/containers/Forms";
20
+ import { navigationActions } from "@ax/containers/Navigation";
21
+ import { structuredDataActions } from "@ax/containers/StructuredData";
22
+ import { handleRequest } from "@ax/helpers";
23
+ import type { ILogItemsByDay, ILogItemsByUser, IRootState } from "@ax/types";
24
+
25
+ import { SET_ACTIVITY_LOG, SET_LIST_MODE } from "./constants";
26
+ import type { ISetActivityLog, ISetListMode } from "./interfaces";
27
+
28
+ function setActivityLog(activityLog: ILogItemsByDay | ILogItemsByUser): ISetActivityLog {
29
+ return { type: SET_ACTIVITY_LOG, payload: { activityLog } };
30
+ }
31
+
32
+ function setListMode(listMode: string): ISetListMode {
33
+ return { type: SET_LIST_MODE, payload: { listMode } };
34
+ }
26
35
 
27
- import { setActivityLog } from "./slice";
36
+ function getActivityLog(params: LogActivityPaginationRequest): (dispatch: Dispatch) => Promise<void> {
37
+ return async (dispatch) => {
38
+ try {
39
+ const callback = async () => logs.getLogActivityTimelineByDay(params);
28
40
 
29
- /**
30
- * Read and export side of UC-AX-view-activity-log, plus the deep-link dispatcher, as
31
- * createAsyncThunk.
32
- *
33
- * They keep calling `handleRequest` inside for the same reason the Languages pilot does —
34
- * see docs/RTK-MIGRATION.md §5.
35
- *
36
- * `goToLogContent` is the last of the wave's mines and the one that does not get cleaned up
37
- * here: it is a 160-line navigation switch that reaches into SEVEN areas that have not been
38
- * migrated yet, invoking their thunks by hand as `thunk(args)(dispatch, getState)`. That still
39
- * works while those areas are legacy, and it is exactly what stops working when they are not —
40
- * a createAsyncThunk expects the `(dispatch, getState, extra)` triple. **When `sites` migrates
41
- * in wave 3, the twelve `sitesActions.getSite(site)(dispatch, getState)` calls below break.**
42
- * Its body is transplanted verbatim on purpose — diffed line by line against the legacy one —
43
- * and the single-argument rule is the only edit it took.
44
- */
45
- const onError = (response: unknown, dispatch: Dispatch) => appActions.handleError(response)(dispatch);
41
+ const responseActions = {
42
+ handleSuccess: (response: PaginationResponse<LogActivityGroupedDayDTO>) => {
43
+ dispatch(setActivityLog(response));
44
+ },
45
+ handleError: (response: any) => appActions.handleError(response)(dispatch),
46
+ };
46
47
 
47
- /**
48
- * The store's real dispatch and state, declared once.
49
- *
50
- * `AppDispatch` is a redux 4 `AnyAction` thunk dispatch (`src/store/hooks.ts`), so the
51
- * nineteen legacy action creators and the seven legacy thunks that `goToLogContent` still
52
- * reaches for keep type-checking, and `getState()` comes back as `IRootState` instead of
53
- * `unknown`. Without this RTK types `dispatch` against redux 5's `UnknownAction`, which the
54
- * hand-written action interfaces are not assignable to — the §8.3 friction, reaching
55
- * production code here for the first time rather than just test files.
56
- */
57
- const createAppAsyncThunk = createAsyncThunk.withTypes<{ state: IRootState; dispatch: AppDispatch }>();
48
+ await handleRequest(callback, responseActions, [appActions.setIsLoading])(dispatch);
49
+ } catch (e) {
50
+ console.log(e);
51
+ }
52
+ };
53
+ }
58
54
 
59
- export const getActivityLog = createAsyncThunk(
60
- "activityLog/getActivityLog",
61
- async (params: LogActivityPaginationRequest, { dispatch }) =>
62
- handleRequest(
63
- async () => logs.getLogActivityTimelineByDay(params),
64
- {
65
- handleSuccess: (response: PaginationResponse<LogActivityGroupedDayDTO>) => dispatch(setActivityLog(response)),
66
- handleError: (response: unknown) => onError(response, dispatch),
67
- },
68
- [appActions.setIsLoading],
69
- )(dispatch),
70
- );
55
+ function getActivityLogByUser(params: LogActivityPaginationRequest): (dispatch: Dispatch) => Promise<void> {
56
+ return async (dispatch) => {
57
+ try {
58
+ const callback = async () => logs.getLogActivityTimelineByUser(params);
71
59
 
72
- export const getActivityLogByUser = createAsyncThunk(
73
- "activityLog/getActivityLogByUser",
74
- async (params: LogActivityPaginationRequest, { dispatch }) =>
75
- handleRequest(
76
- async () => logs.getLogActivityTimelineByUser(params),
77
- {
78
- handleSuccess: (response: PaginationResponse<LogActivityGroupedUserDTO<LogActivityGroupedUserDTO[]>>) =>
79
- dispatch(setActivityLog(response)),
80
- handleError: (response: unknown) => onError(response, dispatch),
81
- },
82
- [appActions.setIsLoading],
83
- )(dispatch),
84
- );
60
+ const responseActions = {
61
+ handleSuccess: (response: PaginationResponse<LogActivityGroupedUserDTO<LogActivityGroupedUserDTO[]>>) => {
62
+ dispatch(setActivityLog(response));
63
+ },
64
+ handleError: (response: any) => appActions.handleError(response)(dispatch),
65
+ };
85
66
 
86
- // The export comes back as a blob and is handed to the browser through a throwaway
87
- // `<a download>`. Kept verbatim from the legacy thunk, side effects included: it is the only
88
- // place in the area that touches the DOM.
89
- const saveExport = (payload: BlobPart, extension: string) => {
90
- const dateString = format(new Date(), "yyyy-MM-dd");
91
- const url = window.URL.createObjectURL(new Blob([payload]));
92
- const a = document.createElement("a");
93
- a.href = url;
94
- a.download = `activity-log-${dateString}.${extension}`;
95
- document.body.appendChild(a);
96
- a.click();
97
- window.URL.revokeObjectURL(url);
98
- };
67
+ await handleRequest(callback, responseActions, [appActions.setIsLoading])(dispatch);
68
+ } catch (e) {
69
+ console.log(e);
70
+ }
71
+ };
72
+ }
99
73
 
100
- export const downloadActivityLog = createAsyncThunk(
101
- "activityLog/downloadActivityLog",
102
- // No loading flag, as before: the export runs without the screen's spinner.
103
- async (data: LogActivityExportRequest, { dispatch }) =>
104
- handleRequest(
105
- async () => logs.logExport(data),
106
- {
107
- handleSuccess: (response: BlobPart) => saveExport(response, data.format[0]),
108
- handleError: (response: unknown) => onError(response, dispatch),
109
- },
110
- [],
111
- )(dispatch),
112
- );
74
+ function downloadActivityLog(data: LogActivityExportRequest): (dispatch: Dispatch) => Promise<void> {
75
+ return async (dispatch) => {
76
+ try {
77
+ const responseActions = {
78
+ handleSuccess: (response: any) => {
79
+ const dateString = format(new Date(), "yyyy-MM-dd");
80
+ const url = window.URL.createObjectURL(new Blob([response]));
81
+ const a = document.createElement("a");
82
+ a.href = url;
83
+ a.download = `activity-log-${dateString}.${data.format[0]}`;
84
+ document.body.appendChild(a);
85
+ a.click();
86
+ window.URL.revokeObjectURL(url);
87
+ },
88
+ handleError: (response: any) => appActions.handleError(response)(dispatch),
89
+ };
113
90
 
114
- interface IGoToLogContentArgs {
115
- contentType: LogContentTypeDTO | null;
116
- content: LogContentDTO | null;
117
- site: number | null;
91
+ const callback = async () => logs.logExport(data);
92
+
93
+ await handleRequest(callback, responseActions, [])(dispatch);
94
+ } catch (e) {
95
+ console.log(e);
96
+ }
97
+ };
118
98
  }
119
99
 
120
- export const goToLogContent = createAppAsyncThunk(
121
- "activityLog/goToLogContent",
122
- async ({ contentType, content, site }: IGoToLogContentArgs, { dispatch, getState }) => {
100
+ function goToLogContent(
101
+ contentType: LogContentTypeDTO | null,
102
+ content: LogContentDTO | null,
103
+ site: number | null,
104
+ ): (dispatch: Dispatch, getState: () => IRootState) => void {
105
+ return async (dispatch, getState) => {
123
106
  const {
124
107
  app: { lang, globalLangs },
125
108
  } = getState();
@@ -274,5 +257,7 @@ export const goToLogContent = createAppAsyncThunk(
274
257
  break;
275
258
  }
276
259
  }
277
- },
278
- );
260
+ };
261
+ }
262
+
263
+ export { getActivityLog, getActivityLogByUser, setListMode, downloadActivityLog, goToLogContent };
@@ -0,0 +1,6 @@
1
+ const NAME = "activityLog";
2
+
3
+ const SET_ACTIVITY_LOG = `${NAME}/SET_ACTIVITY_LOG`;
4
+ const SET_LIST_MODE = `${NAME}/SET_LIST_MODE`;
5
+
6
+ export { SET_ACTIVITY_LOG, SET_LIST_MODE };
@@ -1,17 +1,4 @@
1
- import { setListMode } from "./slice";
2
- import { downloadActivityLog, getActivityLog, getActivityLogByUser, goToLogContent } from "./thunks";
1
+ import * as activityLogActions from "./actions";
2
+ import { activityLogReducer } from "./reducer";
3
3
 
4
- // `setActivityLog` stays private, as it was: the legacy `actions.tsx` did not export it
5
- // either — only the thunks write the page.
6
- const activityLogActions = {
7
- getActivityLog,
8
- getActivityLogByUser,
9
- setListMode,
10
- downloadActivityLog,
11
- goToLogContent,
12
- };
13
-
14
- export { activityLogActions };
15
-
16
- export type { IActivityLogState } from "./slice";
17
- export { activityLogInitialState, activityLogReducer } from "./slice";
4
+ export { activityLogActions, activityLogReducer };
@@ -0,0 +1,12 @@
1
+ import type { ILogItemsByDay, ILogItemsByUser } from "@ax/types";
2
+ import type { SET_ACTIVITY_LOG, SET_LIST_MODE } from "./constants";
3
+
4
+ export interface ISetActivityLog {
5
+ type: typeof SET_ACTIVITY_LOG;
6
+ payload: { activityLog: ILogItemsByDay | ILogItemsByUser };
7
+ }
8
+
9
+ export interface ISetListMode {
10
+ type: typeof SET_LIST_MODE;
11
+ payload: { listMode: string };
12
+ }
@@ -0,0 +1,24 @@
1
+ import type { ILogItemsByDay, ILogItemsByUser } from "@ax/types";
2
+ import { SET_ACTIVITY_LOG, SET_LIST_MODE } from "./constants";
3
+
4
+ export interface IActivityLogState {
5
+ activityLog: ILogItemsByDay | ILogItemsByUser;
6
+ listMode: string;
7
+ }
8
+
9
+ export const initialState: IActivityLogState = {
10
+ activityLog: { totalItems: 0, items: [] },
11
+ listMode: "default",
12
+ };
13
+
14
+ export function reducer(state = initialState, action: any): IActivityLogState {
15
+ switch (action.type) {
16
+ case SET_ACTIVITY_LOG:
17
+ case SET_LIST_MODE:
18
+ return { ...state, ...action.payload };
19
+ default:
20
+ return state;
21
+ }
22
+ }
23
+
24
+ export { initialState as activityLogInitialState, reducer as activityLogReducer };