@griddo/ax 12.6.0 → 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/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@griddo/ax",
|
|
3
3
|
"description": "Griddo Author Experience",
|
|
4
|
-
"version": "12.6.
|
|
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>",
|
|
@@ -201,5 +201,5 @@
|
|
|
201
201
|
"publishConfig": {
|
|
202
202
|
"access": "public"
|
|
203
203
|
},
|
|
204
|
-
"gitHead": "
|
|
204
|
+
"gitHead": "1e6a3ad08eb17a4c57f80fffb65193a82a577969"
|
|
205
205
|
}
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Keeps the edit lock of the page being edited alive, renewing it every 30 seconds.
|
|
5
|
+
*
|
|
6
|
+
* sc-118275: this used to live inside each editor's mount effect, whose dependency list is empty,
|
|
7
|
+
* so the interval kept pinging whichever id it captured on mount. Creating a translation from
|
|
8
|
+
* inside the editor moves the store to the new page but never remounts, so the lock stayed on the
|
|
9
|
+
* original page for as long as the editor was open — and the translation, the one actually being
|
|
10
|
+
* edited, was left unlocked. Keeping pageID in the dependency list is what makes the heartbeat
|
|
11
|
+
* follow the page.
|
|
12
|
+
*
|
|
13
|
+
* The leading ping is not redundant with the interval: on a page switch the new id would otherwise
|
|
14
|
+
* sit unlocked for 30 seconds, because savePage dispatches setUserEditing(page.editing) with a null
|
|
15
|
+
* payload when nobody else holds the page, and the editors' [userEditing] effect only pings when
|
|
16
|
+
* that reference changes.
|
|
17
|
+
*
|
|
18
|
+
* sendPagePing has to keep a stable reference across renders; both editors get it from an object
|
|
19
|
+
* literal mapDispatchToProps, so connect binds it once. If it ever started changing on every
|
|
20
|
+
* render the effect would re-run, the cleanup would clear the interval before it could fire and
|
|
21
|
+
* there would be no heartbeat at all.
|
|
22
|
+
*/
|
|
23
|
+
const useEditingHeartbeat = (pageID: number | null, sendPagePing: (pageID: number) => void): void => {
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (!pageID) return;
|
|
26
|
+
sendPagePing(pageID);
|
|
27
|
+
const interval = setInterval(() => sendPagePing(pageID), 30000);
|
|
28
|
+
return () => clearInterval(interval);
|
|
29
|
+
}, [pageID, sendPagePing]);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export { useEditingHeartbeat };
|
package/src/hooks/index.tsx
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
usePrevious,
|
|
9
9
|
useShouldBeSaved,
|
|
10
10
|
} from "./forms";
|
|
11
|
+
import { useEditingHeartbeat } from "./heartbeat";
|
|
11
12
|
import { useOnMessageReceivedFromIframe, useOnMessageReceivedFromOutside } from "./iframe";
|
|
12
13
|
import { useURLSearchParam } from "./location";
|
|
13
14
|
import { useContextMenu, useHandleClickOutside, useModal, useModals, useToast } from "./modals";
|
|
@@ -29,6 +30,7 @@ export {
|
|
|
29
30
|
useContextMenu,
|
|
30
31
|
useDebounce,
|
|
31
32
|
useDebouncedCallback,
|
|
33
|
+
useEditingHeartbeat,
|
|
32
34
|
useEmptyState,
|
|
33
35
|
useEqualStructured,
|
|
34
36
|
useFirefoxScrollLock,
|
|
@@ -26,7 +26,7 @@ import { structuredDataActions } from "@ax/containers/StructuredData";
|
|
|
26
26
|
import { usersActions } from "@ax/containers/Users";
|
|
27
27
|
import { RouteLeavingGuard } from "@ax/guards";
|
|
28
28
|
import { dateToString, getDefaultTheme } from "@ax/helpers";
|
|
29
|
-
import { useIsDirty, useModals, usePermissionsForPage } from "@ax/hooks";
|
|
29
|
+
import { useEditingHeartbeat, useIsDirty, useModals, usePermissionsForPage } from "@ax/hooks";
|
|
30
30
|
import type {
|
|
31
31
|
HeadingFilter,
|
|
32
32
|
IErrorItem,
|
|
@@ -77,6 +77,7 @@ const GlobalEditor = (props: IProps) => {
|
|
|
77
77
|
schemaVersion,
|
|
78
78
|
updatePageAccessGrants,
|
|
79
79
|
accessGrants,
|
|
80
|
+
sendPagePing,
|
|
80
81
|
} = props;
|
|
81
82
|
|
|
82
83
|
const isAllowedTo = usePermissionsForPage(
|
|
@@ -153,7 +154,7 @@ const GlobalEditor = (props: IProps) => {
|
|
|
153
154
|
|
|
154
155
|
// biome-ignore lint/correctness/useExhaustiveDependencies: TODO: fix this
|
|
155
156
|
useEffect(() => {
|
|
156
|
-
const {
|
|
157
|
+
const { setStructuredDataFilter } = props;
|
|
157
158
|
|
|
158
159
|
editorContent?.structuredData && setStructuredDataFilter(editorContent.structuredData);
|
|
159
160
|
const handleGetPage = async () => await getPage(pageID, true);
|
|
@@ -163,13 +164,10 @@ const GlobalEditor = (props: IProps) => {
|
|
|
163
164
|
if (!pageID) {
|
|
164
165
|
setIsDirty(false);
|
|
165
166
|
}
|
|
166
|
-
|
|
167
|
-
const interval = setInterval(() => {
|
|
168
|
-
pageID && sendPagePing(pageID);
|
|
169
|
-
}, 30000);
|
|
170
|
-
return () => clearInterval(interval);
|
|
171
167
|
}, []);
|
|
172
168
|
|
|
169
|
+
useEditingHeartbeat(pageID, sendPagePing);
|
|
170
|
+
|
|
173
171
|
useEffect(() => {
|
|
174
172
|
setSelectedTab(defaultTab);
|
|
175
173
|
}, [defaultTab]);
|
|
@@ -181,7 +179,7 @@ const GlobalEditor = (props: IProps) => {
|
|
|
181
179
|
|
|
182
180
|
// biome-ignore lint/correctness/useExhaustiveDependencies: TODO: fix this
|
|
183
181
|
useEffect(() => {
|
|
184
|
-
const {
|
|
182
|
+
const { currentUserID } = props;
|
|
185
183
|
if (userEditing && userEditing.id !== currentUserID) {
|
|
186
184
|
setIsReadOnly(true);
|
|
187
185
|
!isOpen("userEditing") && toggleModal("userEditing");
|
|
@@ -24,7 +24,7 @@ import { pageStatus } from "@ax/containers/PageEditor/interfaces";
|
|
|
24
24
|
import { dataPacksActions } from "@ax/containers/Settings/DataPacks";
|
|
25
25
|
import { RouteLeavingGuard } from "@ax/guards";
|
|
26
26
|
import { dateToString, getDeactivatedModules, isModuleDisabled } from "@ax/helpers";
|
|
27
|
-
import { useIsDirty, useModals, usePermissionsForPage } from "@ax/hooks";
|
|
27
|
+
import { useEditingHeartbeat, useIsDirty, useModals, usePermissionsForPage } from "@ax/hooks";
|
|
28
28
|
import type {
|
|
29
29
|
HeadingFilter,
|
|
30
30
|
IErrorItem,
|
|
@@ -80,6 +80,7 @@ const PageEditor = (props: IProps) => {
|
|
|
80
80
|
currentSiteInfo,
|
|
81
81
|
updatePageAccessGrants,
|
|
82
82
|
accessGrants,
|
|
83
|
+
sendPagePing,
|
|
83
84
|
} = props;
|
|
84
85
|
|
|
85
86
|
const isAllowedTo = usePermissionsForPage(
|
|
@@ -158,8 +159,6 @@ const PageEditor = (props: IProps) => {
|
|
|
158
159
|
|
|
159
160
|
// biome-ignore lint/correctness/useExhaustiveDependencies: TODO: fix this
|
|
160
161
|
useEffect(() => {
|
|
161
|
-
const { pageID, getPage, sendPagePing } = props;
|
|
162
|
-
|
|
163
162
|
const handleGetPage = async () => {
|
|
164
163
|
if (isNewTranslation) {
|
|
165
164
|
await getDefaults();
|
|
@@ -175,20 +174,17 @@ const PageEditor = (props: IProps) => {
|
|
|
175
174
|
if (!pageID) {
|
|
176
175
|
setIsDirty(false);
|
|
177
176
|
}
|
|
178
|
-
|
|
179
|
-
const interval = setInterval(() => {
|
|
180
|
-
pageID && sendPagePing(pageID);
|
|
181
|
-
}, 30000);
|
|
182
|
-
return () => clearInterval(interval);
|
|
183
177
|
}, []);
|
|
184
178
|
|
|
179
|
+
useEditingHeartbeat(pageID, sendPagePing);
|
|
180
|
+
|
|
185
181
|
useEffect(() => {
|
|
186
182
|
setSelectedTab(defaultTab);
|
|
187
183
|
}, [defaultTab]);
|
|
188
184
|
|
|
189
185
|
// biome-ignore lint/correctness/useExhaustiveDependencies: TODO: fix this
|
|
190
186
|
useEffect(() => {
|
|
191
|
-
const {
|
|
187
|
+
const { currentUserID } = props;
|
|
192
188
|
if (userEditing && userEditing.id !== currentUserID) {
|
|
193
189
|
setIsReadOnly(true);
|
|
194
190
|
!isOpen("userEditing") && toggleModal("userEditing");
|