@griddo/ax 11.10.50 → 11.10.51
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/package.json +2 -2
- package/src/__tests__/modules/Settings/Social/Social.test.tsx +234 -0
- package/src/components/Fields/ComponentContainer/index.tsx +1 -0
- package/src/components/Fields/DateField/DatePickerInput/index.tsx +20 -6
- package/src/hooks/forms.tsx +19 -9
- package/src/modules/PageEditor/index.tsx +1 -1
- package/src/modules/Settings/Social/index.tsx +88 -58
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@griddo/ax",
|
|
3
3
|
"description": "Griddo Author Experience",
|
|
4
|
-
"version": "11.10.
|
|
4
|
+
"version": "11.10.51",
|
|
5
5
|
"authors": [
|
|
6
6
|
"Álvaro Sánchez' <alvaro.sanches@secuoyas.com>",
|
|
7
7
|
"Diego M. Béjar <diego.bejar@secuoyas.com>",
|
|
@@ -217,5 +217,5 @@
|
|
|
217
217
|
"publishConfig": {
|
|
218
218
|
"access": "public"
|
|
219
219
|
},
|
|
220
|
-
"gitHead": "
|
|
220
|
+
"gitHead": "9d1f19c90a45a5f03e5a2aaa6af48c244cac5b8f"
|
|
221
221
|
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { Router } from "react-router-dom";
|
|
2
|
+
|
|
3
|
+
import { parseTheme } from "@ax/helpers";
|
|
4
|
+
import Social from "@ax/modules/Settings/Social";
|
|
5
|
+
import history from "@ax/routes/history";
|
|
6
|
+
import globalTheme from "@ax/themes/theme.json";
|
|
7
|
+
|
|
8
|
+
import configureStore from "redux-mock-store";
|
|
9
|
+
import thunk from "redux-thunk";
|
|
10
|
+
import { ThemeProvider } from "styled-components";
|
|
11
|
+
|
|
12
|
+
import { act, cleanup, render, screen } from "../../../../../config/jest/test-utils";
|
|
13
|
+
|
|
14
|
+
afterEach(cleanup);
|
|
15
|
+
|
|
16
|
+
const mockComponent = () => null;
|
|
17
|
+
|
|
18
|
+
const mockNavItems = [
|
|
19
|
+
{ title: "Social", path: "/settings/social", component: mockComponent },
|
|
20
|
+
{ title: "Other", path: "/settings/other", component: mockComponent },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const mockCurrentNavItem = { title: "Social", path: "/settings/social", component: mockComponent };
|
|
24
|
+
|
|
25
|
+
const createMockStore = (overrides = {}) => {
|
|
26
|
+
const defaultStore = {
|
|
27
|
+
app: { isSaving: false },
|
|
28
|
+
social: {},
|
|
29
|
+
sites: { currentSiteInfo: { id: 1 } },
|
|
30
|
+
users: { currentPermissions: [] },
|
|
31
|
+
...overrides,
|
|
32
|
+
};
|
|
33
|
+
const middlewares: any = [thunk];
|
|
34
|
+
const mockStore = configureStore(middlewares);
|
|
35
|
+
return mockStore(defaultStore);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
describe("Social Settings Component", () => {
|
|
39
|
+
it("should render the Social settings component", async () => {
|
|
40
|
+
const store = createMockStore();
|
|
41
|
+
|
|
42
|
+
await act(async () => {
|
|
43
|
+
render(
|
|
44
|
+
<Router history={history}>
|
|
45
|
+
<ThemeProvider theme={parseTheme(globalTheme)}>
|
|
46
|
+
<Social navItems={mockNavItems} currentNavItem={mockCurrentNavItem} />
|
|
47
|
+
</ThemeProvider>
|
|
48
|
+
</Router>,
|
|
49
|
+
{ store },
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Check that the title is rendered
|
|
54
|
+
expect(screen.getByRole("heading", { name: "Social" })).toBeTruthy();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("should render social fields from state", async () => {
|
|
58
|
+
const store = createMockStore({
|
|
59
|
+
social: {
|
|
60
|
+
instagram: "https://instagram.com/user",
|
|
61
|
+
twitter: "https://twitter.com/user",
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
await act(async () => {
|
|
66
|
+
render(
|
|
67
|
+
<Router history={history}>
|
|
68
|
+
<ThemeProvider theme={parseTheme(globalTheme)}>
|
|
69
|
+
<Social navItems={mockNavItems} currentNavItem={mockCurrentNavItem} />
|
|
70
|
+
</ThemeProvider>
|
|
71
|
+
</Router>,
|
|
72
|
+
{ store },
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
expect(await screen.findByDisplayValue("https://instagram.com/user")).toBeTruthy();
|
|
77
|
+
expect(screen.getByDisplayValue("https://twitter.com/user")).toBeTruthy();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("should render hardcoded socials when they have values", async () => {
|
|
81
|
+
const store = createMockStore({
|
|
82
|
+
social: {
|
|
83
|
+
instagram: "https://instagram.com/user",
|
|
84
|
+
facebook: "",
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
await act(async () => {
|
|
89
|
+
render(
|
|
90
|
+
<Router history={history}>
|
|
91
|
+
<ThemeProvider theme={parseTheme(globalTheme)}>
|
|
92
|
+
<Social navItems={mockNavItems} currentNavItem={mockCurrentNavItem} />
|
|
93
|
+
</ThemeProvider>
|
|
94
|
+
</Router>,
|
|
95
|
+
{ store },
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Check that fields with values from social state are rendered
|
|
100
|
+
expect(screen.getByDisplayValue("https://instagram.com/user")).toBeTruthy();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("should have Instagram field visible", async () => {
|
|
104
|
+
const store = createMockStore({
|
|
105
|
+
social: {
|
|
106
|
+
instagram: "https://instagram.com/user",
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
await act(async () => {
|
|
111
|
+
render(
|
|
112
|
+
<Router history={history}>
|
|
113
|
+
<ThemeProvider theme={parseTheme(globalTheme)}>
|
|
114
|
+
<Social navItems={mockNavItems} currentNavItem={mockCurrentNavItem} />
|
|
115
|
+
</ThemeProvider>
|
|
116
|
+
</Router>,
|
|
117
|
+
{ store },
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Verify Instagram field is rendered with correct value
|
|
122
|
+
const instagramValue = screen.getByDisplayValue("https://instagram.com/user");
|
|
123
|
+
expect(instagramValue).toBeTruthy();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("should show button with correct state when form is not dirty", async () => {
|
|
127
|
+
const store = createMockStore({
|
|
128
|
+
social: {
|
|
129
|
+
instagram: "https://instagram.com/user",
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
await act(async () => {
|
|
134
|
+
render(
|
|
135
|
+
<Router history={history}>
|
|
136
|
+
<ThemeProvider theme={parseTheme(globalTheme)}>
|
|
137
|
+
<Social navItems={mockNavItems} currentNavItem={mockCurrentNavItem} />
|
|
138
|
+
</ThemeProvider>
|
|
139
|
+
</Router>,
|
|
140
|
+
{ store },
|
|
141
|
+
);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Should have a button (the save button) that is disabled when not dirty
|
|
145
|
+
const buttons = screen.getAllByRole("button");
|
|
146
|
+
const saveButton = buttons.find((btn) => btn.textContent?.includes("Save"));
|
|
147
|
+
expect(saveButton).toBeTruthy();
|
|
148
|
+
expect(saveButton?.hasAttribute("disabled")).toBe(true);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("should display saving state", async () => {
|
|
152
|
+
const store = createMockStore({
|
|
153
|
+
app: { isSaving: true },
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
await act(async () => {
|
|
157
|
+
render(
|
|
158
|
+
<Router history={history}>
|
|
159
|
+
<ThemeProvider theme={parseTheme(globalTheme)}>
|
|
160
|
+
<Social navItems={mockNavItems} currentNavItem={mockCurrentNavItem} />
|
|
161
|
+
</ThemeProvider>
|
|
162
|
+
</Router>,
|
|
163
|
+
{ store },
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const saveButton = screen.getByRole("button", { name: /saving/i });
|
|
168
|
+
expect(saveButton.textContent).toBe("Saving");
|
|
169
|
+
expect(saveButton.hasAttribute("disabled")).toBe(true);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("should render instance socials from config", async () => {
|
|
173
|
+
const store = createMockStore({
|
|
174
|
+
social: {
|
|
175
|
+
customSocial: "https://example.com",
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
await act(async () => {
|
|
180
|
+
render(
|
|
181
|
+
<Router history={history}>
|
|
182
|
+
<ThemeProvider theme={parseTheme(globalTheme)}>
|
|
183
|
+
<Social navItems={mockNavItems} currentNavItem={mockCurrentNavItem} />
|
|
184
|
+
</ThemeProvider>
|
|
185
|
+
</Router>,
|
|
186
|
+
{ store },
|
|
187
|
+
);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// The component should render without errors even if config.schemas.config.socials is empty
|
|
191
|
+
expect(screen.getByRole("heading", { name: "Social" })).toBeTruthy();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("should not duplicate socials between instance and hardcoded", async () => {
|
|
195
|
+
const store = createMockStore({
|
|
196
|
+
social: {
|
|
197
|
+
instagram: "https://instagram.com/user",
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
await act(async () => {
|
|
202
|
+
render(
|
|
203
|
+
<Router history={history}>
|
|
204
|
+
<ThemeProvider theme={parseTheme(globalTheme)}>
|
|
205
|
+
<Social navItems={mockNavItems} currentNavItem={mockCurrentNavItem} />
|
|
206
|
+
</ThemeProvider>
|
|
207
|
+
</Router>,
|
|
208
|
+
{ store },
|
|
209
|
+
);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// Instagram should only appear once (from instance or hardcoded, not both)
|
|
213
|
+
const instagramLabels = screen.queryAllByText(/instagram/i);
|
|
214
|
+
expect(instagramLabels.length).toBeGreaterThanOrEqual(1);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("should handle nav menu clicks with dispatch", async () => {
|
|
218
|
+
const store = createMockStore();
|
|
219
|
+
|
|
220
|
+
await act(async () => {
|
|
221
|
+
render(
|
|
222
|
+
<Router history={history}>
|
|
223
|
+
<ThemeProvider theme={parseTheme(globalTheme)}>
|
|
224
|
+
<Social navItems={mockNavItems} currentNavItem={mockCurrentNavItem} />
|
|
225
|
+
</ThemeProvider>
|
|
226
|
+
</Router>,
|
|
227
|
+
{ store },
|
|
228
|
+
);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// Component should render the Nav component which handles menu clicks
|
|
232
|
+
expect(screen.getByRole("heading", { name: "Social" })).toBeTruthy();
|
|
233
|
+
});
|
|
234
|
+
});
|
|
@@ -51,6 +51,7 @@ const ComponentContainer = (props: IComponentContainerProps): JSX.Element => {
|
|
|
51
51
|
|
|
52
52
|
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({
|
|
53
53
|
id: editorID,
|
|
54
|
+
disabled: !!disabled,
|
|
54
55
|
});
|
|
55
56
|
|
|
56
57
|
const whiteListFirstItem: string | undefined = whiteList?.[0];
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import React
|
|
2
|
-
|
|
3
|
-
import { isValidDate, stringToDate, getStringifyDateRange, getRange, isValidDateRange } from "@ax/helpers";
|
|
1
|
+
import type React from "react";
|
|
2
|
+
import { forwardRef, useEffect, useState } from "react";
|
|
4
3
|
|
|
5
4
|
import { Icon, IconAction } from "@ax/components";
|
|
5
|
+
import { getRange, getStringifyDateRange, isValidDate, isValidDateRange, stringToDate } from "@ax/helpers";
|
|
6
6
|
|
|
7
7
|
import * as S from "./style";
|
|
8
8
|
|
|
9
|
-
const DatePickerInput = (props: IDatePickerProps
|
|
9
|
+
const DatePickerInput = (props: IDatePickerProps): JSX.Element => {
|
|
10
10
|
const { dates, onClick, handleChange, isOpen, error, disabled, handleValidation, validators } = props;
|
|
11
11
|
const { start, end } = dates;
|
|
12
12
|
const placeholder = "dd/mm/yyyy";
|
|
@@ -14,7 +14,8 @@ const DatePickerInput = (props: IDatePickerProps, ref: any): JSX.Element => {
|
|
|
14
14
|
const currentDate = getStringifyDateRange(start, end);
|
|
15
15
|
|
|
16
16
|
const [value, setValue] = useState(currentDate);
|
|
17
|
-
|
|
17
|
+
|
|
18
|
+
useEffect(() => setValue(currentDate), [currentDate]);
|
|
18
19
|
|
|
19
20
|
const handleOnChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
20
21
|
const inputValue = e.target.value;
|
|
@@ -36,10 +37,22 @@ const DatePickerInput = (props: IDatePickerProps, ref: any): JSX.Element => {
|
|
|
36
37
|
}
|
|
37
38
|
};
|
|
38
39
|
|
|
40
|
+
const handleOnKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
41
|
+
const allowed = /^[0-9/\- ]$/;
|
|
42
|
+
const isControlKey = e.key.length > 1; // Backspace, Delete, ArrowLeft, Tab, etc.
|
|
43
|
+
|
|
44
|
+
// Detecta si se está usando Ctrl (Windows) o Cmd (Mac) para permitir atajos
|
|
45
|
+
const isKeyboardShortcut = e.ctrlKey || e.metaKey;
|
|
46
|
+
|
|
47
|
+
if (!isControlKey && !isKeyboardShortcut && !allowed.test(e.key)) {
|
|
48
|
+
e.preventDefault();
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
39
52
|
const handleOnBlur = (e: React.FocusEvent<HTMLInputElement>) => {
|
|
40
53
|
const inputValue = e.target.value;
|
|
41
54
|
|
|
42
|
-
handleValidation
|
|
55
|
+
handleValidation?.(inputValue, validators);
|
|
43
56
|
};
|
|
44
57
|
|
|
45
58
|
const CalendarIcon = <IconAction icon="calendar" onClick={onClick} disabled={disabled} />;
|
|
@@ -55,6 +68,7 @@ const DatePickerInput = (props: IDatePickerProps, ref: any): JSX.Element => {
|
|
|
55
68
|
value={value || ""}
|
|
56
69
|
placeholder={placeholder}
|
|
57
70
|
onChange={handleOnChange}
|
|
71
|
+
onKeyDown={handleOnKeyDown}
|
|
58
72
|
onBlur={handleOnBlur}
|
|
59
73
|
disabled={disabled}
|
|
60
74
|
/>
|
package/src/hooks/forms.tsx
CHANGED
|
@@ -37,24 +37,32 @@ const useEqualStructured = (component: any) => {
|
|
|
37
37
|
/**
|
|
38
38
|
* Stores the provided value (object or string) and returns the previous value,
|
|
39
39
|
* updating it only when a "saved" state is detected (either from the external state
|
|
40
|
-
* manager, or explicitly via
|
|
41
|
-
* and implementing dirty checks.
|
|
40
|
+
* manager, or explicitly via options). Useful for detecting changes and implementing dirty checks.
|
|
42
41
|
*
|
|
43
42
|
* @param value - The current value to track.
|
|
44
|
-
* @param
|
|
43
|
+
* @param options - Optional configuration object.
|
|
44
|
+
* @param options.isSaved - Flag to indicate if the value has been saved/reset externally.
|
|
45
|
+
* @param options.force - Force update of the stored value on next render.
|
|
45
46
|
* @returns The previously saved value, or undefined for first render.
|
|
46
47
|
*/
|
|
47
|
-
const usePrevious = (
|
|
48
|
+
const usePrevious = (
|
|
49
|
+
value: Record<string, unknown> | string,
|
|
50
|
+
options?: {
|
|
51
|
+
isSaved?: boolean;
|
|
52
|
+
force?: boolean;
|
|
53
|
+
},
|
|
54
|
+
) => {
|
|
55
|
+
const { isSaved, force } = options || {};
|
|
48
56
|
const valueStr = value && JSON.stringify(value);
|
|
49
57
|
const ref = useRef<Record<string, unknown> | string | undefined>(undefined);
|
|
50
58
|
const isSavedData = getIsSavedData();
|
|
51
59
|
|
|
52
60
|
useEffect(() => {
|
|
53
61
|
const currentValue = valueStr && JSON.parse(valueStr);
|
|
54
|
-
if (!ref.current || isEmptyObj(ref.current)) {
|
|
62
|
+
if (!ref.current || isEmptyObj(ref.current) || force) {
|
|
55
63
|
ref.current = currentValue;
|
|
56
64
|
}
|
|
57
|
-
}, [valueStr]);
|
|
65
|
+
}, [valueStr, force]);
|
|
58
66
|
|
|
59
67
|
// biome-ignore lint/correctness/useExhaustiveDependencies: TODO fix this
|
|
60
68
|
useEffect(() => {
|
|
@@ -73,8 +81,9 @@ const useIsDirty = (
|
|
|
73
81
|
const [isDirty, setIsDirty] = useState(false);
|
|
74
82
|
const [isSaved, setIsSaved] = useState(false);
|
|
75
83
|
const [isResetting, setIsResetting] = useState(false);
|
|
84
|
+
const [force, setForce] = useState(false);
|
|
76
85
|
|
|
77
|
-
const prevContent = usePrevious(updatedValues, isSaved);
|
|
86
|
+
const prevContent = usePrevious(updatedValues, { isSaved, force });
|
|
78
87
|
|
|
79
88
|
const hasChanged = (): boolean => {
|
|
80
89
|
if (prevContent && updatedValues) {
|
|
@@ -90,10 +99,11 @@ const useIsDirty = (
|
|
|
90
99
|
return false;
|
|
91
100
|
};
|
|
92
101
|
|
|
93
|
-
const resetDirty = (reseting = true) => {
|
|
102
|
+
const resetDirty = (reseting = true, force = false) => {
|
|
94
103
|
setIsDirty(false);
|
|
95
104
|
setIsSaved(true);
|
|
96
105
|
reseting && setIsResetting(true);
|
|
106
|
+
setForce(force);
|
|
97
107
|
};
|
|
98
108
|
|
|
99
109
|
// biome-ignore lint/correctness/useExhaustiveDependencies: TODO: fix this
|
|
@@ -156,4 +166,4 @@ const useShouldBeSaved = (form: Record<string, unknown> | IUser | FormContent) =
|
|
|
156
166
|
return { isDirty, setIsDirty };
|
|
157
167
|
};
|
|
158
168
|
|
|
159
|
-
export { useDebounce, useEqualStructured,
|
|
169
|
+
export { useDebounce, useEqualStructured, useIsDirty, usePrevious, useShouldBeSaved };
|
|
@@ -483,7 +483,7 @@ const PageEditor = (props: IProps) => {
|
|
|
483
483
|
|
|
484
484
|
const toggleDraftPage = async () => {
|
|
485
485
|
const { getPage } = props;
|
|
486
|
-
resetDirty();
|
|
486
|
+
resetDirty(undefined, true);
|
|
487
487
|
const pageID = isDraft ? editorContent.draftFromPage : editorContent.haveDraftPage;
|
|
488
488
|
await getPage(pageID);
|
|
489
489
|
};
|
|
@@ -1,54 +1,102 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
//
|
|
2
|
+
// Refactor: Migrado de connect() HOC a hooks de Redux
|
|
3
|
+
//
|
|
4
|
+
// ANTES: Patrón connect() HOC
|
|
5
|
+
// ├─ interface IProps {
|
|
6
|
+
// │ ├─ isSaving, socialInfo (state)
|
|
7
|
+
// │ └─ saveSocial, getSocial, setHistoryPush (dispatch)
|
|
8
|
+
// │
|
|
9
|
+
// ├─ mapStateToProps: Selectores del estado
|
|
10
|
+
// ├─ mapDispatchToProps: Acciones despachadas
|
|
11
|
+
// └─ export default connect(mapStateToProps, mapDispatchToProps)(Social)
|
|
12
|
+
// └─ El HOC inyecta props automáticamente
|
|
13
|
+
//
|
|
14
|
+
// AHORA: Hooks de Redux
|
|
15
|
+
// ├─ interface IProps { navItems, currentNavItem } (solo props propias)
|
|
16
|
+
// ├─ useDispatch() → acceso directo al dispatch
|
|
17
|
+
// ├─ useSelector() → selectores granulares dentro del componente
|
|
18
|
+
// │ ├─ state.app.isSaving
|
|
19
|
+
// │ └─ state.social
|
|
20
|
+
// └─ export default Social (sin HOC)
|
|
21
|
+
//
|
|
22
|
+
// CAMBIOS ESPECÍFICOS:
|
|
23
|
+
// • Removida toda la lógica de mapeo de props Redux
|
|
24
|
+
// • dispatch() llamado directamente en setRoute() y handleMenuClick()
|
|
25
|
+
// • Mejor legibilidad: el estado necesario está cerca de donde se usa
|
|
26
|
+
// • Menor acoplamiento: el componente no depende de una estructura de props fija
|
|
27
|
+
//
|
|
28
|
+
|
|
29
|
+
import { useEffect, useState } from "react";
|
|
30
|
+
import { useDispatch, useSelector } from "react-redux";
|
|
31
|
+
|
|
32
|
+
import { ErrorToast, FieldsBehavior, MainWrapper, Nav } from "@ax/components";
|
|
33
|
+
import { appActions } from "@ax/containers/App";
|
|
34
|
+
import { socialActions } from "@ax/containers/Settings/Social";
|
|
5
35
|
import { RouteLeavingGuard } from "@ax/guards";
|
|
36
|
+
import { capitalize } from "@ax/helpers";
|
|
6
37
|
import { useIsDirty } from "@ax/hooks";
|
|
38
|
+
import type { INavItem, IRootState } from "@ax/types";
|
|
7
39
|
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
40
|
+
import { config } from "components";
|
|
41
|
+
import type { AnyAction } from "redux";
|
|
42
|
+
import type { ThunkDispatch } from "redux-thunk";
|
|
11
43
|
|
|
12
44
|
import * as S from "./style";
|
|
13
45
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const socialFields: { name: string; title: string }[] = [
|
|
20
|
-
{ name: "instagram", title: "Instagram URL" },
|
|
21
|
-
{ name: "linkedIn", title: "LinkedIn URL" },
|
|
22
|
-
{ name: "facebook", title: "Facebook URL" },
|
|
23
|
-
{ name: "twitter", title: "X/Twitter URL" },
|
|
24
|
-
{ name: "youtube", title: "YouTube URL" },
|
|
25
|
-
{ name: "flickr", title: "Flickr URL" },
|
|
26
|
-
{ name: "tiktok", title: "Tiktok URL" },
|
|
27
|
-
{ name: "snapchat", title: "Snapchat URL" },
|
|
28
|
-
{ name: "newsletter", title: "Newsletter URL" },
|
|
29
|
-
{ name: "podcast", title: "Podcast URL" },
|
|
30
|
-
];
|
|
46
|
+
interface IProps {
|
|
47
|
+
navItems: INavItem[];
|
|
48
|
+
currentNavItem: INavItem;
|
|
49
|
+
}
|
|
31
50
|
|
|
32
|
-
|
|
33
|
-
|
|
51
|
+
const instanceSocials: string[] = config.schemas.config.socials || [];
|
|
52
|
+
const hardcodedSocials = [
|
|
53
|
+
{ name: "instagram", title: "Instagram URL" },
|
|
54
|
+
{ name: "linkedIn", title: "LinkedIn URL" },
|
|
55
|
+
{ name: "facebook", title: "Facebook URL" },
|
|
56
|
+
{ name: "twitter", title: "X/Twitter URL" },
|
|
57
|
+
{ name: "youtube", title: "YouTube URL" },
|
|
58
|
+
{ name: "flickr", title: "Flickr URL" },
|
|
59
|
+
{ name: "tiktok", title: "Tiktok URL" },
|
|
60
|
+
{ name: "snapchat", title: "Snapchat URL" },
|
|
61
|
+
{ name: "newsletter", title: "Newsletter URL" },
|
|
62
|
+
{ name: "podcast", title: "Podcast URL" },
|
|
63
|
+
];
|
|
34
64
|
|
|
65
|
+
const Social = (props: IProps): JSX.Element => {
|
|
66
|
+
const { navItems, currentNavItem } = props;
|
|
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);
|
|
70
|
+
const [socialForm, setSocialForm] = useState<Record<string, string>>({});
|
|
71
|
+
const [serverFields, setServerFields] = useState<Set<string>>(new Set());
|
|
35
72
|
const { isDirty, resetDirty, setIsDirty } = useIsDirty(socialForm);
|
|
36
73
|
|
|
37
|
-
|
|
74
|
+
const socialFields = [
|
|
75
|
+
...instanceSocials.map((name) => ({ name, title: `${capitalize(name)} URL` })),
|
|
76
|
+
...hardcodedSocials.filter(
|
|
77
|
+
(s) => !instanceSocials.includes(s.name) && (serverFields.has(s.name) || !!socialForm[s.name]),
|
|
78
|
+
),
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
const changeSocialForm = (field: string, value: string) => {
|
|
82
|
+
setSocialForm((state) => ({ ...state, [field]: value }));
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: Can't add resetDirty to dependencies because it causes an infinite loop
|
|
38
86
|
useEffect(() => {
|
|
39
|
-
getSocial();
|
|
87
|
+
dispatch(socialActions.getSocial());
|
|
40
88
|
resetDirty();
|
|
41
|
-
}, []);
|
|
89
|
+
}, [dispatch]);
|
|
42
90
|
|
|
43
|
-
// biome-ignore lint/correctness/useExhaustiveDependencies: TODO: fix this
|
|
44
91
|
useEffect(() => {
|
|
45
92
|
setSocialForm({ ...socialInfo });
|
|
93
|
+
setServerFields(new Set(Object.keys(socialInfo).filter((k) => !!socialInfo[k])));
|
|
46
94
|
setIsDirty(false);
|
|
47
|
-
}, [socialInfo]);
|
|
95
|
+
}, [socialInfo, setIsDirty]);
|
|
48
96
|
|
|
49
97
|
const saveForm = async () => {
|
|
50
|
-
const isSaved = await saveSocial(socialForm);
|
|
51
|
-
if (isSaved)
|
|
98
|
+
const isSaved = await dispatch(socialActions.saveSocial(socialForm));
|
|
99
|
+
if (isSaved) resetDirty();
|
|
52
100
|
};
|
|
53
101
|
|
|
54
102
|
const rightButtonProps = {
|
|
@@ -57,7 +105,10 @@ const Social = (props: IProps): JSX.Element => {
|
|
|
57
105
|
action: saveForm,
|
|
58
106
|
};
|
|
59
107
|
|
|
60
|
-
const setRoute = (path: string) =>
|
|
108
|
+
const setRoute = (path: string) => {
|
|
109
|
+
dispatch(appActions.setHistoryPush(path));
|
|
110
|
+
};
|
|
111
|
+
|
|
61
112
|
const modalText = (
|
|
62
113
|
<>
|
|
63
114
|
Some settings <strong>are not saved</strong>.{" "}
|
|
@@ -65,13 +116,13 @@ const Social = (props: IProps): JSX.Element => {
|
|
|
65
116
|
);
|
|
66
117
|
|
|
67
118
|
const handleMenuClick = (path: string) => {
|
|
68
|
-
setHistoryPush(path);
|
|
119
|
+
dispatch(appActions.setHistoryPush(path));
|
|
69
120
|
};
|
|
70
121
|
|
|
71
122
|
return (
|
|
72
123
|
<>
|
|
73
124
|
<RouteLeavingGuard when={isDirty} action={setRoute} text={modalText} />
|
|
74
|
-
<MainWrapper backLink={false} title=
|
|
125
|
+
<MainWrapper backLink={false} title="Social" rightButton={rightButtonProps}>
|
|
75
126
|
<S.Wrapper>
|
|
76
127
|
<Nav current={currentNavItem} items={navItems} onClick={handleMenuClick} />
|
|
77
128
|
<S.ContentWrapper>
|
|
@@ -95,25 +146,4 @@ const Social = (props: IProps): JSX.Element => {
|
|
|
95
146
|
);
|
|
96
147
|
};
|
|
97
148
|
|
|
98
|
-
|
|
99
|
-
isSaving: boolean;
|
|
100
|
-
socialInfo: ISocialState;
|
|
101
|
-
navItems: INavItem[];
|
|
102
|
-
currentNavItem: INavItem;
|
|
103
|
-
saveSocial: (form: Record<string, string>) => Promise<boolean>;
|
|
104
|
-
getSocial(): void;
|
|
105
|
-
setHistoryPush(path: string, isEditor?: boolean): void;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const mapDispatchToProps = {
|
|
109
|
-
setHistoryPush: appActions.setHistoryPush,
|
|
110
|
-
saveSocial: socialActions.saveSocial,
|
|
111
|
-
getSocial: socialActions.getSocial,
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
const mapStateToProps = (state: IRootState) => ({
|
|
115
|
-
isSaving: state.app.isSaving,
|
|
116
|
-
socialInfo: state.social,
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
export default connect(mapStateToProps, mapDispatchToProps)(Social);
|
|
149
|
+
export default Social;
|