@carlonicora/nextjs-jsonapi 6.12.2 → 6.13.0
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/dist/{ApiData-DPKNfY-9.d.mts → ApiData-Be8yxCOX.d.mts} +2 -0
- package/dist/{ApiData-DPKNfY-9.d.ts → ApiData-Be8yxCOX.d.ts} +2 -0
- package/dist/{BlockNoteEditor-75ED2CN2.mjs → BlockNoteEditor-22Q4DJBU.mjs} +20 -8
- package/dist/BlockNoteEditor-22Q4DJBU.mjs.map +1 -0
- package/dist/{BlockNoteEditor-7M5QW2YQ.js → BlockNoteEditor-2VLPSEKS.js} +34 -22
- package/dist/BlockNoteEditor-2VLPSEKS.js.map +1 -0
- package/dist/{BlockNoteViewer-WGCDB7IJ.js → BlockNoteViewer-2XYASA5Y.js} +8 -5
- package/dist/BlockNoteViewer-2XYASA5Y.js.map +1 -0
- package/dist/{BlockNoteViewer-MTGS6YJV.mjs → BlockNoteViewer-NBW2H6LC.mjs} +7 -4
- package/dist/{BlockNoteViewer-MTGS6YJV.mjs.map → BlockNoteViewer-NBW2H6LC.mjs.map} +1 -1
- package/dist/{JsonApiRequest-A66XK5WT.mjs → JsonApiRequest-F6TMJ42G.mjs} +2 -2
- package/dist/JsonApiRequest-WISJZSDE.js +26 -0
- package/dist/{JsonApiRequest-FZGPLNUF.js.map → JsonApiRequest-WISJZSDE.js.map} +1 -1
- package/dist/billing/index.js +499 -494
- package/dist/billing/index.js.map +1 -1
- package/dist/billing/index.mjs +13 -8
- package/dist/billing/index.mjs.map +1 -1
- package/dist/chunk-27LQDGNP.mjs +317 -0
- package/dist/chunk-27LQDGNP.mjs.map +1 -0
- package/dist/{chunk-V3YDVJ63.js → chunk-2H342HNN.js} +948 -921
- package/dist/chunk-2H342HNN.js.map +1 -0
- package/dist/{chunk-KSYQEKJX.js → chunk-2SV4SS6N.js} +14 -14
- package/dist/{chunk-KSYQEKJX.js.map → chunk-2SV4SS6N.js.map} +1 -1
- package/dist/{chunk-4FNGWALA.mjs → chunk-47BBY5KA.mjs} +6 -4
- package/dist/{chunk-4FNGWALA.mjs.map → chunk-47BBY5KA.mjs.map} +1 -1
- package/dist/chunk-6JUCIWDC.mjs +570 -0
- package/dist/chunk-6JUCIWDC.mjs.map +1 -0
- package/dist/chunk-7DBJTNSU.mjs +1049 -0
- package/dist/chunk-7DBJTNSU.mjs.map +1 -0
- package/dist/chunk-7JZHPMGW.mjs +704 -0
- package/dist/chunk-7JZHPMGW.mjs.map +1 -0
- package/dist/{chunk-L6PYFQTO.js → chunk-7ZHTPFFT.js} +16 -14
- package/dist/chunk-7ZHTPFFT.js.map +1 -0
- package/dist/chunk-B6JGKDP2.js +214 -0
- package/dist/chunk-B6JGKDP2.js.map +1 -0
- package/dist/{chunk-3YW35YHQ.mjs → chunk-BH6JWIKI.mjs} +5 -5
- package/dist/chunk-BZ5J2FIV.js +570 -0
- package/dist/chunk-BZ5J2FIV.js.map +1 -0
- package/dist/{chunk-V5MI7RNB.mjs → chunk-DGWOVYEC.mjs} +2 -2
- package/dist/chunk-DOCIFE6A.mjs +212 -0
- package/dist/chunk-DOCIFE6A.mjs.map +1 -0
- package/dist/chunk-GB6DBFYY.js +25 -0
- package/dist/chunk-GB6DBFYY.js.map +1 -0
- package/dist/{chunk-RXQYEEKZ.mjs → chunk-H4NLDX5G.mjs} +51 -24
- package/dist/chunk-H4NLDX5G.mjs.map +1 -0
- package/dist/{chunk-EWCY6QJA.mjs → chunk-JJMMT24D.mjs} +37 -3
- package/dist/chunk-JJMMT24D.mjs.map +1 -0
- package/dist/{chunk-JTPRJNT2.js → chunk-KMW7UNL2.js} +38 -3
- package/dist/chunk-KMW7UNL2.js.map +1 -0
- package/dist/chunk-MHB36MYE.js +317 -0
- package/dist/chunk-MHB36MYE.js.map +1 -0
- package/dist/{chunk-FF4EIQ6H.js → chunk-MOGEGWXW.js} +1039 -1680
- package/dist/chunk-MOGEGWXW.js.map +1 -0
- package/dist/{chunk-AA52ORW2.mjs → chunk-P445QGOC.mjs} +61 -702
- package/dist/chunk-P445QGOC.mjs.map +1 -0
- package/dist/chunk-PV4Z5W6V.js +212 -0
- package/dist/chunk-PV4Z5W6V.js.map +1 -0
- package/dist/chunk-PXDUJ47N.js +1049 -0
- package/dist/chunk-PXDUJ47N.js.map +1 -0
- package/dist/{chunk-GHE4VT2R.js → chunk-QKNGZDSQ.js} +2 -2
- package/dist/{chunk-GHE4VT2R.js.map → chunk-QKNGZDSQ.js.map} +1 -1
- package/dist/chunk-W62MHDWM.js +704 -0
- package/dist/chunk-W62MHDWM.js.map +1 -0
- package/dist/chunk-XXP3TQHF.mjs +25 -0
- package/dist/chunk-XXP3TQHF.mjs.map +1 -0
- package/dist/chunk-YRKIFYOE.mjs +214 -0
- package/dist/chunk-YRKIFYOE.mjs.map +1 -0
- package/dist/{chunk-O3PDL2CV.js → chunk-Z4POQWFG.js} +40 -6
- package/dist/chunk-Z4POQWFG.js.map +1 -0
- package/dist/{chunk-PI56A4DG.mjs → chunk-ZLTOUZN2.mjs} +37 -2
- package/dist/chunk-ZLTOUZN2.mjs.map +1 -0
- package/dist/client/index.d.mts +2 -2
- package/dist/client/index.d.ts +2 -2
- package/dist/client/index.js +14 -9
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +24 -19
- package/dist/components/index.d.mts +7 -1
- package/dist/components/index.d.ts +7 -1
- package/dist/components/index.js +11 -7
- package/dist/components/index.js.map +1 -1
- package/dist/components/index.mjs +10 -6
- package/dist/contexts/index.d.mts +1 -1
- package/dist/contexts/index.d.ts +1 -1
- package/dist/contexts/index.js +11 -7
- package/dist/contexts/index.js.map +1 -1
- package/dist/contexts/index.mjs +10 -6
- package/dist/core/index.d.mts +128 -2
- package/dist/core/index.d.ts +128 -2
- package/dist/core/index.js +17 -4
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +22 -9
- package/dist/email-automations/index.js +101 -96
- package/dist/email-automations/index.js.map +1 -1
- package/dist/email-automations/index.mjs +13 -8
- package/dist/email-automations/index.mjs.map +1 -1
- package/dist/features/analytics/index.js +128 -123
- package/dist/features/analytics/index.js.map +1 -1
- package/dist/features/analytics/index.mjs +13 -8
- package/dist/features/analytics/index.mjs.map +1 -1
- package/dist/features/analytics/server.js +11 -8
- package/dist/features/analytics/server.js.map +1 -1
- package/dist/features/analytics/server.mjs +9 -6
- package/dist/features/analytics/server.mjs.map +1 -1
- package/dist/features/blog/index.js +158 -153
- package/dist/features/blog/index.js.map +1 -1
- package/dist/features/blog/index.mjs +14 -9
- package/dist/features/blog/index.mjs.map +1 -1
- package/dist/features/blog/server.js +30 -26
- package/dist/features/blog/server.js.map +1 -1
- package/dist/features/blog/server.mjs +10 -6
- package/dist/features/blog/server.mjs.map +1 -1
- package/dist/features/help/index.js +44 -40
- package/dist/features/help/index.js.map +1 -1
- package/dist/features/help/index.mjs +10 -6
- package/dist/features/help/index.mjs.map +1 -1
- package/dist/features/tokenusage/index.js +134 -129
- package/dist/features/tokenusage/index.js.map +1 -1
- package/dist/features/tokenusage/index.mjs +13 -8
- package/dist/features/tokenusage/index.mjs.map +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +18 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +24 -11
- package/dist/lifecycle-4GUOTB46.js +23 -0
- package/dist/lifecycle-4GUOTB46.js.map +1 -0
- package/dist/lifecycle-LKGPRBS7.mjs +23 -0
- package/dist/local-transport-7H3SPZSA.js +15 -0
- package/dist/local-transport-7H3SPZSA.js.map +1 -0
- package/dist/local-transport-QYFKA6AN.mjs +15 -0
- package/dist/local-transport-QYFKA6AN.mjs.map +1 -0
- package/dist/offline/index.d.mts +607 -0
- package/dist/offline/index.d.ts +607 -0
- package/dist/offline/index.js +1052 -0
- package/dist/offline/index.js.map +1 -0
- package/dist/offline/index.mjs +1052 -0
- package/dist/offline/index.mjs.map +1 -0
- package/dist/offline/sqlite/device-worker.mjs +62 -0
- package/dist/offline/sqlite/index.mjs +17392 -0
- package/dist/offline/sqlite/sqlite3-opfs-async-proxy.js +881 -0
- package/dist/offline/sqlite/sqlite3.wasm +0 -0
- package/dist/request-KTBV6HHH.js +10 -0
- package/dist/request-KTBV6HHH.js.map +1 -0
- package/dist/request-SQOUNWZ4.mjs +10 -0
- package/dist/request-SQOUNWZ4.mjs.map +1 -0
- package/dist/server/cachedGet.d.mts +1 -1
- package/dist/server/cachedGet.d.ts +1 -1
- package/dist/server/cachedGet.stub.d.mts +1 -1
- package/dist/server/cachedGet.stub.d.ts +1 -1
- package/dist/server/index.d.mts +28 -2
- package/dist/server/index.d.ts +28 -2
- package/dist/server/index.js +18 -8
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +18 -8
- package/dist/{useSocket-IRHtk2xc.d.ts → useSocket-Bo9M0t0J.d.ts} +3 -1
- package/dist/{useSocket-C8fqU4od.d.mts → useSocket-J5qanQPN.d.mts} +3 -1
- package/dist/write-through-FDQ7N4YI.js +15 -0
- package/dist/write-through-FDQ7N4YI.js.map +1 -0
- package/dist/write-through-R25VQMH6.mjs +15 -0
- package/dist/write-through-R25VQMH6.mjs.map +1 -0
- package/package.json +7 -1
- package/pwa/sw.js +277 -26
- package/src/client/request.ts +37 -0
- package/src/components/editors/BlockNoteEditor.tsx +14 -0
- package/src/components/editors/__tests__/BlockNoteEditor.empty-change.spec.tsx +205 -0
- package/src/contexts/SocketContext.tsx +7 -1
- package/src/contexts/__tests__/SocketContext.session.spec.tsx +52 -5
- package/src/core/factories/RehydrationFactory.ts +28 -8
- package/src/core/factories/__tests__/RehydrationFactory.spec.ts +33 -0
- package/src/core/index.ts +3 -0
- package/src/core/interfaces/ApiData.ts +2 -0
- package/src/core/registry/ModuleRegistry.ts +3 -0
- package/src/features/auth/components/forms/Logout.tsx +8 -1
- package/src/features/auth/components/forms/__tests__/Logout.spec.tsx +62 -0
- package/src/hooks/__tests__/useSocket.pause.spec.ts +39 -0
- package/src/hooks/useSocket.ts +20 -1
- package/src/offline/__tests__/answer-builders.spec.ts +176 -0
- package/src/offline/__tests__/device-switch.spec.ts +179 -0
- package/src/offline/__tests__/generic-answers.spec.ts +380 -0
- package/src/offline/__tests__/local-transport.spec.ts +150 -0
- package/src/offline/__tests__/logout.spec.ts +122 -0
- package/src/offline/__tests__/offline-fixtures.ts +89 -0
- package/src/offline/__tests__/router.spec.ts +56 -0
- package/src/offline/__tests__/state.spec.ts +62 -0
- package/src/offline/__tests__/write-through.spec.ts +137 -0
- package/src/offline/config.ts +229 -0
- package/src/offline/configure.ts +14 -0
- package/src/offline/device-switch.ts +75 -0
- package/src/offline/generic-answers.ts +706 -0
- package/src/offline/index.ts +67 -0
- package/src/offline/local-transport.ts +189 -0
- package/src/offline/logout.ts +41 -0
- package/src/offline/router.ts +41 -0
- package/src/offline/server/__tests__/build-offline-manifest.spec.ts +76 -0
- package/src/offline/server/build-offline-manifest.ts +49 -0
- package/src/offline/state-hooks.ts +10 -0
- package/src/offline/state.ts +143 -0
- package/src/offline/store/DeviceStore.ts +112 -0
- package/src/offline/store/__tests__/device-store-lifecycle.spec.ts +343 -0
- package/src/offline/store/__tests__/shared-database.spec.ts +151 -0
- package/src/offline/store/__tests__/sql-device-store.spec.ts +399 -0
- package/src/offline/store/lifecycle.ts +315 -0
- package/src/offline/store/schema.ts +101 -0
- package/src/offline/store/shared-database.ts +321 -0
- package/src/offline/store/sql-device-store.ts +469 -0
- package/src/offline/store/worker-client.ts +71 -0
- package/src/offline/store/worker.ts +87 -0
- package/src/offline/sync/__tests__/conflicts.spec.ts +185 -0
- package/src/offline/sync/__tests__/queue-drain.spec.ts +364 -0
- package/src/offline/sync/__tests__/runtime.spec.ts +337 -0
- package/src/offline/sync/__tests__/sync-fixtures.ts +103 -0
- package/src/offline/sync/__tests__/sync-root.spec.ts +727 -0
- package/src/offline/sync/artwork-cache.ts +67 -0
- package/src/offline/sync/conflicts.ts +211 -0
- package/src/offline/sync/counts.ts +39 -0
- package/src/offline/sync/drain-queue.ts +246 -0
- package/src/offline/sync/known-roots.ts +37 -0
- package/src/offline/sync/models/OfflineChangeSet.ts +34 -0
- package/src/offline/sync/models/OfflineChangeSetInterface.ts +54 -0
- package/src/offline/sync/models/RootSync.ts +68 -0
- package/src/offline/sync/models/RootSyncInterface.ts +20 -0
- package/src/offline/sync/models/__tests__/OfflineChangeSet.spec.ts +47 -0
- package/src/offline/sync/models/__tests__/RootSync.spec.ts +54 -0
- package/src/offline/sync/models/index.ts +5 -0
- package/src/offline/sync/models/offline.modules.ts +14 -0
- package/src/offline/sync/runtime.ts +226 -0
- package/src/offline/sync/session-refresh.ts +32 -0
- package/src/offline/sync/sync-root.ts +336 -0
- package/src/offline/sync/waiting.ts +40 -0
- package/src/offline/types.ts +115 -0
- package/src/offline/write-through.ts +189 -0
- package/src/pwa/__tests__/sw.spec.ts +358 -1
- package/src/server/index.ts +5 -0
- package/src/unified/__tests__/JsonApiRequest.offline.spec.ts +243 -0
- package/dist/BlockNoteEditor-75ED2CN2.mjs.map +0 -1
- package/dist/BlockNoteEditor-7M5QW2YQ.js.map +0 -1
- package/dist/BlockNoteViewer-WGCDB7IJ.js.map +0 -1
- package/dist/JsonApiRequest-FZGPLNUF.js +0 -26
- package/dist/chunk-AA52ORW2.mjs.map +0 -1
- package/dist/chunk-EWCY6QJA.mjs.map +0 -1
- package/dist/chunk-FF4EIQ6H.js.map +0 -1
- package/dist/chunk-JTPRJNT2.js.map +0 -1
- package/dist/chunk-L6PYFQTO.js.map +0 -1
- package/dist/chunk-O3PDL2CV.js.map +0 -1
- package/dist/chunk-PI56A4DG.mjs.map +0 -1
- package/dist/chunk-RXQYEEKZ.mjs.map +0 -1
- package/dist/chunk-V3YDVJ63.js.map +0 -1
- package/dist/request-5RLK2AM3.mjs +0 -9
- package/dist/request-EUGTI3BE.js +0 -9
- package/dist/request-EUGTI3BE.js.map +0 -1
- /package/dist/{JsonApiRequest-A66XK5WT.mjs.map → JsonApiRequest-F6TMJ42G.mjs.map} +0 -0
- /package/dist/{chunk-3YW35YHQ.mjs.map → chunk-BH6JWIKI.mjs.map} +0 -0
- /package/dist/{chunk-V5MI7RNB.mjs.map → chunk-DGWOVYEC.mjs.map} +0 -0
- /package/dist/{request-5RLK2AM3.mjs.map → lifecycle-LKGPRBS7.mjs.map} +0 -0
package/pwa/sw.js
CHANGED
|
@@ -6,12 +6,147 @@
|
|
|
6
6
|
//
|
|
7
7
|
// Per-app values (appId, offlineRevision, notificationIcon) come from the app's
|
|
8
8
|
// own public/sw-config.js, which sets self.NJA_SW_CONFIG.
|
|
9
|
+
//
|
|
10
|
+
// Offline mode (opt-in; without these keys the worker behaves exactly as before):
|
|
11
|
+
// - offlineShell: { default: "/offline-shell", locales: { it: "/it/offline-shell" } }
|
|
12
|
+
// At install the worker reads /offline-manifest.json ({ buildId, assets }) and
|
|
13
|
+
// caches every asset plus each shell URL in `${appId}-shell-${buildId}`, then
|
|
14
|
+
// a completion marker last; one failed file fails the install and deletes the
|
|
15
|
+
// half-filled cache, so the previous worker stays in charge and no worker ever
|
|
16
|
+
// answers from a cache without the marker. A
|
|
17
|
+
// failed navigation answers the shell of its locale (the first path segment
|
|
18
|
+
// matched against `locales`, otherwise `default`), then offline.html. GETs
|
|
19
|
+
// under /_next/static/ are served cache-first from the shell cache; after an
|
|
20
|
+
// online navigation the worker re-reads the manifest (at most every few
|
|
21
|
+
// minutes) and precaches a new build's shell in the background, so a deploy
|
|
22
|
+
// refreshes the shell without a new worker; any other
|
|
23
|
+
// same-origin GET goes to the network first and, when it fails, is answered
|
|
24
|
+
// from the shell cache (the device worker and SQLite files). Activate deletes
|
|
25
|
+
// shell caches of other builds.
|
|
26
|
+
// - offlineImageHosts: ["api.example.com"] (a host or an origin)
|
|
27
|
+
// A GET to one of these hosts goes to the network; when the network fails it
|
|
28
|
+
// is answered from `${appId}-images`, where the app stores each artwork image
|
|
29
|
+
// under its exact URL. That cache is kept across activations.
|
|
9
30
|
|
|
10
31
|
importScripts("/sw-config.js");
|
|
11
32
|
|
|
12
33
|
const OFFLINE_URL = "/offline.html";
|
|
13
34
|
const CACHE_NAME = `${self.NJA_SW_CONFIG.appId}-offline-${self.NJA_SW_CONFIG.offlineRevision}`;
|
|
14
35
|
|
|
36
|
+
const OFFLINE_SHELL = self.NJA_SW_CONFIG.offlineShell || null;
|
|
37
|
+
const OFFLINE_IMAGE_HOSTS = self.NJA_SW_CONFIG.offlineImageHosts || [];
|
|
38
|
+
const MANIFEST_URL = "/offline-manifest.json";
|
|
39
|
+
const SHELL_CACHE_PREFIX = `${self.NJA_SW_CONFIG.appId}-shell-`;
|
|
40
|
+
const IMAGES_CACHE_NAME = `${self.NJA_SW_CONFIG.appId}-images`;
|
|
41
|
+
// Written last into a shell cache: a shell cache without it is incomplete and never used.
|
|
42
|
+
const SHELL_COMPLETE_URL = "/__nja-offline-shell-complete__";
|
|
43
|
+
|
|
44
|
+
// The shell cache this worker installed. A worker can be stopped and restarted
|
|
45
|
+
// between events, losing this value; it then falls back to the newest complete
|
|
46
|
+
// shell cache (caches.keys() lists caches in creation order).
|
|
47
|
+
let shellCacheName = null;
|
|
48
|
+
|
|
49
|
+
async function currentShellCacheName() {
|
|
50
|
+
if (shellCacheName) return shellCacheName;
|
|
51
|
+
const keys = await caches.keys();
|
|
52
|
+
const shells = keys.filter(function (key) {
|
|
53
|
+
return key.startsWith(SHELL_CACHE_PREFIX);
|
|
54
|
+
});
|
|
55
|
+
for (let index = shells.length - 1; index >= 0; index--) {
|
|
56
|
+
const cache = await caches.open(shells[index]);
|
|
57
|
+
if (await cache.match(SHELL_COMPLETE_URL)) return shells[index];
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function shellUrls() {
|
|
63
|
+
const urls = [OFFLINE_SHELL.default];
|
|
64
|
+
const locales = OFFLINE_SHELL.locales || {};
|
|
65
|
+
for (const locale of Object.keys(locales)) urls.push(locales[locale]);
|
|
66
|
+
return urls;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function shellUrlFor(url) {
|
|
70
|
+
const segment = new URL(url).pathname.split("/")[1] || "";
|
|
71
|
+
const locales = OFFLINE_SHELL.locales || {};
|
|
72
|
+
return Object.prototype.hasOwnProperty.call(locales, segment) ? locales[segment] : OFFLINE_SHELL.default;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function installShell() {
|
|
76
|
+
const response = await fetch(new Request(MANIFEST_URL, { cache: "no-store" }));
|
|
77
|
+
if (!response.ok) throw new Error(`${MANIFEST_URL} answered ${response.status}`);
|
|
78
|
+
const manifest = await response.json();
|
|
79
|
+
const name = `${SHELL_CACHE_PREFIX}${manifest.buildId}`;
|
|
80
|
+
try {
|
|
81
|
+
const cache = await caches.open(name);
|
|
82
|
+
// cache.addAll rejects when any file fails, so the install fails with it.
|
|
83
|
+
await cache.addAll(
|
|
84
|
+
manifest.assets.concat(shellUrls()).map(function (url) {
|
|
85
|
+
return new Request(url, { cache: "reload" });
|
|
86
|
+
}),
|
|
87
|
+
);
|
|
88
|
+
await cache.put(SHELL_COMPLETE_URL, new Response("", { status: 200 }));
|
|
89
|
+
} catch (error) {
|
|
90
|
+
// Never leave a half-filled shell cache behind for any worker to pick.
|
|
91
|
+
await caches.delete(name);
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
shellCacheName = name;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// A deploy changes the app's build, not this file: sw.js is byte-identical across builds, so no
|
|
98
|
+
// new worker is installed. After an online navigation the worker reads the manifest and, when
|
|
99
|
+
// the server runs a build it has no complete shell cache for, precaches that build's shell in
|
|
100
|
+
// the background, then drops the other shell caches. At most one check per few minutes.
|
|
101
|
+
const SHELL_REFRESH_INTERVAL_MS = 5 * 60 * 1000;
|
|
102
|
+
let lastShellRefresh = 0;
|
|
103
|
+
let shellRefresh = null;
|
|
104
|
+
|
|
105
|
+
async function refreshShellIfStale() {
|
|
106
|
+
const response = await fetch(new Request(MANIFEST_URL, { cache: "no-store" }));
|
|
107
|
+
if (!response.ok) return;
|
|
108
|
+
const manifest = await response.json();
|
|
109
|
+
const name = `${SHELL_CACHE_PREFIX}${manifest.buildId}`;
|
|
110
|
+
const keys = await caches.keys();
|
|
111
|
+
if (keys.indexOf(name) !== -1) {
|
|
112
|
+
const existing = await caches.open(name);
|
|
113
|
+
if (await existing.match(SHELL_COMPLETE_URL)) {
|
|
114
|
+
shellCacheName = name;
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
await installShell();
|
|
119
|
+
const others = (await caches.keys()).filter(function (key) {
|
|
120
|
+
return key.startsWith(SHELL_CACHE_PREFIX) && key !== shellCacheName;
|
|
121
|
+
});
|
|
122
|
+
await Promise.all(
|
|
123
|
+
others.map(function (key) {
|
|
124
|
+
return caches.delete(key);
|
|
125
|
+
}),
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function maybeRefreshShell() {
|
|
130
|
+
const now = Date.now();
|
|
131
|
+
if (shellRefresh || now - lastShellRefresh < SHELL_REFRESH_INTERVAL_MS) return shellRefresh || Promise.resolve();
|
|
132
|
+
lastShellRefresh = now;
|
|
133
|
+
shellRefresh = refreshShellIfStale()
|
|
134
|
+
.catch(function (error) {
|
|
135
|
+
// The current shell stays in charge; the next navigation tries again later.
|
|
136
|
+
console.warn("[sw] refreshing the offline shell failed", error);
|
|
137
|
+
})
|
|
138
|
+
.finally(function () {
|
|
139
|
+
shellRefresh = null;
|
|
140
|
+
});
|
|
141
|
+
return shellRefresh;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function isImageHost(url) {
|
|
145
|
+
if (!OFFLINE_IMAGE_HOSTS.length) return false;
|
|
146
|
+
const parsed = new URL(url);
|
|
147
|
+
return OFFLINE_IMAGE_HOSTS.indexOf(parsed.host) !== -1 || OFFLINE_IMAGE_HOSTS.indexOf(parsed.origin) !== -1;
|
|
148
|
+
}
|
|
149
|
+
|
|
15
150
|
// Served when a navigation fails and the cached offline page is missing too
|
|
16
151
|
// (evicted or cleared). Never answer a navigation with a bare network error.
|
|
17
152
|
const OFFLINE_FALLBACK_HTML = `<!doctype html>
|
|
@@ -58,11 +193,16 @@ const OFFLINE_FALLBACK_HTML = `<!doctype html>
|
|
|
58
193
|
|
|
59
194
|
self.addEventListener("install", function (event) {
|
|
60
195
|
event.waitUntil(
|
|
61
|
-
caches
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
196
|
+
caches
|
|
197
|
+
.open(CACHE_NAME)
|
|
198
|
+
.then(function (cache) {
|
|
199
|
+
// cache.add rejects on a network error or a non-2xx response, so the
|
|
200
|
+
// install fails and the previous worker stays in charge.
|
|
201
|
+
return cache.add(new Request(OFFLINE_URL, { cache: "reload" }));
|
|
202
|
+
})
|
|
203
|
+
.then(function () {
|
|
204
|
+
if (OFFLINE_SHELL) return installShell();
|
|
205
|
+
}),
|
|
66
206
|
);
|
|
67
207
|
});
|
|
68
208
|
|
|
@@ -70,10 +210,16 @@ self.addEventListener("activate", function (event) {
|
|
|
70
210
|
event.waitUntil(
|
|
71
211
|
(async function () {
|
|
72
212
|
const keys = await caches.keys();
|
|
213
|
+
const keep = [CACHE_NAME];
|
|
214
|
+
if (OFFLINE_SHELL) {
|
|
215
|
+
const shell = await currentShellCacheName();
|
|
216
|
+
if (shell) keep.push(shell);
|
|
217
|
+
}
|
|
218
|
+
if (OFFLINE_SHELL || OFFLINE_IMAGE_HOSTS.length) keep.push(IMAGES_CACHE_NAME);
|
|
73
219
|
await Promise.all(
|
|
74
220
|
keys
|
|
75
221
|
.filter(function (key) {
|
|
76
|
-
return key
|
|
222
|
+
return keep.indexOf(key) === -1;
|
|
77
223
|
})
|
|
78
224
|
.map(function (key) {
|
|
79
225
|
return caches.delete(key);
|
|
@@ -88,28 +234,133 @@ self.addEventListener("activate", function (event) {
|
|
|
88
234
|
});
|
|
89
235
|
|
|
90
236
|
self.addEventListener("fetch", function (event) {
|
|
91
|
-
// Only navigations are intercepted
|
|
92
|
-
// network without
|
|
93
|
-
|
|
237
|
+
// Only navigations are intercepted, plus (offline mode only) build assets and
|
|
238
|
+
// artwork images. Everything else goes straight to the network without
|
|
239
|
+
// respondWith.
|
|
240
|
+
if (event.request.mode !== "navigate") {
|
|
241
|
+
if (!OFFLINE_SHELL && !OFFLINE_IMAGE_HOSTS.length) return;
|
|
242
|
+
const method = event.request.method || "GET";
|
|
243
|
+
if (method !== "GET") return;
|
|
244
|
+
// A no-store request (the page's session refresh of the shell) is never answered from a
|
|
245
|
+
// cache: when the network fails it must fail, so the page knows the app is unreachable.
|
|
246
|
+
if (event.request.cache === "no-store") return;
|
|
247
|
+
const url = new URL(event.request.url);
|
|
94
248
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
249
|
+
if (OFFLINE_SHELL && url.origin === self.location.origin && url.pathname.startsWith("/_next/static/")) {
|
|
250
|
+
event.respondWith(
|
|
251
|
+
(async function () {
|
|
252
|
+
const name = await currentShellCacheName();
|
|
253
|
+
if (name) {
|
|
254
|
+
const cache = await caches.open(name);
|
|
255
|
+
const cached = await cache.match(event.request.url);
|
|
256
|
+
if (cached) return cached;
|
|
257
|
+
}
|
|
258
|
+
try {
|
|
259
|
+
return await fetch(event.request);
|
|
260
|
+
} catch {
|
|
261
|
+
// Offline and not in the shell: a plain network error for the page, no uncaught rejection here.
|
|
262
|
+
return Response.error();
|
|
263
|
+
}
|
|
264
|
+
})(),
|
|
265
|
+
);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// An image of an offline image host through Next's image optimizer (`/_next/image?url=...`):
|
|
270
|
+
// network first, then the downloaded original (the images cache keeps it under its own URL).
|
|
271
|
+
const optimizedSource = url.origin === self.location.origin && url.pathname === "/_next/image" ? url.searchParams.get("url") : null;
|
|
272
|
+
if (optimizedSource && /^https?:/.test(optimizedSource) && isImageHost(optimizedSource)) {
|
|
273
|
+
event.respondWith(
|
|
274
|
+
(async function () {
|
|
275
|
+
try {
|
|
276
|
+
return await fetch(event.request);
|
|
277
|
+
} catch {
|
|
278
|
+
const cache = await caches.open(IMAGES_CACHE_NAME);
|
|
279
|
+
const cached = await cache.match(optimizedSource);
|
|
280
|
+
if (cached) return cached;
|
|
281
|
+
return Response.error();
|
|
282
|
+
}
|
|
283
|
+
})(),
|
|
284
|
+
);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Any other same-origin file the offline manifest precached (the device worker and the
|
|
289
|
+
// SQLite files it loads): network first, the shell cache when the network fails.
|
|
290
|
+
if (OFFLINE_SHELL && url.origin === self.location.origin) {
|
|
291
|
+
event.respondWith(
|
|
292
|
+
(async function () {
|
|
293
|
+
try {
|
|
294
|
+
return await fetch(event.request);
|
|
295
|
+
} catch {
|
|
296
|
+
const name = await currentShellCacheName();
|
|
297
|
+
if (name) {
|
|
298
|
+
const cache = await caches.open(name);
|
|
299
|
+
const cached = await cache.match(event.request.url);
|
|
300
|
+
if (cached) return cached;
|
|
301
|
+
}
|
|
302
|
+
return Response.error();
|
|
303
|
+
}
|
|
304
|
+
})(),
|
|
305
|
+
);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (isImageHost(event.request.url)) {
|
|
310
|
+
event.respondWith(
|
|
311
|
+
(async function () {
|
|
312
|
+
try {
|
|
313
|
+
return await fetch(event.request);
|
|
314
|
+
} catch {
|
|
315
|
+
const cache = await caches.open(IMAGES_CACHE_NAME);
|
|
316
|
+
const cached = await cache.match(event.request.url);
|
|
317
|
+
if (cached) return cached;
|
|
318
|
+
return Response.error();
|
|
319
|
+
}
|
|
320
|
+
})(),
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
let reachedNetwork = false;
|
|
327
|
+
const answer = (async function () {
|
|
328
|
+
try {
|
|
329
|
+
const preload = await event.preloadResponse;
|
|
330
|
+
if (preload) {
|
|
331
|
+
reachedNetwork = true;
|
|
332
|
+
return preload;
|
|
110
333
|
}
|
|
111
|
-
|
|
112
|
-
|
|
334
|
+
// A 4xx/5xx or an opaque redirect is passed through as is.
|
|
335
|
+
const response = await fetch(event.request);
|
|
336
|
+
reachedNetwork = true;
|
|
337
|
+
return response;
|
|
338
|
+
} catch (error) {
|
|
339
|
+
if (OFFLINE_SHELL) {
|
|
340
|
+
const name = await currentShellCacheName();
|
|
341
|
+
if (name) {
|
|
342
|
+
const shellCache = await caches.open(name);
|
|
343
|
+
const shell = await shellCache.match(shellUrlFor(event.request.url));
|
|
344
|
+
if (shell) return shell;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
const cache = await caches.open(CACHE_NAME);
|
|
348
|
+
const cached = await cache.match(OFFLINE_URL);
|
|
349
|
+
if (cached) return cached;
|
|
350
|
+
return new Response(OFFLINE_FALLBACK_HTML, {
|
|
351
|
+
status: 200,
|
|
352
|
+
headers: { "content-type": "text/html; charset=utf-8" },
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
})();
|
|
356
|
+
event.respondWith(answer);
|
|
357
|
+
if (OFFLINE_SHELL) {
|
|
358
|
+
event.waitUntil(
|
|
359
|
+
answer.then(function () {
|
|
360
|
+
if (reachedNetwork) return maybeRefreshShell();
|
|
361
|
+
}),
|
|
362
|
+
);
|
|
363
|
+
}
|
|
113
364
|
});
|
|
114
365
|
|
|
115
366
|
self.addEventListener("message", function (event) {
|
package/src/client/request.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { ApiData } from "../core/interfaces/ApiData";
|
|
4
|
+
import { isOfflineConfigured } from "../offline/config";
|
|
5
|
+
import { getOfflineState, reachesNetworkWhileOffline, setOfflineState } from "../offline/state";
|
|
6
|
+
import { OFFLINE_REPLAY_HEADER } from "../offline/types";
|
|
4
7
|
|
|
5
8
|
export interface DirectFetchParams {
|
|
6
9
|
method: string;
|
|
@@ -18,6 +21,39 @@ export interface DirectFetchParams {
|
|
|
18
21
|
* Use this for client-side API calls.
|
|
19
22
|
*/
|
|
20
23
|
export async function directFetch(params: DirectFetchParams): Promise<ApiData> {
|
|
24
|
+
// Offline mode is opt-in (`configureOffline`). Without it nothing below loads or runs. A request
|
|
25
|
+
// the server runs as an offline replay always goes to the network, and so do the reads and the
|
|
26
|
+
// change-set request of a reconnect attempt (the session stays offline until its sync succeeds).
|
|
27
|
+
const offline = isOfflineConfigured() && !hasReplayHeader(params.additionalHeaders);
|
|
28
|
+
if (offline && !getOfflineState().online && !reachesNetworkWhileOffline(params.method, params.url)) {
|
|
29
|
+
return answerLocally(params);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const response = await fetchFromApi(params);
|
|
33
|
+
|
|
34
|
+
if (offline && response.networkError) {
|
|
35
|
+
setOfflineState({ online: false });
|
|
36
|
+
return answerLocally(params);
|
|
37
|
+
}
|
|
38
|
+
if (offline && response.ok && params.method.toUpperCase() !== "GET") {
|
|
39
|
+
const { writeThrough } = await import("../offline/write-through");
|
|
40
|
+
await writeThrough({ method: params.method, url: params.url }, response);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return response;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function hasReplayHeader(headers?: Record<string, string>): boolean {
|
|
47
|
+
if (!headers) return false;
|
|
48
|
+
return Object.keys(headers).some((key) => key.toLowerCase() === OFFLINE_REPLAY_HEADER);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function answerLocally(params: DirectFetchParams): Promise<ApiData> {
|
|
52
|
+
const { localTransport } = await import("../offline/local-transport");
|
|
53
|
+
return localTransport({ method: params.method, url: params.url, body: params.body, files: params.files });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function fetchFromApi(params: DirectFetchParams): Promise<ApiData> {
|
|
21
57
|
const response: ApiData = {
|
|
22
58
|
data: undefined,
|
|
23
59
|
ok: false,
|
|
@@ -95,6 +131,7 @@ export async function directFetch(params: DirectFetchParams): Promise<ApiData> {
|
|
|
95
131
|
response.status = 503;
|
|
96
132
|
response.statusText = "Service Unavailable";
|
|
97
133
|
response.data = undefined;
|
|
134
|
+
response.networkError = true;
|
|
98
135
|
}
|
|
99
136
|
|
|
100
137
|
return response;
|
|
@@ -821,11 +821,21 @@ export default function BlockNoteEditor({
|
|
|
821
821
|
// initialContent is just an echo of our own emission (form-controlled flow).
|
|
822
822
|
const lastEmittedHashRef = useRef<string | null>(null);
|
|
823
823
|
|
|
824
|
+
// Whether the parent's copy of the text is empty: the content it handed in, then each change
|
|
825
|
+
// reported to it. A change from empty to still empty is not reported: BlockNote emits one when an
|
|
826
|
+
// empty editor mounts (its trailing empty paragraph), and reported it became a save of nothing on
|
|
827
|
+
// every view of a page with an empty field.
|
|
828
|
+
const processedContentRef = useRef<any[]>([]);
|
|
829
|
+
const parentKnowsEmptyRef = useRef<boolean | null>(null);
|
|
830
|
+
|
|
824
831
|
const handleChange = useCallback(async () => {
|
|
825
832
|
if (!onChange) return;
|
|
826
833
|
const newBlocks = editor.document;
|
|
827
834
|
lastEmittedHashRef.current = JSON.stringify(newBlocks);
|
|
828
835
|
const isEmpty = isDocumentEmpty(newBlocks);
|
|
836
|
+
const wasEmpty = parentKnowsEmptyRef.current ?? isDocumentEmpty(processedContentRef.current);
|
|
837
|
+
if (isEmpty && wasEmpty) return;
|
|
838
|
+
parentKnowsEmptyRef.current = isEmpty;
|
|
829
839
|
|
|
830
840
|
function hasUnresolvedDiffsRecursive(block: any): boolean {
|
|
831
841
|
if (!block || typeof block !== "object") return false;
|
|
@@ -880,6 +890,8 @@ export default function BlockNoteEditor({
|
|
|
880
890
|
onChange(newBlocks, isEmpty, hasUnresolvedDiff);
|
|
881
891
|
}, [editor, onChange, id, acceptedChanges, rejectedChanges]);
|
|
882
892
|
|
|
893
|
+
processedContentRef.current = processedContent as any[];
|
|
894
|
+
|
|
883
895
|
// Utility: deep equality for arrays of blocks
|
|
884
896
|
const areBlocksEqual = (a: any[], b: any[]): boolean => {
|
|
885
897
|
return JSON.stringify(a) === JSON.stringify(b);
|
|
@@ -912,6 +924,8 @@ export default function BlockNoteEditor({
|
|
|
912
924
|
if (collaboration) return;
|
|
913
925
|
const hash = JSON.stringify(processedContent);
|
|
914
926
|
if (previousContentHashRef.current === hash) return; // no changes
|
|
927
|
+
// New content from the parent: that is now the text it knows.
|
|
928
|
+
parentKnowsEmptyRef.current = isDocumentEmpty(processedContent as any[]);
|
|
915
929
|
// Skip replaceBlocks when the new initialContent is just an echo of what
|
|
916
930
|
// the editor itself emitted. Without this, a form-controlled parent
|
|
917
931
|
// (FormBlockNote) round-trips field.value back as initialContent on every
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { act, render } from "@testing-library/react";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* BlockNote emits one change when an empty editor mounts (it inserts its trailing empty
|
|
7
|
+
* paragraph). Reported to the parent, that no-op became a save on every page view (an autosave
|
|
8
|
+
* of an empty field), and offline a queued change. The editor reports a change unless the text
|
|
9
|
+
* went from empty to still empty. Mocks as in BlockNoteEditor.ai.spec.tsx, plus the view's
|
|
10
|
+
* onChange and the editor's replaceBlocks.
|
|
11
|
+
*/
|
|
12
|
+
const h = vi.hoisted(() => {
|
|
13
|
+
const AIExtension = vi.fn((options: unknown) => ({ aiFactory: true, options }));
|
|
14
|
+
return {
|
|
15
|
+
AIExtension,
|
|
16
|
+
editorsCreated: 0,
|
|
17
|
+
company: undefined as { id: string } | undefined,
|
|
18
|
+
editor: undefined as any,
|
|
19
|
+
ai: undefined as any,
|
|
20
|
+
transportConfig: undefined as any,
|
|
21
|
+
viewOnChange: undefined as undefined | (() => void),
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
vi.mock("next-intl", () => ({ useTranslations: () => (key: string) => key }));
|
|
26
|
+
vi.mock("next-themes", () => ({ useTheme: () => ({ resolvedTheme: "light" }) }));
|
|
27
|
+
vi.mock("../../../i18n/config", () => ({ useI18nLocale: () => "en" }));
|
|
28
|
+
vi.mock("../../../client/config", () => ({ getPublicApiUrl: () => "http://api.test/" }));
|
|
29
|
+
vi.mock("../../../client/token", () => ({ getClientToken: async () => "token" }));
|
|
30
|
+
vi.mock("../../../contexts", () => ({
|
|
31
|
+
useCurrentUserContextOptional: () => (h.company ? { company: h.company } : undefined),
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
vi.mock("@blocknote/react", async () => {
|
|
35
|
+
const React = await import("react");
|
|
36
|
+
const EditorContext = React.createContext<any>(null);
|
|
37
|
+
const useBlockNoteEditor = () => React.useContext(EditorContext);
|
|
38
|
+
const requireAI = (editor: any) => {
|
|
39
|
+
if (!editor?.getExtension(h.AIExtension)) throw new Error("Extension not found");
|
|
40
|
+
};
|
|
41
|
+
return {
|
|
42
|
+
EditorContext,
|
|
43
|
+
createReactInlineContentSpec: (config: unknown) => ({ config }),
|
|
44
|
+
getDefaultReactSlashMenuItems: () => [],
|
|
45
|
+
SuggestionMenuController: () => null,
|
|
46
|
+
useBlockNoteEditor,
|
|
47
|
+
useExtension: () => {
|
|
48
|
+
const editor = useBlockNoteEditor();
|
|
49
|
+
requireAI(editor);
|
|
50
|
+
return editor.getExtension(h.AIExtension);
|
|
51
|
+
},
|
|
52
|
+
useExtensionState: () => "closed",
|
|
53
|
+
useCreateBlockNote: (options: any, deps: React.DependencyList = []) =>
|
|
54
|
+
React.useMemo(() => {
|
|
55
|
+
h.editorsCreated += 1;
|
|
56
|
+
const extensions: any[] = options?.extensions ?? [];
|
|
57
|
+
// Mirrors xl-ai 0.55: a store holding aiMenuState, and the calls that
|
|
58
|
+
// move it back to "closed".
|
|
59
|
+
const store = { state: { aiMenuState: "closed" as any } };
|
|
60
|
+
const ai = {
|
|
61
|
+
store,
|
|
62
|
+
invokeAI: vi.fn(),
|
|
63
|
+
openAIMenuAtBlock: vi.fn((blockId: string) => {
|
|
64
|
+
store.state.aiMenuState = { blockId, status: "user-input" };
|
|
65
|
+
}),
|
|
66
|
+
abort: vi.fn(async () => {
|
|
67
|
+
const menu = store.state.aiMenuState;
|
|
68
|
+
if (menu !== "closed") store.state.aiMenuState = { ...menu, status: "user-reviewing" };
|
|
69
|
+
}),
|
|
70
|
+
closeAIMenu: vi.fn(() => {
|
|
71
|
+
store.state.aiMenuState = "closed";
|
|
72
|
+
}),
|
|
73
|
+
rejectChanges: vi.fn(() => {
|
|
74
|
+
store.state.aiMenuState = "closed";
|
|
75
|
+
}),
|
|
76
|
+
};
|
|
77
|
+
const hasAI = extensions.some((e) => e?.aiFactory);
|
|
78
|
+
const editor = {
|
|
79
|
+
document: [] as any[],
|
|
80
|
+
replaceBlocks: (_old: unknown, blocks: any[]) => {
|
|
81
|
+
editor.document = blocks;
|
|
82
|
+
},
|
|
83
|
+
focus: () => undefined,
|
|
84
|
+
getSelection: () => undefined,
|
|
85
|
+
getExtension: (factory: unknown) => (factory === h.AIExtension && hasAI ? ai : undefined),
|
|
86
|
+
};
|
|
87
|
+
h.editor = editor;
|
|
88
|
+
h.ai = hasAI ? ai : undefined;
|
|
89
|
+
return editor;
|
|
90
|
+
}, deps),
|
|
91
|
+
BasicTextStyleButton: () => null,
|
|
92
|
+
BlockTypeSelect: () => null,
|
|
93
|
+
CreateLinkButton: () => null,
|
|
94
|
+
FileCaptionButton: () => null,
|
|
95
|
+
FileReplaceButton: () => null,
|
|
96
|
+
TextAlignButton: () => null,
|
|
97
|
+
FormattingToolbar: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
|
98
|
+
FormattingToolbarController: ({ formattingToolbar }: { formattingToolbar: () => React.ReactNode }) => (
|
|
99
|
+
<>{formattingToolbar()}</>
|
|
100
|
+
),
|
|
101
|
+
// Shared with the xl-ai stubs below.
|
|
102
|
+
__requireAI: requireAI,
|
|
103
|
+
};
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
vi.mock("@blocknote/shadcn", async () => {
|
|
107
|
+
const { EditorContext } = (await import("@blocknote/react")) as any;
|
|
108
|
+
return {
|
|
109
|
+
BlockNoteView: ({
|
|
110
|
+
editor,
|
|
111
|
+
children,
|
|
112
|
+
onChange,
|
|
113
|
+
}: {
|
|
114
|
+
editor: unknown;
|
|
115
|
+
children: React.ReactNode;
|
|
116
|
+
onChange?: () => void;
|
|
117
|
+
}) => {
|
|
118
|
+
h.viewOnChange = onChange;
|
|
119
|
+
return <EditorContext.Provider value={editor}>{children}</EditorContext.Provider>;
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
vi.mock("@blocknote/xl-ai", async () => {
|
|
125
|
+
const { useBlockNoteEditor, __requireAI } = (await import("@blocknote/react")) as any;
|
|
126
|
+
return {
|
|
127
|
+
AIExtension: h.AIExtension,
|
|
128
|
+
AIMenuController: () => {
|
|
129
|
+
__requireAI(useBlockNoteEditor());
|
|
130
|
+
return <div data-testid="ai-menu" />;
|
|
131
|
+
},
|
|
132
|
+
AIToolbarButton: () => {
|
|
133
|
+
__requireAI(useBlockNoteEditor());
|
|
134
|
+
return <div data-testid="ai-toolbar-button" />;
|
|
135
|
+
},
|
|
136
|
+
getAISlashMenuItems: () => [],
|
|
137
|
+
getDefaultAIMenuItems: () => [],
|
|
138
|
+
PromptSuggestionMenu: () => null,
|
|
139
|
+
useAIDictionary: () => ({}),
|
|
140
|
+
};
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
vi.mock("ai", () => ({
|
|
144
|
+
DefaultChatTransport: function (this: any, config: unknown) {
|
|
145
|
+
h.transportConfig = config;
|
|
146
|
+
},
|
|
147
|
+
}));
|
|
148
|
+
vi.mock("@blocknote/core", () => ({
|
|
149
|
+
BlockNoteSchema: { create: (config: unknown) => ({ config }) },
|
|
150
|
+
defaultInlineContentSpecs: {},
|
|
151
|
+
filterSuggestionItems: (items: unknown[]) => items,
|
|
152
|
+
}));
|
|
153
|
+
vi.mock("@blocknote/core/locales", () => ({ en: { core: "en" } }));
|
|
154
|
+
vi.mock("@blocknote/xl-ai/locales", () => ({ en: { ai: "en" } }));
|
|
155
|
+
vi.mock("@blocknote/shadcn/style.css", () => ({}));
|
|
156
|
+
vi.mock("@blocknote/xl-ai/style.css", () => ({}));
|
|
157
|
+
vi.mock("../BlockNoteEditorMentionInlineContent", () => ({ createMentionInlineContentSpec: () => ({}) }));
|
|
158
|
+
|
|
159
|
+
import BlockNoteEditor from "../BlockNoteEditor";
|
|
160
|
+
|
|
161
|
+
const emptyParagraph = () => [{ id: "p0", type: "paragraph", props: {}, content: [], children: [] }];
|
|
162
|
+
const textParagraph = (text: string) => [
|
|
163
|
+
{ id: "p1", type: "paragraph", props: {}, content: [{ type: "text", text, styles: {} }], children: [] },
|
|
164
|
+
];
|
|
165
|
+
|
|
166
|
+
const flush = () => new Promise((resolve) => setTimeout(resolve, 0));
|
|
167
|
+
|
|
168
|
+
async function emit(document: any[]): Promise<void> {
|
|
169
|
+
h.editor.document = document;
|
|
170
|
+
await act(async () => {
|
|
171
|
+
h.viewOnChange?.();
|
|
172
|
+
await flush();
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
describe("BlockNoteEditor change reports", () => {
|
|
177
|
+
const onChange = vi.fn();
|
|
178
|
+
|
|
179
|
+
beforeEach(() => {
|
|
180
|
+
onChange.mockReset();
|
|
181
|
+
h.viewOnChange = undefined;
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("does not report the change an empty editor emits when it mounts (empty to still empty)", async () => {
|
|
185
|
+
render(<BlockNoteEditor id="npc-1" type="npcs" onChange={onChange} />);
|
|
186
|
+
await emit(emptyParagraph());
|
|
187
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("reports typing into an empty editor, then clearing it again", async () => {
|
|
191
|
+
render(<BlockNoteEditor id="npc-1" type="npcs" onChange={onChange} />);
|
|
192
|
+
await emit(textParagraph("Hello"));
|
|
193
|
+
expect(onChange).toHaveBeenLastCalledWith(textParagraph("Hello"), false, false);
|
|
194
|
+
await emit(emptyParagraph());
|
|
195
|
+
expect(onChange).toHaveBeenLastCalledWith(emptyParagraph(), true, false);
|
|
196
|
+
expect(onChange).toHaveBeenCalledTimes(2);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("reports clearing an editor that started with text, even as its first change", async () => {
|
|
200
|
+
render(<BlockNoteEditor id="npc-1" type="npcs" initialContent={textParagraph("Old") as any} onChange={onChange} />);
|
|
201
|
+
await emit(emptyParagraph());
|
|
202
|
+
expect(onChange).toHaveBeenCalledTimes(1);
|
|
203
|
+
expect(onChange).toHaveBeenLastCalledWith(emptyParagraph(), true, false);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
import React, { createContext, useContext, useEffect, useState } from "react";
|
|
4
4
|
import { NotificationInterface } from "../features/notification/data";
|
|
5
5
|
import { useSocket } from "../hooks";
|
|
6
|
+
import { isOfflineConfigured } from "../offline/config";
|
|
7
|
+
import { useOfflineState } from "../offline/state-hooks";
|
|
6
8
|
import type { SessionSnapshot } from "../server/ServerSession";
|
|
7
9
|
|
|
8
10
|
interface SocketContextProps {
|
|
@@ -62,8 +64,12 @@ export const SocketProvider: React.FC<SocketProviderProps> = ({ token, session,
|
|
|
62
64
|
}, [session]);
|
|
63
65
|
|
|
64
66
|
const resolvedToken = token ?? sessionToken;
|
|
67
|
+
// Offline mode (opt-in): the socket stops retrying while the session is offline and connects
|
|
68
|
+
// again once it is back online. Without offline mode the state stays online: never paused.
|
|
69
|
+
const offlineState = useOfflineState();
|
|
70
|
+
const paused = isOfflineConfigured() && !offlineState.online;
|
|
65
71
|
// Called unconditionally: useSocket returns early while the token is falsy.
|
|
66
|
-
const socketHookData = useSocket({ token: resolvedToken as string });
|
|
72
|
+
const socketHookData = useSocket({ token: resolvedToken as string, paused });
|
|
67
73
|
|
|
68
74
|
const sendMessage = (event: string, data: any) => {
|
|
69
75
|
const currentSocket = socketHookData ? socketHookData.socket : defaultContextValue.socket;
|