bitboss-ui 3.0.0-beta.64 → 3.0.0-beta.65
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/ai/changelog.json +82 -4
- package/dist/ai/components.json +2 -2
- package/dist/ai/composables/useBbConfig.md +8 -1
- package/dist/ai/guides/inertia-helpers.md +1127 -275
- package/dist/ai/guides/migration/v2-to-v3.md +48 -0
- package/dist/ai/guides/passthrough.md +4 -2
- package/dist/ai/helpers/inertia/manifest.json +145 -62
- package/dist/ai/helpers/inertia/messages.ts +33 -0
- package/dist/ai/helpers/inertia/useConfirmOnRouteLeave.ts +116 -16
- package/dist/ai/helpers/inertia/useHasChanged.ts +34 -16
- package/dist/ai/helpers/inertia/useLazyProp.ts +102 -36
- package/dist/ai/helpers/inertia/useLazyPropGetter.ts +3 -1
- package/dist/ai/helpers/inertia/usePaginatedResponseContext.ts +129 -24
- package/dist/ai/helpers/inertia/usePromiseForm.ts +121 -14
- package/dist/ai/helpers/inertia/useQuery.ts +133 -11
- package/dist/ai/helpers/inertia/useQueryObject.ts +74 -67
- package/dist/ai/helpers/inertia/useResourceContext.ts +60 -14
- package/dist/ai/helpers/inertia/useRouteQuery.ts +29 -24
- package/dist/ai/helpers/inertia/useScreenSizes.ts +9 -7
- package/dist/ai/helpers/inertia/useTableContext.ts +5 -2
- package/dist/ai/helpers/inertia/utils.ts +14 -0
- package/dist/ai/manifest/index.json +2 -2
- package/dist/ai/recipes/inertia/approvals-inbox.md +1 -1
- package/dist/ai/recipes/inertia/inline-edit-workspace.md +1 -1
- package/dist/ai/recipes/inertia/ownership-atlas.md +1 -1
- package/dist/ai/recipes/inertia/records-workspace.md +1 -1
- package/dist/ai/recipes/inertia/upload-center.md +1 -1
- package/dist/ai/source/BbSelectPopover.md +25 -1
- package/dist/components/BbSelectPopover/BbSelectPopover.vue_vue_type_script_setup_true_lang.js +306 -294
- package/dist/composables/useBbConfig.d.ts +1 -0
- package/dist/composables/useBbConfig.js +11 -7
- package/dist/llms-full.txt +1192 -283
- package/dist/styles.css +1 -1
- package/dist/utils/versionCheck.js +1 -1
- package/package.json +3 -2
- package/scripts/lib/helpers-add.mjs +26 -2
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// inertia-helpers/useConfirmOnRouteLeave.ts
|
|
2
|
+
import { useHelperMessages } from './messages';
|
|
2
3
|
import { router } from '@inertiajs/vue3';
|
|
3
4
|
import { useConfirm, type BbConfirmMessage } from 'bitboss-ui';
|
|
4
5
|
import { onScopeDispose, toValue, type MaybeRefOrGetter } from 'vue';
|
|
@@ -12,7 +13,11 @@ type Visit = {
|
|
|
12
13
|
};
|
|
13
14
|
|
|
14
15
|
type Options = {
|
|
15
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* Submit targets that bypass the dialog (the form's own save). Only a
|
|
18
|
+
* submit (POST / PUT / PATCH / DELETE) passes: a link to the same path
|
|
19
|
+
* (Cancel → the list) still asks.
|
|
20
|
+
*/
|
|
16
21
|
whitelist?: MaybeRefOrGetter<string[]>;
|
|
17
22
|
/** Disable the guard (bind to `!hasChanged` / `!form.isDirty`). */
|
|
18
23
|
disabled?: MaybeRefOrGetter<boolean>;
|
|
@@ -24,12 +29,14 @@ type Options = {
|
|
|
24
29
|
text?: string;
|
|
25
30
|
/**
|
|
26
31
|
* The dialog's buttons and look, BbConfirm's own options: a label string
|
|
27
|
-
* or a full button config.
|
|
28
|
-
* `variant: 'destructive'`.
|
|
32
|
+
* or a full button config. Default words: `leave` in `messages.ts`, in the
|
|
33
|
+
* library's `locale`; `variant: 'destructive'`.
|
|
29
34
|
*/
|
|
30
35
|
yes?: BbConfirmMessage['yes'];
|
|
31
36
|
no?: BbConfirmMessage['no'];
|
|
32
37
|
variant?: BbConfirmMessage['variant'];
|
|
38
|
+
/** The `BbDialog` stack the dialog joins (BbConfirm's `stack`). */
|
|
39
|
+
stack?: BbConfirmMessage['stack'];
|
|
33
40
|
};
|
|
34
41
|
|
|
35
42
|
const normalizeUrl = (value: string | URL) => {
|
|
@@ -56,24 +63,26 @@ export const useConfirmOnRouteLeave = ({
|
|
|
56
63
|
disabled = false,
|
|
57
64
|
onBefore = () => true,
|
|
58
65
|
onLeave = () => true,
|
|
59
|
-
title
|
|
60
|
-
text
|
|
61
|
-
yes
|
|
62
|
-
no
|
|
66
|
+
title,
|
|
67
|
+
text,
|
|
68
|
+
yes,
|
|
69
|
+
no,
|
|
63
70
|
variant = 'destructive',
|
|
71
|
+
stack,
|
|
64
72
|
}: Options = {}) => {
|
|
65
73
|
// Captured in setup so the built-in dialog is owned by the guarded page:
|
|
66
74
|
// if the page unmounts anyway, the dialog dismisses itself quietly.
|
|
67
75
|
const { confirm } = useConfirm();
|
|
76
|
+
const messages = useHelperMessages();
|
|
68
77
|
let bypassNextVisit = false;
|
|
69
78
|
|
|
70
79
|
const unbind = router.on('before', (event) => {
|
|
71
80
|
const visit = event.detail.visit as Visit;
|
|
72
81
|
|
|
73
82
|
// A hover / mount prefetch (`<Link prefetch>`, `<BbButton href prefetch>`)
|
|
74
|
-
// fires `before` too, but leaves nothing: no dialog
|
|
75
|
-
//
|
|
76
|
-
if (visit.prefetch) return;
|
|
83
|
+
// fires `before` too, but leaves nothing: no dialog. A visit another
|
|
84
|
+
// guard on the page already stopped is that guard's to ask about.
|
|
85
|
+
if (visit.prefetch || event.defaultPrevented) return;
|
|
77
86
|
|
|
78
87
|
if (bypassNextVisit) {
|
|
79
88
|
bypassNextVisit = false;
|
|
@@ -85,32 +94,123 @@ export const useConfirmOnRouteLeave = ({
|
|
|
85
94
|
|
|
86
95
|
const nextPath = normalizeUrl(visit.url.href);
|
|
87
96
|
if (nextPath === normalizeUrl(window.location.href)) return;
|
|
88
|
-
if (
|
|
97
|
+
if (
|
|
98
|
+
visit.method !== 'get' &&
|
|
99
|
+
toValue(whitelist).map(normalizeUrl).includes(nextPath)
|
|
100
|
+
) {
|
|
101
|
+
// Marked: a save answered with a 409 redirect (a redirect with a
|
|
102
|
+
// #fragment) is re-sent by Inertia with these same headers, and that
|
|
103
|
+
// follow-up must not ask either.
|
|
104
|
+
visit.headers = { ...visit.headers, [WHITELIST_HEADER]: '1' };
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
89
107
|
|
|
90
108
|
// `return`, not `return false`: an Inertia `before` listener that returns
|
|
91
109
|
// false CANCELS the visit, which is the opposite of what `onBefore`
|
|
92
110
|
// promises (false = let this visit through, no dialog).
|
|
93
111
|
if (onBefore(visit) === false) return;
|
|
94
112
|
event.preventDefault();
|
|
113
|
+
// Held, not refused: the dialog decides. `usePromiseForm` reads this
|
|
114
|
+
// flag and waits instead of answering `false` at once.
|
|
115
|
+
visit.awaitingConfirm = true;
|
|
95
116
|
|
|
96
117
|
void (async () => {
|
|
97
|
-
const
|
|
98
|
-
|
|
118
|
+
const words = messages.value.leave;
|
|
119
|
+
const confirmed = await confirm({
|
|
120
|
+
title: title ?? words.title,
|
|
121
|
+
text: text ?? words.text,
|
|
122
|
+
yes: yes ?? words.yes,
|
|
123
|
+
no: no ?? words.no,
|
|
124
|
+
variant,
|
|
125
|
+
...(stack && { stack }),
|
|
126
|
+
});
|
|
127
|
+
if (!confirmed) {
|
|
128
|
+
// "Keep editing" ends the held visit the way Inertia ends a
|
|
129
|
+
// cancelled one, so its own callbacks (a form's processing state,
|
|
130
|
+
// a pending promise) settle.
|
|
131
|
+
const callbacks = visit as {
|
|
132
|
+
onCancel?: () => void;
|
|
133
|
+
onFinish?: (visit: Visit) => void;
|
|
134
|
+
};
|
|
135
|
+
callbacks.onCancel?.();
|
|
136
|
+
callbacks.onFinish?.({ ...visit, cancelled: true });
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
99
139
|
|
|
100
140
|
await onLeave(visit);
|
|
141
|
+
// Re-sent as a plain visit: never as a prefetch or a background one,
|
|
142
|
+
// and without the visit's own onBefore (its `confirm('Sure?')` was
|
|
143
|
+
// already answered).
|
|
144
|
+
const {
|
|
145
|
+
prefetch: _prefetch,
|
|
146
|
+
async: _async,
|
|
147
|
+
onBefore: _onBefore,
|
|
148
|
+
awaitingConfirm: _awaitingConfirm,
|
|
149
|
+
...rest
|
|
150
|
+
} = visit;
|
|
151
|
+
// `before` fires inside router.visit(): the bypass covers exactly
|
|
152
|
+
// that visit, and is never left armed for the next one (a re-send
|
|
153
|
+
// its own callbacks stop before `before`).
|
|
101
154
|
bypassNextVisit = true;
|
|
102
|
-
// Re-sent as a plain visit: never as a prefetch or a background one.
|
|
103
|
-
const { prefetch: _prefetch, async: _async, ...rest } = visit;
|
|
104
155
|
router.visit(visit.url, {
|
|
105
156
|
...(rest as Parameters<typeof router.visit>[1]),
|
|
106
157
|
headers: { ...visit.headers, [WHITELIST_HEADER]: '1' },
|
|
107
158
|
});
|
|
159
|
+
bypassNextVisit = false;
|
|
108
160
|
})();
|
|
109
161
|
});
|
|
110
162
|
|
|
163
|
+
// Leaving the app (closing the tab, reloading, a typed URL, a plain `<a>`)
|
|
164
|
+
// never reaches `before`: the browser's own prompt covers it. Not for a
|
|
165
|
+
// visit this guard let through: a save answered with `Inertia::location()`
|
|
166
|
+
// is a real unload with the form still dirty. Inertia sets
|
|
167
|
+
// `window.location` inside that visit, and WebKit asks only after the visit
|
|
168
|
+
// finished, so a let-through visit that completed with no answer (no page,
|
|
169
|
+
// no error bag, no error response: Inertia left) keeps it quiet until the
|
|
170
|
+
// next visit starts.
|
|
171
|
+
const released = new Set<unknown>();
|
|
172
|
+
let answered = false;
|
|
173
|
+
let leaving = false;
|
|
174
|
+
const marked = (visit: { headers?: Record<string, string> }) =>
|
|
175
|
+
getHeaderValue(visit.headers, WHITELIST_HEADER) === '1';
|
|
176
|
+
const unbindStart = router.on('start', ({ detail: { visit } }) => {
|
|
177
|
+
leaving = false;
|
|
178
|
+
answered = false;
|
|
179
|
+
if (marked(visit)) released.add(visit);
|
|
180
|
+
});
|
|
181
|
+
// Every way a visit gets an answer; the last two pairs are Inertia 3's
|
|
182
|
+
// names and Inertia 2's (each fires only on its own version).
|
|
183
|
+
const onRouterEvent = (name: string, listener: () => void) =>
|
|
184
|
+
router.on(name as 'start', listener);
|
|
185
|
+
const unbindAnswers = [
|
|
186
|
+
'success',
|
|
187
|
+
'error',
|
|
188
|
+
'httpException',
|
|
189
|
+
'invalid',
|
|
190
|
+
'networkError',
|
|
191
|
+
'exception',
|
|
192
|
+
].map((name) => onRouterEvent(name, () => void (answered = true)));
|
|
193
|
+
const unbindFinish = router.on('finish', ({ detail: { visit } }) => {
|
|
194
|
+
released.delete(visit);
|
|
195
|
+
if (marked(visit) && visit.completed && !answered) leaving = true;
|
|
196
|
+
});
|
|
197
|
+
const onBeforeUnload = (event: BeforeUnloadEvent) => {
|
|
198
|
+
if (toValue(disabled) || released.size || leaving) return;
|
|
199
|
+
event.preventDefault();
|
|
200
|
+
event.returnValue = '';
|
|
201
|
+
};
|
|
202
|
+
const inBrowser = typeof window !== 'undefined';
|
|
203
|
+
if (inBrowser) window.addEventListener('beforeunload', onBeforeUnload);
|
|
204
|
+
|
|
111
205
|
// `router.on` is a global bus that outlives this component: unbind it.
|
|
112
206
|
// `onScopeDispose` over `onUnmounted`: it fires on unmount too (`setup()`
|
|
113
207
|
// runs inside a component's own effect scope), but also works if this
|
|
114
208
|
// composable is ever called from outside one.
|
|
115
|
-
onScopeDispose(
|
|
209
|
+
onScopeDispose(() => {
|
|
210
|
+
unbind();
|
|
211
|
+
unbindStart();
|
|
212
|
+
unbindAnswers.forEach((unbindAnswer) => unbindAnswer());
|
|
213
|
+
unbindFinish();
|
|
214
|
+
if (inBrowser) window.removeEventListener('beforeunload', onBeforeUnload);
|
|
215
|
+
});
|
|
116
216
|
};
|
|
@@ -1,36 +1,54 @@
|
|
|
1
1
|
// inertia-helpers/useHasChanged.ts
|
|
2
|
-
import {
|
|
2
|
+
import { isPlainObject } from './utils';
|
|
3
|
+
import { hash } from 'object-code';
|
|
3
4
|
import { computed, ref, toValue, type WatchSource } from 'vue';
|
|
4
5
|
|
|
5
6
|
export function useHasChanged<T extends WatchSource<unknown>[]>(
|
|
6
7
|
dependencies: T
|
|
7
8
|
) {
|
|
8
9
|
/*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
10
|
+
* The baseline is a structural HASH, not a copy. A copy has to survive the
|
|
11
|
+
* values a form holds: a JSON round-trip turns a Date into a string, a File,
|
|
12
|
+
* Set or Map into `{}`, drops `undefined` (and throws on a bare one), so
|
|
13
|
+
* an untouched form read as changed; `structuredClone` throws on Vue proxies.
|
|
14
|
+
* `object-code` hashes straight through a proxy and keeps Dates (by time),
|
|
15
|
+
* Sets / Maps (by entries), `undefined` and NaN apart, and a number is all
|
|
16
|
+
* the baseline needs to hold. It hashes every File alike (no own keys), so
|
|
17
|
+
* a File is read by name, size, type and date first: picking another file
|
|
18
|
+
* over a picked one counts as a change.
|
|
18
19
|
*/
|
|
19
20
|
const snapshot = (dep: WatchSource<unknown>) =>
|
|
20
|
-
|
|
21
|
+
hash(withFileFingerprints(toValue(dep)));
|
|
21
22
|
|
|
22
|
-
const
|
|
23
|
+
const baselines = ref<number[]>(dependencies.map(snapshot));
|
|
23
24
|
|
|
24
25
|
/** Re-baseline (call after a successful save). */
|
|
25
26
|
const takeSnapshot = (): void => {
|
|
26
|
-
|
|
27
|
+
baselines.value = dependencies.map(snapshot);
|
|
27
28
|
};
|
|
28
29
|
|
|
29
30
|
const hasChanged = computed(() =>
|
|
30
|
-
dependencies.some(
|
|
31
|
-
(dep, index) => !isEqual(toValue(dep), baselineValues.value[index])
|
|
32
|
-
)
|
|
31
|
+
dependencies.some((dep, index) => snapshot(dep) !== baselines.value[index])
|
|
33
32
|
);
|
|
34
33
|
|
|
35
34
|
return { hasChanged, takeSnapshot };
|
|
36
35
|
}
|
|
36
|
+
|
|
37
|
+
const withFileFingerprints = (value: unknown): unknown =>
|
|
38
|
+
value instanceof Blob
|
|
39
|
+
? [
|
|
40
|
+
value.size,
|
|
41
|
+
value.type,
|
|
42
|
+
(value as Partial<File>).name,
|
|
43
|
+
(value as Partial<File>).lastModified,
|
|
44
|
+
]
|
|
45
|
+
: Array.isArray(value)
|
|
46
|
+
? value.map(withFileFingerprints)
|
|
47
|
+
: isPlainObject(value)
|
|
48
|
+
? Object.fromEntries(
|
|
49
|
+
Object.entries(value).map(([key, item]) => [
|
|
50
|
+
key,
|
|
51
|
+
withFileFingerprints(item),
|
|
52
|
+
])
|
|
53
|
+
)
|
|
54
|
+
: value;
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
// inertia-helpers/useLazyProp.ts
|
|
2
2
|
import { isEqual, isNil, isNotNil } from './utils';
|
|
3
|
+
import type { PageProps } from '@inertiajs/core';
|
|
3
4
|
import { router, usePage } from '@inertiajs/vue3';
|
|
4
5
|
import type { Ref } from 'vue';
|
|
5
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
customRef,
|
|
8
|
+
nextTick,
|
|
9
|
+
onBeforeUnmount,
|
|
10
|
+
readonly,
|
|
11
|
+
ref,
|
|
12
|
+
watch,
|
|
13
|
+
} from 'vue';
|
|
6
14
|
|
|
7
15
|
export type UseLazyPropOptions = {
|
|
8
16
|
/** Keep the last non-nil value across visits that reset the prop. Default true. */
|
|
@@ -34,7 +42,9 @@ export function useLazyProp<T>(
|
|
|
34
42
|
initialValue?: T,
|
|
35
43
|
options: UseLazyPropOptions = {}
|
|
36
44
|
): UseLazyProp<T | undefined> {
|
|
37
|
-
|
|
45
|
+
// `PageProps &` keeps usePage's constraint satisfied in an app that
|
|
46
|
+
// augments Inertia's `PageProps` (TS2344 otherwise).
|
|
47
|
+
const page = usePage<PageProps & { [key: string]: T | undefined }>();
|
|
38
48
|
const { cache = true } = options;
|
|
39
49
|
const loading = ref(false);
|
|
40
50
|
const loaded = ref(page.props[propName] !== undefined);
|
|
@@ -42,50 +52,107 @@ export function useLazyProp<T>(
|
|
|
42
52
|
let inFlightPromise: Promise<void> | null = null;
|
|
43
53
|
let isUnmounted = false;
|
|
44
54
|
let cached = page.props[propName];
|
|
55
|
+
// Settles a reload a navigation cancelled, once that navigation is over.
|
|
56
|
+
let resumeCancelled: (() => void) | null = null;
|
|
45
57
|
|
|
46
58
|
onBeforeUnmount(() => {
|
|
47
59
|
isUnmounted = true;
|
|
60
|
+
resumeCancelled?.();
|
|
48
61
|
});
|
|
49
62
|
|
|
50
63
|
const loadProp = (onSuccess: () => void): Promise<void> => {
|
|
51
64
|
if (inFlightPromise) return inFlightPromise;
|
|
52
65
|
|
|
53
66
|
error.value = null;
|
|
67
|
+
let started = false;
|
|
68
|
+
let succeeded = false;
|
|
69
|
+
let settle!: () => void;
|
|
70
|
+
const promise = new Promise<void>((resolve) => {
|
|
71
|
+
settle = () => {
|
|
72
|
+
if (!isUnmounted) loading.value = false;
|
|
73
|
+
if (inFlightPromise === promise) inFlightPromise = null;
|
|
74
|
+
resolve();
|
|
75
|
+
};
|
|
76
|
+
});
|
|
77
|
+
inFlightPromise = promise;
|
|
54
78
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
79
|
+
router.reload({
|
|
80
|
+
only: [propName],
|
|
81
|
+
replace: true,
|
|
82
|
+
// Like Inertia's own deferred-props loader: keep the page's
|
|
83
|
+
// validation errors (Inertia 3; Inertia 2 has no such option).
|
|
84
|
+
...({ preserveErrors: true } as object),
|
|
85
|
+
...(options.headers ? { headers: options.headers } : {}),
|
|
86
|
+
onStart: () => {
|
|
87
|
+
started = true;
|
|
88
|
+
loading.value = true;
|
|
89
|
+
},
|
|
90
|
+
onSuccess: () => {
|
|
91
|
+
succeeded = true;
|
|
92
|
+
if (!isUnmounted) {
|
|
93
|
+
loaded.value = true;
|
|
94
|
+
cached = page.props[propName];
|
|
95
|
+
onSuccess();
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
onError: (errors) => {
|
|
99
|
+
if (!isUnmounted) {
|
|
100
|
+
error.value =
|
|
101
|
+
errors instanceof Error
|
|
102
|
+
? errors
|
|
103
|
+
: new Error(`Failed to load lazy prop "${propName}"`);
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
onFinish: (visit) => {
|
|
107
|
+
// A visit to another page cancels this reload: no failure. Stay
|
|
108
|
+
// loading (a reader must not re-fire it at the outgoing URL) until
|
|
109
|
+
// that navigation is over, then retry if this page is still
|
|
110
|
+
// mounted (a same-component visit) or settle if it unmounted.
|
|
111
|
+
if (visit.cancelled) {
|
|
112
|
+
const cancelledOn = page.url;
|
|
113
|
+
const unbindFinish = router.on('finish', (event) => {
|
|
114
|
+
const next = event.detail.visit;
|
|
115
|
+
if (next.cancelled || next.interrupted) return;
|
|
116
|
+
// An async visit is over too (a mutation answered on another
|
|
117
|
+
// URL redirects back with no other visit), unless it is one
|
|
118
|
+
// of this page's own reloads (a poll, another lazy prop).
|
|
119
|
+
if (
|
|
120
|
+
next.async &&
|
|
121
|
+
next.url.href === new URL(cancelledOn, next.url).href
|
|
122
|
+
)
|
|
123
|
+
return;
|
|
124
|
+
resumeCancelled?.();
|
|
125
|
+
});
|
|
126
|
+
// Back / Forward swaps the page with no finish event.
|
|
127
|
+
const unbindNavigate = router.on('navigate', () =>
|
|
128
|
+
resumeCancelled?.()
|
|
129
|
+
);
|
|
130
|
+
resumeCancelled = () => {
|
|
131
|
+
resumeCancelled = null;
|
|
132
|
+
unbindFinish();
|
|
133
|
+
unbindNavigate();
|
|
134
|
+
void nextTick(() => {
|
|
135
|
+
inFlightPromise = null;
|
|
136
|
+
if (isUnmounted || loaded.value) settle();
|
|
137
|
+
else void loadProp(onSuccess).then(settle);
|
|
138
|
+
});
|
|
139
|
+
};
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
// A network error or a non-Inertia response (Inertia's error
|
|
143
|
+
// dialog) reaches only onFinish: report it. The self-loading read
|
|
144
|
+
// does not retry a failed load; `execute()` does.
|
|
145
|
+
if (!succeeded && !error.value && !isUnmounted) {
|
|
146
|
+
error.value = new Error(`Failed to load lazy prop "${propName}"`);
|
|
147
|
+
}
|
|
148
|
+
settle();
|
|
149
|
+
},
|
|
86
150
|
});
|
|
151
|
+
// Refused before it started (a `before` listener cancelled it, or no
|
|
152
|
+
// `window` during SSR): no callback will ever come.
|
|
153
|
+
if (!started) settle();
|
|
87
154
|
|
|
88
|
-
return
|
|
155
|
+
return promise;
|
|
89
156
|
};
|
|
90
157
|
|
|
91
158
|
/** Self-loading ref: reading it while the prop is absent starts the fetch. */
|
|
@@ -94,8 +161,7 @@ export function useLazyProp<T>(
|
|
|
94
161
|
track();
|
|
95
162
|
const isMissing = page.props[propName] === undefined;
|
|
96
163
|
|
|
97
|
-
if (isMissing && !loaded.value && !loading.value) {
|
|
98
|
-
loading.value = true;
|
|
164
|
+
if (isMissing && !loaded.value && !loading.value && !error.value) {
|
|
99
165
|
void loadProp(trigger);
|
|
100
166
|
}
|
|
101
167
|
|
|
@@ -39,7 +39,9 @@ export function useLazyPropGetter<T, U = T>(
|
|
|
39
39
|
if (!loaded.value) {
|
|
40
40
|
await execute();
|
|
41
41
|
}
|
|
42
|
-
|
|
42
|
+
// `loaded` first: reading `value` while unloaded would start a
|
|
43
|
+
// second, background reload.
|
|
44
|
+
if (!loaded.value || isNil(value.value)) {
|
|
43
45
|
throw new Error(`Could not load lazy prop "${propName}"`);
|
|
44
46
|
}
|
|
45
47
|
if (isNil(formatter)) return value.value as unknown as U;
|