@griddo/ax 12.6.0 → 12.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/config/griddo-config/index.js +1 -0
  2. package/package.json +3 -4
  3. package/src/GlobalStore.tsx +91 -35
  4. package/src/__tests__/modules/ActivityLog/ActivityLog.uc.test.tsx +132 -0
  5. package/src/__tests__/modules/Analytics/Analytics.uc.test.tsx +168 -0
  6. package/src/__tests__/modules/GlobalSettings/Robots/Robots.test.tsx +110 -0
  7. package/src/__tests__/modules/GlobalSettings/Robots/Robots.uc.test.tsx +125 -0
  8. package/src/__tests__/modules/Redirects/Redirects.uc.test.tsx +164 -0
  9. package/src/__tests__/modules/Settings/Languages/Languages.uc.test.tsx +213 -0
  10. package/src/__tests__/modules/Settings/Social/Social.uc.test.tsx +171 -0
  11. package/src/__tests__/store/activityLog.test.ts +301 -0
  12. package/src/__tests__/store/analytics.test.ts +134 -0
  13. package/src/__tests__/store/domains.test.ts +107 -0
  14. package/src/__tests__/store/languages.test.ts +129 -0
  15. package/src/__tests__/store/persist.test.ts +47 -0
  16. package/src/__tests__/store/redirects.test.ts +313 -0
  17. package/src/__tests__/store/social.test.ts +165 -0
  18. package/src/components/MainWrapper/index.tsx +14 -2
  19. package/src/containers/ActivityLog/index.tsx +16 -3
  20. package/src/containers/ActivityLog/slice.ts +45 -0
  21. package/src/containers/ActivityLog/{actions.tsx → thunks.ts} +104 -89
  22. package/src/containers/Analytics/index.tsx +14 -3
  23. package/src/containers/Analytics/slice.ts +48 -0
  24. package/src/containers/Analytics/thunks.ts +103 -0
  25. package/src/containers/Domains/index.tsx +12 -3
  26. package/src/containers/Domains/slice.ts +37 -0
  27. package/src/containers/Domains/thunks.ts +60 -0
  28. package/src/containers/Redirects/constants.tsx +1 -17
  29. package/src/containers/Redirects/index.tsx +14 -3
  30. package/src/containers/Redirects/slice.ts +60 -0
  31. package/src/containers/Redirects/thunks.ts +196 -0
  32. package/src/containers/Settings/Languages/index.tsx +15 -3
  33. package/src/containers/Settings/Languages/slice.ts +48 -0
  34. package/src/containers/Settings/Languages/thunks.ts +83 -0
  35. package/src/containers/Settings/Social/index.tsx +11 -3
  36. package/src/containers/Settings/Social/slice.ts +40 -0
  37. package/src/containers/Settings/Social/thunks.ts +68 -0
  38. package/src/containers/Sites/actions.tsx +6 -2
  39. package/src/index.tsx +7 -6
  40. package/src/modules/ActivityLog/ItemLog/EventItem/index.tsx +13 -27
  41. package/src/modules/ActivityLog/ItemLogUser/UserItem/EventItem/index.tsx +13 -27
  42. package/src/modules/ActivityLog/index.tsx +22 -51
  43. package/src/modules/Analytics/index.tsx +33 -42
  44. package/src/modules/App/Routing/index.tsx +5 -4
  45. package/src/modules/FileDrive/index.tsx +1 -1
  46. package/src/modules/GlobalSettings/Robots/index.tsx +24 -47
  47. package/src/modules/MediaGallery/index.tsx +1 -1
  48. package/src/modules/Redirects/RedirectItem/index.tsx +9 -22
  49. package/src/modules/Redirects/RedirectPanel/index.tsx +13 -14
  50. package/src/modules/Redirects/index.tsx +26 -69
  51. package/src/modules/Settings/ContentTypes/DataPacks/index.tsx +4 -3
  52. package/src/modules/Settings/Languages/LanguagePanel/Form/ConnectedField/index.tsx +11 -18
  53. package/src/modules/Settings/Languages/LanguagePanel/Form/index.tsx +7 -9
  54. package/src/modules/Settings/Languages/LanguagePanel/index.tsx +38 -44
  55. package/src/modules/Settings/Languages/Table/Header/index.tsx +12 -19
  56. package/src/modules/Settings/Languages/Table/Item/index.tsx +21 -36
  57. package/src/modules/Settings/Languages/Table/index.tsx +6 -14
  58. package/src/modules/Settings/Languages/index.tsx +18 -30
  59. package/src/modules/Settings/SeoAnalyticsSettings/Analytics/index.tsx +42 -52
  60. package/src/modules/Settings/Social/index.tsx +17 -12
  61. package/src/modules/StructuredData/StructuredDataList/index.tsx +3 -2
  62. package/src/store/hooks.ts +37 -0
  63. package/src/store/index.ts +2 -0
  64. package/src/types/index.tsx +5 -6
  65. package/tsconfig.paths.json +1 -0
  66. package/src/containers/ActivityLog/constants.tsx +0 -6
  67. package/src/containers/ActivityLog/interfaces.tsx +0 -12
  68. package/src/containers/ActivityLog/reducer.tsx +0 -24
  69. package/src/containers/Analytics/actions.tsx +0 -109
  70. package/src/containers/Analytics/constants.tsx +0 -5
  71. package/src/containers/Analytics/interfaces.tsx +0 -9
  72. package/src/containers/Analytics/reducer.tsx +0 -28
  73. package/src/containers/Domains/actions.tsx +0 -58
  74. package/src/containers/Domains/constants.tsx +0 -5
  75. package/src/containers/Domains/interfaces.tsx +0 -9
  76. package/src/containers/Domains/reducer.tsx +0 -22
  77. package/src/containers/Redirects/actions.tsx +0 -190
  78. package/src/containers/Redirects/interfaces.tsx +0 -23
  79. package/src/containers/Redirects/reducer.tsx +0 -35
  80. package/src/containers/Settings/Languages/actions.tsx +0 -88
  81. package/src/containers/Settings/Languages/constants.tsx +0 -6
  82. package/src/containers/Settings/Languages/reducer.tsx +0 -31
  83. package/src/containers/Settings/Social/actions.tsx +0 -63
  84. package/src/containers/Settings/Social/constants.tsx +0 -3
  85. package/src/containers/Settings/Social/interfaces.tsx +0 -9
  86. package/src/containers/Settings/Social/reducer.tsx +0 -18
@@ -1,5 +1,4 @@
1
1
  import { useCallback, useEffect, useRef, useState } from "react";
2
- import { connect } from "react-redux";
3
2
 
4
3
  import {
5
4
  EmptyState,
@@ -14,7 +13,8 @@ import {
14
13
  import { appActions } from "@ax/containers/App";
15
14
  import { redirectsActions } from "@ax/containers/Redirects";
16
15
  import { useBulkSelection, useModals, useToast } from "@ax/hooks";
17
- import type { IEmptyStateProps, INavItem, IQueryValue, IRedirect, IRootState, ISite } from "@ax/types";
16
+ import { useAppDispatch, useAppSelector } from "@ax/store";
17
+ import type { IEmptyStateProps, INavItem, IQueryValue, IRedirect } from "@ax/types";
18
18
 
19
19
  import { DeleteModal, ImportCheckModal, ImportModal, OverwriteModal } from "./atoms";
20
20
  import BulkHeader from "./BulkHeader";
@@ -26,22 +26,15 @@ import { getSortedListStatus } from "./utils";
26
26
  import * as S from "./style";
27
27
 
28
28
  const Redirects = (props: IProps): JSX.Element => {
29
- const {
30
- navItems,
31
- currentNavItem,
32
- setHistoryPush,
33
- getRedirects,
34
- redirects,
35
- totalItems,
36
- deleteRedirect,
37
- addRedirect,
38
- currentSite,
39
- importRedirects,
40
- imports,
41
- totalImports,
42
- isLoading,
43
- isSaving,
44
- } = props;
29
+ const { navItems, currentNavItem } = props;
30
+ const dispatch = useAppDispatch();
31
+ const redirects = useAppSelector((state) => state.redirects.redirects);
32
+ const totalItems = useAppSelector((state) => state.redirects.totalItems);
33
+ const totalImports = useAppSelector((state) => state.redirects.totalImports);
34
+ const imports = useAppSelector((state) => state.redirects.imports);
35
+ const currentSite = useAppSelector((state) => state.sites.currentSiteInfo);
36
+ const isLoading = useAppSelector((state) => state.app.isLoading);
37
+ const isSaving = useAppSelector((state) => state.app.isSaving);
45
38
 
46
39
  const itemsPerPage = 50;
47
40
  const firstPage = 1;
@@ -88,7 +81,7 @@ const Redirects = (props: IProps): JSX.Element => {
88
81
  // biome-ignore lint/correctness/useExhaustiveDependencies: TODO: fix this
89
82
  useEffect(() => {
90
83
  const params = getParams();
91
- getRedirects(params, filterQuery);
84
+ dispatch(redirectsActions.getRedirects({ params, filters: filterQuery }));
92
85
  if (tableRef.current) {
93
86
  tableRef.current.scrollTo(0, 0);
94
87
  }
@@ -125,7 +118,7 @@ const Redirects = (props: IProps): JSX.Element => {
125
118
  } = useBulkSelection(redIds);
126
119
 
127
120
  const bulkDelete = async () => {
128
- const deleted = await deleteRedirect(selectedItems.all);
121
+ const deleted = await dispatch(redirectsActions.deleteRedirect({ redirectID: selectedItems.all })).unwrap();
129
122
  if (deleted) {
130
123
  toggleToast();
131
124
  }
@@ -191,7 +184,7 @@ const Redirects = (props: IProps): JSX.Element => {
191
184
  };
192
185
 
193
186
  const handleMenuClick = (path: string) => {
194
- setHistoryPush(path);
187
+ dispatch(appActions.setHistoryPush(path));
195
188
  };
196
189
 
197
190
  const handleModal = () => {
@@ -202,7 +195,13 @@ const Redirects = (props: IProps): JSX.Element => {
202
195
 
203
196
  const handleAddRedirect = async (force?: boolean) => {
204
197
  const toPage = formValues.to.pageId ? formValues.to.pageId : formValues.to.url;
205
- await addRedirect({ from: formValues.from, to: toPage }, () => toggleModal("overwrite"), force, filterQuery);
198
+ // A taken origin (409) comes back as data now, instead of through a callback the thunk
199
+ // used to be handed: a function in `meta.arg` trips the serializableCheck, and deciding
200
+ // which modal to open was never the thunk's business (docs/RTK-MIGRATION.md §5).
201
+ const { isOriginTaken } = await dispatch(
202
+ redirectsActions.addRedirect({ redirect: { from: formValues.from, to: toPage }, force, filter: filterQuery }),
203
+ ).unwrap();
204
+ if (isOriginTaken) toggleModal("overwrite");
206
205
  isOpen("edit") && toggleModal("edit");
207
206
  };
208
207
 
@@ -230,14 +229,14 @@ const Redirects = (props: IProps): JSX.Element => {
230
229
 
231
230
  const handleCheckImportData = async (data: { from: string; to: string }[]) => {
232
231
  setImportData(data);
233
- await importRedirects(data, true);
232
+ await dispatch(redirectsActions.importRedirects({ rows: data, check: true }));
234
233
  toggleModal("import");
235
234
  setIsUploading(false);
236
235
  toggleModal("checkImport");
237
236
  };
238
237
 
239
238
  const handleImportData = async () => {
240
- const imported = await importRedirects(importData, false);
239
+ const imported = await dispatch(redirectsActions.importRedirects({ rows: importData, check: false })).unwrap();
241
240
  if (imported) {
242
241
  toggleImportToast();
243
242
  }
@@ -253,6 +252,7 @@ const Redirects = (props: IProps): JSX.Element => {
253
252
  <MainWrapper
254
253
  backLink={false}
255
254
  title="SEO Settings"
255
+ screen="redirects"
256
256
  rightButton={rightButtonProps}
257
257
  rightLineButton={rightLineButtonProps}
258
258
  searchAction={handleSearch}
@@ -360,52 +360,9 @@ const Redirects = (props: IProps): JSX.Element => {
360
360
  );
361
361
  };
362
362
 
363
- const mapStateToProps = (state: IRootState) => ({
364
- redirects: state.redirects.redirects,
365
- totalItems: state.redirects.totalItems,
366
- currentSite: state.sites.currentSiteInfo,
367
- totalImports: state.redirects.totalImports,
368
- imports: state.redirects.imports,
369
- isLoading: state.app.isLoading,
370
- isSaving: state.app.isSaving,
371
- });
372
-
373
- const mapDispatchToProps = {
374
- setHistoryPush: appActions.setHistoryPush,
375
- getRedirects: redirectsActions.getRedirects,
376
- deleteRedirect: redirectsActions.deleteRedirect,
377
- addRedirect: redirectsActions.addRedirect,
378
- importRedirects: redirectsActions.importRedirects,
379
- };
380
- interface IRedirectsProps {
363
+ interface IProps {
381
364
  navItems: INavItem[];
382
365
  currentNavItem: INavItem;
383
- redirects: IRedirect[];
384
- totalItems: number;
385
- currentSite: ISite | null;
386
- totalImports: number;
387
- isLoading: boolean;
388
- isSaving: boolean;
389
- imports: null | {
390
- error: IRedirect[];
391
- existing: IRedirect[];
392
- ok: IRedirect[];
393
- };
394
366
  }
395
367
 
396
- interface IDispatchProps {
397
- setHistoryPush(path: string, isEditor?: boolean): void;
398
- getRedirects(params: any, filters: string): void;
399
- deleteRedirect(redirectID: number[]): Promise<boolean>;
400
- addRedirect(
401
- redirect: { from: string; to: string | number },
402
- errorAction: () => void,
403
- force?: boolean,
404
- filter?: string,
405
- ): Promise<void>;
406
- importRedirects(redirects: { from: string; to: string | number }[], check: boolean): Promise<boolean>;
407
- }
408
-
409
- type IProps = IRedirectsProps & IDispatchProps;
410
-
411
- export default connect(mapStateToProps, mapDispatchToProps)(Redirects);
368
+ export default Redirects;
@@ -1,5 +1,6 @@
1
1
  import React, { useCallback, useEffect, useState } from "react";
2
2
  import { connect } from "react-redux";
3
+ import { useHistory } from "react-router-dom";
3
4
 
4
5
  import { EmptyState, ErrorToast, Loading, MainWrapper, Modal, Toast } from "@ax/components";
5
6
  import { appActions } from "@ax/containers/App";
@@ -30,9 +31,11 @@ const DataPacks = (props: IProps): JSX.Element => {
30
31
  addDataPacks,
31
32
  deleteSiteDataPack,
32
33
  activatedDataPacks,
33
- routerAction,
34
34
  } = props;
35
35
 
36
+ // Was `state.router.action` from connected-react-router's router slice.
37
+ const { action: routerAction } = useHistory();
38
+
36
39
  const isFromPage = selected?.templates && selected.templates.length > 0;
37
40
 
38
41
  const { isDirty, setIsDirty } = useShouldBeSaved(selected, form);
@@ -220,7 +223,6 @@ interface IProps {
220
223
  selected: any;
221
224
  form: any;
222
225
  activatedDataPacks: IDataPack[];
223
- routerAction: string;
224
226
  getSiteDataPacks: (queryParams?: string) => void;
225
227
  setSelected: (dataPack?: any) => void;
226
228
  updateDataPack: (dataPack?: any) => Promise<boolean>;
@@ -246,7 +248,6 @@ const mapStateToProps = (state: IRootState) => ({
246
248
  selected: state.dataPacks.selected,
247
249
  form: state.dataPacks.configFormData,
248
250
  activatedDataPacks: state.dataPacks.activated,
249
- routerAction: state.router.action,
250
251
  });
251
252
 
252
253
  export default connect(mapStateToProps, mapDispatchToProps)(DataPacks);
@@ -1,17 +1,19 @@
1
- import React from "react";
2
- import { connect } from "react-redux";
1
+ import React, { memo } from "react";
3
2
 
4
- import type { IRootState } from "@ax/types";
5
- import { languagesActions } from "@ax/containers/Settings";
6
3
  import { FieldsBehavior } from "@ax/components";
4
+ import { languagesActions } from "@ax/containers/Settings";
5
+ import { useAppDispatch, useAppSelector } from "@ax/store";
7
6
 
8
7
  const ConnectedField = (props: IProps) => {
9
- const { form, type, name, updateFormValue } = props;
8
+ const { type, name } = props;
10
9
 
11
- const value = form[name];
10
+ const dispatch = useAppDispatch();
11
+ // Granular selector: this field only re-renders when its own value changes,
12
+ // not on every change to the form.
13
+ const value = useAppSelector((state) => state.languages.form[name as keyof typeof state.languages.form]);
12
14
 
13
15
  const handleChange = (newValue: any) => {
14
- updateFormValue({ [name]: newValue });
16
+ dispatch(languagesActions.updateFormValue({ [name]: newValue }));
15
17
  };
16
18
 
17
19
  const fieldProps = {
@@ -24,7 +26,6 @@ const ConnectedField = (props: IProps) => {
24
26
  };
25
27
 
26
28
  interface IProps {
27
- form: any;
28
29
  type: string;
29
30
  title: string;
30
31
  name: string;
@@ -33,16 +34,8 @@ interface IProps {
33
34
  helptext?: string;
34
35
  options?: any;
35
36
  filter?: any;
36
- updateFormValue: (value: any) => void;
37
37
  autoComplete?: string;
38
38
  }
39
39
 
40
- const mapStateToProps = (state: IRootState) => ({
41
- form: state.languages.form,
42
- });
43
-
44
- const mapDispatchToProps = {
45
- updateFormValue: languagesActions.updateFormValue,
46
- };
47
-
48
- export default connect(mapStateToProps, mapDispatchToProps)(ConnectedField);
40
+ // `connect` used to memoise this component implicitly; keep that now it is gone.
41
+ export default memo(ConnectedField);
@@ -1,14 +1,17 @@
1
1
  import React from "react";
2
- import { connect } from "react-redux";
3
2
 
4
- import type { ILanguage, IRootState } from "@ax/types";
5
3
  import { Flag } from "@ax/components";
4
+ import { useAppSelector } from "@ax/store";
5
+ import type { ILanguage } from "@ax/types";
6
+
6
7
  import ConnectedField from "./ConnectedField";
7
8
 
8
9
  import * as S from "./style";
9
10
 
10
11
  const Form = (props: IProps): JSX.Element => {
11
- const { currentSiteLanguages, item } = props;
12
+ const { item } = props;
13
+
14
+ const currentSiteLanguages = useAppSelector((state) => state.sites.currentSiteLanguages);
12
15
 
13
16
  const currentSiteLanguagesIds = currentSiteLanguages.map((e) => e.id);
14
17
 
@@ -52,13 +55,8 @@ const Form = (props: IProps): JSX.Element => {
52
55
  );
53
56
  };
54
57
 
55
- const mapStateToProps = (state: IRootState) => ({
56
- currentSiteLanguages: state.sites.currentSiteLanguages,
57
- });
58
-
59
58
  interface IProps {
60
59
  item?: ILanguage;
61
- currentSiteLanguages: ILanguage[];
62
60
  }
63
61
 
64
- export default connect(mapStateToProps)(Form);
62
+ export default Form;
@@ -1,28 +1,34 @@
1
1
  import React from "react";
2
- import { connect } from "react-redux";
3
2
 
4
- import type { ILanguage, IRootState, ISiteLanguage } from "@ax/types";
3
+ import { Button, FloatingPanel } from "@ax/components";
5
4
  import { languagesActions } from "@ax/containers/Settings";
6
- import { FloatingPanel, Button } from "@ax/components";
5
+ import { useAppDispatch, useAppSelector } from "@ax/store";
6
+ import type { ILanguage } from "@ax/types";
7
7
 
8
8
  import Form from "./Form";
9
9
 
10
10
  import * as S from "./style";
11
11
 
12
12
  const LanguagePanel = (props: IProps): JSX.Element => {
13
- const { isOpen, toggleModal, createSiteLanguage, updateSiteLanguage, currentSiteID, languageData, item } = props;
13
+ const { isOpen, toggleModal, item } = props;
14
+
15
+ const dispatch = useAppDispatch();
16
+ const currentSiteID = useAppSelector((state) => state.sites.currentSiteInfo?.id ?? null);
17
+ const languageData = useAppSelector((state) => state.languages.form);
14
18
 
15
19
  if (!currentSiteID) {
16
20
  throw new Error(`ERROR: User reached Languages with null site info`);
17
21
  }
18
22
 
23
+ // Closes without awaiting the thunk, as the connect version did: a failure
24
+ // still closes the panel and only surfaces through the error toast.
19
25
  const addItemAction = () => {
20
- createSiteLanguage(currentSiteID, languageData);
26
+ dispatch(languagesActions.createSiteLanguage({ siteID: currentSiteID, data: languageData }));
21
27
  toggleModal();
22
28
  };
23
29
 
24
30
  const editItemAction = () => {
25
- updateSiteLanguage(currentSiteID, languageData);
31
+ dispatch(languagesActions.updateSiteLanguage({ siteID: currentSiteID, data: languageData }));
26
32
  toggleModal();
27
33
  };
28
34
 
@@ -40,52 +46,40 @@ const LanguagePanel = (props: IProps): JSX.Element => {
40
46
 
41
47
  const title = item ? "Update Language" : "New Language";
42
48
 
49
+ // The panel itself stays mounted — FloatingPanel animates off `isOpen` and
50
+ // unmounting it would drop that animation — but its body only mounts once the
51
+ // panel opens. The Form holds the Language and Domain AsyncSelects, each of
52
+ // which fetches its options on mount, and there is one panel per row plus one
53
+ // in the header: keeping them mounted meant N+1 requests for `/select/domains`
54
+ // on every visit to a screen where the user may open no panel at all. The form
55
+ // state lives in the store (the row dispatches setForm before opening), so
56
+ // mounting late loses nothing.
43
57
  return (
44
58
  <FloatingPanel title={title} toggleModal={toggleModal} isOpen={isOpen}>
45
- <S.Wrapper>
46
- <Form item={item} />
47
- <S.Footer>
48
- {item ? (
49
- <Button className="button" type="button" onClick={editButton.action} disabled={editButton.disabled}>
50
- {editButton.label}
51
- </Button>
52
- ) : (
53
- <Button className="button" type="button" onClick={addButton.action} disabled={addButton.disabled}>
54
- {addButton.label}
55
- </Button>
56
- )}
57
- </S.Footer>
58
- </S.Wrapper>
59
+ {isOpen && (
60
+ <S.Wrapper>
61
+ <Form item={item} />
62
+ <S.Footer>
63
+ {item ? (
64
+ <Button className="button" type="button" onClick={editButton.action} disabled={editButton.disabled}>
65
+ {editButton.label}
66
+ </Button>
67
+ ) : (
68
+ <Button className="button" type="button" onClick={addButton.action} disabled={addButton.disabled}>
69
+ {addButton.label}
70
+ </Button>
71
+ )}
72
+ </S.Footer>
73
+ </S.Wrapper>
74
+ )}
59
75
  </FloatingPanel>
60
76
  );
61
77
  };
62
78
 
63
- const mapStateToProps = (state: IRootState) => ({
64
- currentSiteID: state.sites.currentSiteInfo && state.sites.currentSiteInfo.id,
65
- languageData: state.languages.form,
66
- });
67
-
68
- const mapDispatchToProps = {
69
- createSiteLanguage: languagesActions.createSiteLanguage,
70
- updateSiteLanguage: languagesActions.updateSiteLanguage,
71
- };
72
-
73
- interface IStateProps {
74
- currentSiteID: number | null;
75
- languageData: ISiteLanguage;
76
- }
77
-
78
- interface ILanguagePanelProps {
79
+ interface IProps {
79
80
  item?: ILanguage;
80
81
  isOpen: boolean;
81
82
  toggleModal(): any;
82
83
  }
83
84
 
84
- interface IDispatchProps {
85
- createSiteLanguage(siteID: number, data: ISiteLanguage): void;
86
- updateSiteLanguage(siteID: number, data: ISiteLanguage): void;
87
- }
88
-
89
- type IProps = IDispatchProps & ILanguagePanelProps & IStateProps;
90
-
91
- export default connect(mapStateToProps, mapDispatchToProps)(LanguagePanel);
85
+ export default LanguagePanel;
@@ -1,26 +1,27 @@
1
1
  import React from "react";
2
- import { connect } from "react-redux";
3
2
 
4
- import { useModal } from "@ax/hooks";
5
- import { languagesActions } from "@ax/containers/Settings";
6
- import type { ILanguage } from "@ax/types";
7
3
  import { IconAction } from "@ax/components";
4
+ import { languagesActions } from "@ax/containers/Settings";
8
5
  import { Restricted } from "@ax/guards";
6
+ import { useModal } from "@ax/hooks";
7
+ import { useAppDispatch } from "@ax/store";
8
+ import type { ILanguage } from "@ax/types";
9
+
9
10
  import LanguagePanel from "../../LanguagePanel";
10
11
 
11
12
  import * as S from "./style";
12
13
 
13
14
  const Header = (props: IProps): JSX.Element => {
14
- const { items, resetForm } = props;
15
+ const { items } = props;
16
+
17
+ const dispatch = useAppDispatch();
15
18
 
16
19
  const title = items.length > 1 ? `${items.length} items` : `${items.length} item`;
17
20
 
18
21
  const { isOpen, toggleModal } = useModal();
19
22
 
20
- const resetValues = () => resetForm();
21
-
22
23
  const openModal = () => {
23
- resetValues();
24
+ dispatch(languagesActions.resetForm());
24
25
  toggleModal();
25
26
  };
26
27
 
@@ -34,17 +35,9 @@ const Header = (props: IProps): JSX.Element => {
34
35
  </S.Header>
35
36
  );
36
37
  };
37
- interface ITableHeaderProps {
38
+
39
+ interface IProps {
38
40
  items: ILanguage[];
39
41
  }
40
- interface IDispatchProps {
41
- resetForm(): void;
42
- }
43
-
44
- type IProps = ITableHeaderProps & IDispatchProps;
45
-
46
- const mapDispatchToProps = {
47
- resetForm: languagesActions.resetForm,
48
- };
49
42
 
50
- export default connect(null, mapDispatchToProps)(Header);
43
+ export default Header;
@@ -1,26 +1,32 @@
1
- import React from "react";
2
- import { connect } from "react-redux";
1
+ import React, { memo } from "react";
3
2
 
4
3
  import { ContextMenu, Flag } from "@ax/components";
5
- import { useContextMenu, useModal, usePermission } from "@ax/hooks";
6
- import type { ILanguage, ISiteLanguage, IRootState, ISite } from "@ax/types";
7
4
  import { languagesActions } from "@ax/containers/Settings";
5
+ import { useContextMenu, useModal, usePermission } from "@ax/hooks";
6
+ import { useAppDispatch, useAppSelector } from "@ax/store";
7
+ import type { ILanguage, ISiteLanguage } from "@ax/types";
8
+
8
9
  import LanguagePanel from "../../LanguagePanel";
9
10
 
10
11
  import * as S from "./style";
11
12
 
12
- const Item = (props: IItemProps): JSX.Element => {
13
- const { item, resetForm, setForm, deleteSiteLanguage, currentSiteInfo } = props;
13
+ const Item = (props: IProps): JSX.Element => {
14
+ const { item } = props;
14
15
 
15
- if (!currentSiteInfo) {
16
- throw new Error(`ERROR: User reached Site Languages with null site info`);
17
- }
16
+ const dispatch = useAppDispatch();
17
+ const currentSiteInfo = useAppSelector((state) => state.sites.currentSiteInfo);
18
18
 
19
19
  const { isOpen, toggleModal } = useModal();
20
20
  const isAllowedToEdit = usePermission("general.editLanguage");
21
21
  const isAllowedToDelete = usePermission("general.deleteLanguages");
22
22
  const { contextMenu, handleContextMenu, closeContextMenu } = useContextMenu();
23
23
 
24
+ // After the hooks, not before: bailing out earlier would make the hook order
25
+ // depend on state. Same guard as the connect version, just moved down.
26
+ if (!currentSiteInfo) {
27
+ throw new Error(`ERROR: User reached Site Languages with null site info`);
28
+ }
29
+
24
30
  const siteLanguage: ISiteLanguage = {
25
31
  language: item.id,
26
32
  path: item.path || "",
@@ -28,7 +34,8 @@ const Item = (props: IItemProps): JSX.Element => {
28
34
  isDefault: item.isDefault,
29
35
  };
30
36
 
31
- const removeItemAction = () => deleteSiteLanguage(currentSiteInfo.id, item.id);
37
+ const removeItemAction = () =>
38
+ dispatch(languagesActions.deleteSiteLanguage({ siteID: currentSiteInfo.id, languageID: item.id }));
32
39
 
33
40
  const menuOptions = isAllowedToDelete
34
41
  ? [
@@ -40,13 +47,9 @@ const Item = (props: IItemProps): JSX.Element => {
40
47
  ]
41
48
  : [];
42
49
 
43
- const resetValues = () => resetForm();
44
-
45
- const setItem = (item: ISiteLanguage) => setForm(item);
46
-
47
50
  const openModal = () => {
48
- resetValues();
49
- setItem(siteLanguage);
51
+ dispatch(languagesActions.resetForm());
52
+ dispatch(languagesActions.setForm(siteLanguage));
50
53
  toggleModal();
51
54
  };
52
55
 
@@ -68,25 +71,7 @@ const Item = (props: IItemProps): JSX.Element => {
68
71
 
69
72
  interface IProps {
70
73
  item: ILanguage;
71
- currentSiteInfo: ISite | null;
72
74
  }
73
75
 
74
- const mapStateToProps = (state: IRootState) => ({
75
- currentSiteInfo: state.sites.currentSiteInfo,
76
- });
77
-
78
- const mapDispatchToProps = {
79
- resetForm: languagesActions.resetForm,
80
- setForm: languagesActions.setForm,
81
- deleteSiteLanguage: languagesActions.deleteSiteLanguage,
82
- };
83
-
84
- interface IDispatchProps {
85
- setForm(item: ISiteLanguage): void;
86
- resetForm(): void;
87
- deleteSiteLanguage(siteId: number, languageId: number): void;
88
- }
89
-
90
- type IItemProps = IProps & IDispatchProps;
91
-
92
- export default connect(mapStateToProps, mapDispatchToProps)(Item);
76
+ // One per row: `connect` memoised it implicitly, so keep that now it is gone.
77
+ export default memo(Item);
@@ -1,15 +1,15 @@
1
1
  import React from "react";
2
- import { connect } from "react-redux";
3
2
 
4
- import type { ILanguage, IRootState } from "@ax/types";
3
+ import { useAppSelector } from "@ax/store";
4
+ import type { ILanguage } from "@ax/types";
5
5
 
6
- import Item from "./Item";
7
6
  import Header from "./Header";
7
+ import Item from "./Item";
8
8
 
9
9
  import * as S from "./style";
10
10
 
11
- const Table = (props: IStateProps): JSX.Element => {
12
- const { items } = props;
11
+ const Table = (): JSX.Element => {
12
+ const items = useAppSelector((state) => state.sites.currentSiteLanguages);
13
13
 
14
14
  const mapItemList = (menu: any) =>
15
15
  menu && menu.map((item: ILanguage, i: number) => <Item key={`${item.label}${i}`} item={item} />);
@@ -22,12 +22,4 @@ const Table = (props: IStateProps): JSX.Element => {
22
22
  );
23
23
  };
24
24
 
25
- const mapStateToProps = (state: IRootState) => ({
26
- items: state.sites.currentSiteLanguages,
27
- });
28
-
29
- interface IStateProps {
30
- items: ILanguage[];
31
- }
32
-
33
- export default connect(mapStateToProps)(Table);
25
+ export default Table;
@@ -1,30 +1,37 @@
1
1
  import React, { useEffect } from "react";
2
- import { connect } from "react-redux";
3
2
 
4
- import type { INavItem, IRootState, ISite } from "@ax/types";
5
- import { sitesActions } from "@ax/containers/Sites";
3
+ import { ErrorToast, Loading, MainWrapper, Nav } from "@ax/components";
6
4
  import { appActions } from "@ax/containers/App";
7
- import { MainWrapper, Loading, ErrorToast, Nav } from "@ax/components";
5
+ import { sitesActions } from "@ax/containers/Sites";
6
+ import { useAppDispatch, useAppSelector } from "@ax/store";
7
+ import type { INavItem } from "@ax/types";
8
8
 
9
9
  import Table from "./Table";
10
10
 
11
11
  import * as S from "./style";
12
12
 
13
13
  const Languages = (props: IProps): JSX.Element => {
14
- const { isLoading, currentSiteInfo, getSiteLanguages, setHistoryPush, navItems, currentNavItem } = props;
14
+ const { navItems, currentNavItem } = props;
15
+
16
+ const dispatch = useAppDispatch();
17
+ const currentSiteInfo = useAppSelector((state) => state.sites.currentSiteInfo);
18
+ const isLoading = useAppSelector((state) => state.app.isLoading);
15
19
 
20
+ // getSiteLanguages and setHistoryPush are still legacy thunks from other areas
21
+ // (sites, app). The RTK dispatch takes them unchanged, which is what makes the
22
+ // area-by-area strangler possible.
16
23
  useEffect(() => {
17
24
  if (currentSiteInfo) {
18
- getSiteLanguages(currentSiteInfo.id);
25
+ dispatch(sitesActions.getSiteLanguages(currentSiteInfo.id));
19
26
  }
20
- }, [currentSiteInfo, getSiteLanguages]);
27
+ }, [currentSiteInfo, dispatch]);
21
28
 
22
29
  const handleMenuClick = (path: string) => {
23
- setHistoryPush(path);
30
+ dispatch(appActions.setHistoryPush(path));
24
31
  };
25
32
 
26
33
  return (
27
- <MainWrapper title={`Languages`}>
34
+ <MainWrapper title={`Languages`} screen="languages">
28
35
  <S.Wrapper>
29
36
  <Nav current={currentNavItem} items={navItems} onClick={handleMenuClick} />
30
37
  <S.ContentWrapper>
@@ -42,28 +49,9 @@ const Languages = (props: IProps): JSX.Element => {
42
49
  );
43
50
  };
44
51
 
45
- const mapStateToProps = (state: IRootState) => ({
46
- currentSiteInfo: state.sites.currentSiteInfo,
47
- isLoading: state.app.isLoading,
48
- });
49
-
50
- const mapDispatchToProps = {
51
- getSiteLanguages: sitesActions.getSiteLanguages,
52
- setHistoryPush: appActions.setHistoryPush,
53
- };
54
-
55
- interface IDispatchProps {
56
- getSiteLanguages(siteID: number): void;
57
- setHistoryPush(path: string, isEditor?: boolean): void;
58
- }
59
-
60
- interface ILanguagesProps {
61
- currentSiteInfo: ISite | null;
62
- isLoading: boolean;
52
+ interface IProps {
63
53
  navItems: INavItem[];
64
54
  currentNavItem: INavItem;
65
55
  }
66
56
 
67
- type IProps = ILanguagesProps & IDispatchProps;
68
-
69
- export default connect(mapStateToProps, mapDispatchToProps)(Languages);
57
+ export default Languages;