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,95 +1,98 @@
|
|
|
1
1
|
// inertia-helpers/useQueryObject.ts
|
|
2
2
|
// requires: npm i query-string
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
import {
|
|
4
|
+
QUERY_ARRAY_FORMAT,
|
|
5
|
+
currentUrl,
|
|
6
|
+
parseQuery,
|
|
7
|
+
visitUrl,
|
|
8
|
+
withQuery,
|
|
9
|
+
} from './useQuery';
|
|
10
|
+
import type { VisitOptions } from '@inertiajs/core';
|
|
11
|
+
import qs from 'query-string';
|
|
7
12
|
import { onScopeDispose, toValue, watch, type MaybeRefOrGetter } from 'vue';
|
|
8
13
|
|
|
14
|
+
/** A server visit's options; the URL, method and data are the helper's. */
|
|
15
|
+
type ServerVisitOptions = Omit<VisitOptions, 'method' | 'data' | 'preserveUrl'>;
|
|
16
|
+
/** A URL-only write (`server: false`) has no request to configure. */
|
|
17
|
+
type ClientVisitOptions = Pick<
|
|
18
|
+
VisitOptions,
|
|
19
|
+
'preserveState' | 'preserveScroll' | 'replace'
|
|
20
|
+
>;
|
|
21
|
+
|
|
9
22
|
type Options<T> = {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Query keys removed on every change. Default `['page']`, so a filter
|
|
25
|
+
* change never strands the list on a page past its new end. Passing it
|
|
26
|
+
* REPLACES the default: name `page` too when it should still reset.
|
|
27
|
+
*/
|
|
14
28
|
resetKeys?: string[];
|
|
15
|
-
/** Inertia partial reload prop names (supports reactive getter). */
|
|
16
|
-
only?: MaybeRefOrGetter<string[]>;
|
|
17
29
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
30
|
+
* While it resolves to `true`, changes are tracked but no visit is sent.
|
|
31
|
+
* When it goes back to `false`, the object's current state is sent once,
|
|
32
|
+
* if the URL does not already hold it. See the guide for when to use it.
|
|
21
33
|
*/
|
|
22
|
-
|
|
34
|
+
paused?: MaybeRefOrGetter<boolean>;
|
|
23
35
|
/**
|
|
24
36
|
* Keys whose changes wait this many ms before the visit, e.g. a search
|
|
25
37
|
* box: `{ search: 300 }`. A change to any other key visits at once and
|
|
26
38
|
* carries the waiting value with it, so nothing typed is lost.
|
|
27
39
|
*/
|
|
28
40
|
debounce?: Partial<Record<keyof T & string, number>>;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
41
|
+
} & (
|
|
42
|
+
| {
|
|
43
|
+
/** Default `true`: every change is an Inertia visit. */
|
|
44
|
+
server?: true;
|
|
45
|
+
/** Merged over `{ preserveState, preserveScroll, replace: true }`. */
|
|
46
|
+
visitOptions?: MaybeRefOrGetter<ServerVisitOptions>;
|
|
47
|
+
}
|
|
48
|
+
| {
|
|
49
|
+
/** `false` writes the URL with no request (`router.replace` / `push`). */
|
|
50
|
+
server: false;
|
|
51
|
+
visitOptions?: MaybeRefOrGetter<ClientVisitOptions>;
|
|
52
|
+
}
|
|
53
|
+
);
|
|
34
54
|
|
|
35
55
|
export function useQueryObject<T extends Record<string, unknown>>(
|
|
36
56
|
object: MaybeRefOrGetter<T>,
|
|
37
57
|
options: Options<T> = {}
|
|
38
58
|
): void {
|
|
39
59
|
const {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
resetKeys = [],
|
|
43
|
-
preserveState = true,
|
|
44
|
-
preserveScroll = true,
|
|
45
|
-
replace = true,
|
|
46
|
-
only: onlyOption = [],
|
|
47
|
-
shouldNavigate = true,
|
|
60
|
+
resetKeys = ['page'],
|
|
61
|
+
paused = false,
|
|
48
62
|
debounce = {},
|
|
49
|
-
|
|
63
|
+
server = true,
|
|
64
|
+
visitOptions,
|
|
50
65
|
} = options;
|
|
51
66
|
|
|
52
|
-
const filterValidValues = (obj: T): Partial<T> => {
|
|
53
|
-
const filtered: Partial<T> = {};
|
|
54
|
-
Object.entries(obj).forEach(([key, val]) => {
|
|
55
|
-
if (!isNil(val) && val !== '') {
|
|
56
|
-
(filtered as Record<string, unknown>)[key] = val;
|
|
57
|
-
}
|
|
58
|
-
});
|
|
59
|
-
return filtered;
|
|
60
|
-
};
|
|
61
|
-
|
|
62
67
|
/** Every key this composable has ever managed, for stable URL cleanup. */
|
|
63
68
|
const seenKeys = new Set<string>(Object.keys(toValue(object) ?? {}));
|
|
64
69
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
// Start from the existing query to preserve keys owned elsewhere.
|
|
70
|
-
const existingQuery = useQuery({ arrayFormat });
|
|
71
|
-
seenKeys.forEach((key) => delete existingQuery[key]);
|
|
72
|
-
if (resetPage) delete existingQuery.page;
|
|
73
|
-
resetKeys.forEach((key) => delete existingQuery[key]);
|
|
74
|
-
|
|
75
|
-
Object.entries(transformedValues).forEach(([key, val]) => {
|
|
76
|
-
if (!isEmpty(val)) existingQuery[key] = val;
|
|
77
|
-
});
|
|
70
|
+
/** A query in one canonical form, so encoding and key order never count. */
|
|
71
|
+
const canonical = (url: string) =>
|
|
72
|
+
qs.stringify(parseQuery(url), { arrayFormat: QUERY_ARRAY_FORMAT });
|
|
78
73
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
? [window.location.pathname, stringified].join('?')
|
|
82
|
-
: window.location.pathname;
|
|
74
|
+
const navigate = (newObject: T) => {
|
|
75
|
+
if (toValue(paused)) return;
|
|
83
76
|
|
|
84
|
-
|
|
85
|
-
|
|
77
|
+
// Only the reset keys and the keys this object owns change; every
|
|
78
|
+
// other parameter (a tab, a sort) stays as it is.
|
|
79
|
+
const changes: Record<string, unknown> = {};
|
|
80
|
+
resetKeys.forEach((key) => (changes[key] = null));
|
|
81
|
+
seenKeys.forEach((key) => (changes[key] = newObject[key]));
|
|
82
|
+
const from = currentUrl();
|
|
83
|
+
const url = withQuery(from, changes);
|
|
84
|
+
if (canonical(url) === canonical(from)) return;
|
|
86
85
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
86
|
+
visitUrl(
|
|
87
|
+
url,
|
|
88
|
+
{
|
|
89
|
+
preserveState: true,
|
|
90
|
+
preserveScroll: true,
|
|
91
|
+
replace: true,
|
|
92
|
+
...toValue(visitOptions),
|
|
93
|
+
},
|
|
94
|
+
server
|
|
95
|
+
);
|
|
93
96
|
};
|
|
94
97
|
|
|
95
98
|
/** Each key's value as last seen, to tell which keys a change touched. */
|
|
@@ -102,8 +105,8 @@ export function useQueryObject<T extends Record<string, unknown>>(
|
|
|
102
105
|
onScopeDispose(() => clearTimeout(pending));
|
|
103
106
|
|
|
104
107
|
watch(
|
|
105
|
-
() => toValue(object),
|
|
106
|
-
(newObject) => {
|
|
108
|
+
[() => toValue(object), () => toValue(paused)],
|
|
109
|
+
([newObject, isPaused], [, wasPaused]) => {
|
|
107
110
|
if (!newObject) return;
|
|
108
111
|
|
|
109
112
|
Object.keys(newObject).forEach((key) => {
|
|
@@ -122,12 +125,15 @@ export function useQueryObject<T extends Record<string, unknown>>(
|
|
|
122
125
|
);
|
|
123
126
|
lastSeen = now;
|
|
124
127
|
clearTimeout(pending);
|
|
128
|
+
if (isPaused) return;
|
|
129
|
+
// Resuming sends what changed while paused, without waiting.
|
|
130
|
+
const resumed = wasPaused && !isPaused;
|
|
125
131
|
const waits = changed.map(
|
|
126
132
|
(key) => (debounce as Record<string, number | undefined>)[key] ?? 0
|
|
127
133
|
);
|
|
128
134
|
// Only debounced keys changed: wait for the typing to settle, then
|
|
129
135
|
// visit with whatever the object holds by then.
|
|
130
|
-
if (changed.length && waits.every((ms) => ms > 0)) {
|
|
136
|
+
if (!resumed && changed.length && waits.every((ms) => ms > 0)) {
|
|
131
137
|
pending = setTimeout(
|
|
132
138
|
() => navigate(toValue(object)),
|
|
133
139
|
Math.max(...waits)
|
|
@@ -141,7 +147,8 @@ export function useQueryObject<T extends Record<string, unknown>>(
|
|
|
141
147
|
* into one watcher run, so a `clearFilters()` that blanks three keys
|
|
142
148
|
* fires ONE visit instead of three (measured: 3 writes → 3 runs under
|
|
143
149
|
* `sync`, 1 under the default). The two it saves used to self-cancel,
|
|
144
|
-
* but they were still sent.
|
|
150
|
+
* but they were still sent. Watching `paused` in the same source means
|
|
151
|
+
* resuming and the change that comes with it are one run, one visit.
|
|
145
152
|
*
|
|
146
153
|
* This does NOT make typing cheap on its own — one visit per settled
|
|
147
154
|
* keystroke is still one visit per keystroke. `debounce` is for that.
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
// inertia-helpers/useResourceContext.ts
|
|
2
2
|
import { useLoading } from './useLoading';
|
|
3
3
|
import { noop } from './utils';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
onScopeDispose,
|
|
6
|
+
ref,
|
|
7
|
+
toValue,
|
|
8
|
+
watch,
|
|
9
|
+
type MaybeRefOrGetter,
|
|
10
|
+
type Ref,
|
|
11
|
+
} from 'vue';
|
|
5
12
|
|
|
6
13
|
type UseResourceContextOptions<T, ID> = {
|
|
7
14
|
immediate?: boolean;
|
|
@@ -14,7 +21,8 @@ type UseResourceContextOptions<T, ID> = {
|
|
|
14
21
|
|
|
15
22
|
export function useResourceContext<T, ID = string>(
|
|
16
23
|
id: MaybeRefOrGetter<ID | null | undefined>,
|
|
17
|
-
|
|
24
|
+
/** `signal` aborts when the load goes stale: pass it to `fetch` / axios. */
|
|
25
|
+
fn: (id: ID, signal: AbortSignal) => Promise<T>,
|
|
18
26
|
options: UseResourceContextOptions<T, ID> = {}
|
|
19
27
|
) {
|
|
20
28
|
const data = ref<T | null>(null) as Ref<T | null>;
|
|
@@ -28,31 +36,69 @@ export function useResourceContext<T, ID = string>(
|
|
|
28
36
|
onFinish = noop,
|
|
29
37
|
} = options;
|
|
30
38
|
|
|
31
|
-
const { loading, fn: fetch } = useLoading(
|
|
39
|
+
const { loading, fn: fetch } = useLoading(
|
|
40
|
+
(resourceId: ID, signal: AbortSignal) => fn(resourceId, signal)
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
// Each load takes a ticket; a newer load, an id change or the scope's end
|
|
44
|
+
// makes older tickets stale, and a stale response is dropped (no data, no
|
|
45
|
+
// callbacks): a slow answer for the row the user left never lands. Going
|
|
46
|
+
// stale also aborts its request.
|
|
47
|
+
let ticket = 0;
|
|
48
|
+
let controller: AbortController | null = null;
|
|
49
|
+
const stale = () => {
|
|
50
|
+
ticket++;
|
|
51
|
+
controller?.abort();
|
|
52
|
+
};
|
|
53
|
+
let loadedId: ID | null = null;
|
|
54
|
+
const clear = () => {
|
|
55
|
+
stale();
|
|
56
|
+
data.value = null;
|
|
57
|
+
loadedId = null;
|
|
58
|
+
};
|
|
59
|
+
onScopeDispose(stale);
|
|
32
60
|
|
|
33
61
|
const load = async (resourceId: ID) => {
|
|
62
|
+
stale();
|
|
63
|
+
const own = ticket;
|
|
64
|
+
controller = new AbortController();
|
|
65
|
+
const outcome = await fetch(resourceId, controller.signal).then(
|
|
66
|
+
(result) => ({ ok: true as const, result }),
|
|
67
|
+
(error: unknown) => ({ ok: false as const, error })
|
|
68
|
+
);
|
|
69
|
+
if (own !== ticket) return;
|
|
34
70
|
try {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
71
|
+
if (outcome.ok) {
|
|
72
|
+
data.value = outcome.result;
|
|
73
|
+
loadedId = resourceId;
|
|
74
|
+
// Outside the fetch's catch: a throwing onSuccess is the
|
|
75
|
+
// caller's bug, not a failed fetch.
|
|
76
|
+
await onSuccess(outcome.result, resourceId);
|
|
77
|
+
} else {
|
|
78
|
+
clear();
|
|
79
|
+
await onError(outcome.error, resourceId);
|
|
80
|
+
}
|
|
41
81
|
} finally {
|
|
42
82
|
await onFinish(resourceId);
|
|
43
83
|
}
|
|
44
84
|
};
|
|
45
85
|
|
|
86
|
+
// Per-item sources: a re-run that yields the same id and flag (a partial
|
|
87
|
+
// reload handing a new row object with the same id) does not refetch.
|
|
46
88
|
watch(
|
|
47
|
-
() =>
|
|
48
|
-
async (
|
|
89
|
+
[() => toValue(id), () => toValue(disabled)],
|
|
90
|
+
async ([resourceId, isDisabled]) => {
|
|
49
91
|
if (!resourceId) {
|
|
50
|
-
|
|
92
|
+
clear();
|
|
51
93
|
await options.onReset?.();
|
|
52
94
|
return;
|
|
53
95
|
}
|
|
54
|
-
// Keep already-loaded data while disabled
|
|
55
|
-
|
|
96
|
+
// Keep already-loaded data while disabled, but only for the id it
|
|
97
|
+
// was loaded for: never show row a's resource under row b.
|
|
98
|
+
if (isDisabled) {
|
|
99
|
+
if (resourceId !== loadedId) clear();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
56
102
|
await load(resourceId as ID);
|
|
57
103
|
},
|
|
58
104
|
{ immediate }
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// inertia-helpers/useRouteQuery.ts
|
|
2
2
|
// requires: npm i query-string
|
|
3
|
-
import {
|
|
3
|
+
import { currentUrl, splitUrl, visitUrl, withQuery } from './useQuery';
|
|
4
|
+
import type { VisitOptions } from '@inertiajs/core';
|
|
4
5
|
import qs from 'query-string';
|
|
5
6
|
import {
|
|
6
7
|
computed,
|
|
@@ -12,8 +13,15 @@ import {
|
|
|
12
13
|
type Options<T> = {
|
|
13
14
|
/** Parse the raw string (e.g. `Number`). Defaults to identity. */
|
|
14
15
|
transform?: (value: string) => T;
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
/** `false` writes the URL with no request. Default `true`. */
|
|
17
|
+
server?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Merged over `{ preserveState, preserveScroll, replace: true }`. With
|
|
20
|
+
* `server: false` only those three apply.
|
|
21
|
+
*/
|
|
22
|
+
visitOptions?: MaybeRefOrGetter<
|
|
23
|
+
Omit<VisitOptions, 'method' | 'data' | 'preserveUrl'>
|
|
24
|
+
>;
|
|
17
25
|
};
|
|
18
26
|
|
|
19
27
|
export function useRouteQuery<T = string>(
|
|
@@ -24,37 +32,34 @@ export function useRouteQuery<T = string>(
|
|
|
24
32
|
): WritableComputedRef<T> {
|
|
25
33
|
const {
|
|
26
34
|
transform = (value) => value as unknown as T,
|
|
27
|
-
|
|
28
|
-
|
|
35
|
+
server = true,
|
|
36
|
+
visitOptions,
|
|
29
37
|
} = options;
|
|
30
|
-
const page = usePage();
|
|
31
38
|
|
|
32
39
|
const readRaw = (): string => {
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
const single = Array.isArray(raw) ? raw
|
|
40
|
+
const raw = qs.parse(splitUrl(currentUrl()).query)[key];
|
|
41
|
+
// A repeated key reads its last value, as PHP does.
|
|
42
|
+
const single = Array.isArray(raw) ? raw.at(-1) : raw;
|
|
36
43
|
return single != null ? String(single) : toValue(defaultValue);
|
|
37
44
|
};
|
|
38
45
|
|
|
39
46
|
return computed<T>({
|
|
40
47
|
get: () => transform(readRaw()),
|
|
41
48
|
set: (next) => {
|
|
42
|
-
|
|
49
|
+
// `null` / `undefined` mean "no value": the key leaves the URL.
|
|
50
|
+
const nextRaw = next == null ? toValue(defaultValue) : String(next);
|
|
43
51
|
if (nextRaw === readRaw()) return;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
preserveState,
|
|
56
|
-
replace: true,
|
|
57
|
-
});
|
|
52
|
+
const value = nextRaw === toValue(defaultValue) ? null : nextRaw; // keep URLs clean at the default
|
|
53
|
+
visitUrl(
|
|
54
|
+
withQuery(currentUrl(), { [key]: value }),
|
|
55
|
+
{
|
|
56
|
+
preserveScroll: true,
|
|
57
|
+
preserveState: true,
|
|
58
|
+
replace: true,
|
|
59
|
+
...toValue(visitOptions),
|
|
60
|
+
},
|
|
61
|
+
server
|
|
62
|
+
);
|
|
58
63
|
},
|
|
59
64
|
});
|
|
60
65
|
}
|
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
// inertia-helpers/useScreenSizes.ts
|
|
2
2
|
import { createSharedComposable, useWindowSize } from '@vueuse/core';
|
|
3
|
+
import { useMobile } from 'bitboss-ui';
|
|
3
4
|
import { computed } from 'vue';
|
|
4
5
|
|
|
5
|
-
/** Tailwind
|
|
6
|
-
export const SCREEN_BREAKPOINT_MD = 768;
|
|
7
|
-
/** Tailwind lg breakpoint (px). Tablet when 768 <= width < 1024. */
|
|
6
|
+
/** Tailwind lg breakpoint (px). Tablet from the library's mobile cut up to here. */
|
|
8
7
|
export const SCREEN_BREAKPOINT_LG = 1024;
|
|
9
8
|
|
|
10
9
|
const useScreenSizesImpl = () => {
|
|
11
10
|
const { width } = useWindowSize();
|
|
11
|
+
// The library's own signal (below the plugin's `mobileMaxWidth`), so a page
|
|
12
|
+
// and the components on it never disagree about "mobile".
|
|
13
|
+
const { isMobile } = useMobile();
|
|
12
14
|
|
|
13
|
-
const isMobile = computed(() => width.value < SCREEN_BREAKPOINT_MD);
|
|
14
15
|
const isTablet = computed(
|
|
15
|
-
() =>
|
|
16
|
-
|
|
16
|
+
() => !isMobile.value && width.value < SCREEN_BREAKPOINT_LG
|
|
17
|
+
);
|
|
18
|
+
const isDesktop = computed(
|
|
19
|
+
() => !isMobile.value && width.value >= SCREEN_BREAKPOINT_LG
|
|
17
20
|
);
|
|
18
|
-
const isDesktop = computed(() => width.value >= SCREEN_BREAKPOINT_LG);
|
|
19
21
|
|
|
20
22
|
return { width, isMobile, isTablet, isDesktop };
|
|
21
23
|
};
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import {
|
|
3
3
|
usePaginatedResponseContext,
|
|
4
4
|
type PaginatedResponseContext,
|
|
5
|
+
type PaginatedResponseOptions,
|
|
5
6
|
} from './usePaginatedResponseContext';
|
|
6
7
|
import {
|
|
7
8
|
useTableSelectionContext,
|
|
@@ -16,7 +17,9 @@ export type TableContext<T, U> = TableSelectionContext<U> &
|
|
|
16
17
|
const toTableKey = (key: string): string => `${key}:table`;
|
|
17
18
|
|
|
18
19
|
export const useTableContext = <T = object, U = T>(
|
|
19
|
-
dataKey: string
|
|
20
|
+
dataKey: string,
|
|
21
|
+
/** For the paginated half: query keys and pager visit options. */
|
|
22
|
+
options?: PaginatedResponseOptions
|
|
20
23
|
): TableContext<T, U> => {
|
|
21
24
|
const existing = injectLocal<TableContext<T, U> | null>(
|
|
22
25
|
toTableKey(dataKey),
|
|
@@ -26,7 +29,7 @@ export const useTableContext = <T = object, U = T>(
|
|
|
26
29
|
|
|
27
30
|
const context: TableContext<T, U> = {
|
|
28
31
|
...useTableSelectionContext<U>(dataKey),
|
|
29
|
-
...usePaginatedResponseContext<T>(dataKey),
|
|
32
|
+
...usePaginatedResponseContext<T>(dataKey, options),
|
|
30
33
|
};
|
|
31
34
|
|
|
32
35
|
provideLocal(toTableKey(dataKey), context);
|
|
@@ -22,3 +22,17 @@ export const isPlainObject = (
|
|
|
22
22
|
typeof value === 'object' &&
|
|
23
23
|
value !== null &&
|
|
24
24
|
Object.getPrototypeOf(value) === Object.prototype;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Deep copy of server data (arrays and plain objects; anything else, e.g. a
|
|
28
|
+
* Date an app put there, is shared). Reads through Vue proxies, which
|
|
29
|
+
* `structuredClone` refuses.
|
|
30
|
+
*/
|
|
31
|
+
export const cloneDeep = <T>(value: T): T =>
|
|
32
|
+
(Array.isArray(value)
|
|
33
|
+
? value.map(cloneDeep)
|
|
34
|
+
: isPlainObject(value)
|
|
35
|
+
? Object.fromEntries(
|
|
36
|
+
Object.entries(value).map(([key, item]) => [key, cloneDeep(item)])
|
|
37
|
+
)
|
|
38
|
+
: value) as T;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"library": "bitboss-ui",
|
|
4
|
-
"version": "3.0.0-beta.
|
|
5
|
-
"generatedAt": "2026-10-
|
|
4
|
+
"version": "3.0.0-beta.65",
|
|
5
|
+
"generatedAt": "2026-10-27T14:11:46.011Z",
|
|
6
6
|
"about": "The bitboss-ui API for agents: open components/<Name>.json for one component (every prop, event, slot, v-model, pt part and state, and the public types its API uses; one member per line; a row or pt part written { name, common: true } is whole once in common.json, read it once). components/<Name>.tree.json has its component tree and CSS internals; files.removed the old names. The machine file with everything in one place is ../components.json.",
|
|
7
7
|
"reading": {
|
|
8
8
|
"components[]": "one row per component; description = its first sentence. Its API file is files.component with <Name> = name; its components/<Name>.tree.json (component tree, css classes, CSS custom properties) exists unless the row says tree: false.",
|
|
@@ -351,7 +351,7 @@ export function provideApprovalsInbox(): ApprovalsInboxContext {
|
|
|
351
351
|
});
|
|
352
352
|
// The search waits for typing to settle; every other filter visits at once.
|
|
353
353
|
useQueryObject(filters, {
|
|
354
|
-
only: [...RELOAD_PROPS],
|
|
354
|
+
visitOptions: { only: [...RELOAD_PROPS] },
|
|
355
355
|
debounce: { search: 300 },
|
|
356
356
|
});
|
|
357
357
|
|
|
@@ -495,7 +495,7 @@ export function provideBacklogWorkspace(): BacklogWorkspaceContext {
|
|
|
495
495
|
: String(filters.assignee.id ?? 'none'),
|
|
496
496
|
}),
|
|
497
497
|
// The search waits for typing to settle; every other filter visits at once.
|
|
498
|
-
{ only: [...RELOAD_PROPS], debounce: { search: 300 } }
|
|
498
|
+
{ visitOptions: { only: [...RELOAD_PROPS] }, debounce: { search: 300 } }
|
|
499
499
|
);
|
|
500
500
|
|
|
501
501
|
const hasActiveFilters = computed(
|
|
@@ -334,7 +334,7 @@ export function provideOwnershipAtlas(): OwnershipAtlasContext {
|
|
|
334
334
|
});
|
|
335
335
|
// The search waits for typing to settle; every other filter visits at once.
|
|
336
336
|
useQueryObject(filters, {
|
|
337
|
-
only: [...RELOAD_PROPS],
|
|
337
|
+
visitOptions: { only: [...RELOAD_PROPS] },
|
|
338
338
|
debounce: { search: 300 },
|
|
339
339
|
});
|
|
340
340
|
|
|
@@ -346,7 +346,7 @@ export function provideInvoicesWorkspace(): InvoicesWorkspaceContext {
|
|
|
346
346
|
});
|
|
347
347
|
// The search waits for typing to settle; every other filter visits at once.
|
|
348
348
|
useQueryObject(filters, {
|
|
349
|
-
only: [...RELOAD_PROPS],
|
|
349
|
+
visitOptions: { only: [...RELOAD_PROPS] },
|
|
350
350
|
debounce: { search: 300 },
|
|
351
351
|
});
|
|
352
352
|
|
|
@@ -361,7 +361,7 @@ export function provideUploadCenter(): UploadCenterContext {
|
|
|
361
361
|
});
|
|
362
362
|
// The search waits for typing to settle; every other filter visits at once.
|
|
363
363
|
useQueryObject(filters, {
|
|
364
|
-
only: [...RELOAD_PROPS],
|
|
364
|
+
visitOptions: { only: [...RELOAD_PROPS] },
|
|
365
365
|
debounce: { search: 300 },
|
|
366
366
|
});
|
|
367
367
|
|
|
@@ -988,8 +988,32 @@ const typeahead = useTypeahead<BaseOption<Item>>({
|
|
|
988
988
|
},
|
|
989
989
|
locale: () => config.locale,
|
|
990
990
|
});
|
|
991
|
+
/**
|
|
992
|
+
* A key typed while a keyboard open is still placing focus belongs to the
|
|
993
|
+
* search field it is about to reach: it waits for the open, then is written
|
|
994
|
+
* there as an input, so typing straight after Enter is not lost (MW-106).
|
|
995
|
+
*/
|
|
996
|
+
const typeIntoSearchAfterOpen = async (event: KeyboardEvent) => {
|
|
997
|
+
event.preventDefault();
|
|
998
|
+
await afterKeyboardOpen();
|
|
999
|
+
const input = searchInput.value;
|
|
1000
|
+
if (!(input instanceof HTMLInputElement)) return;
|
|
1001
|
+
if (document.activeElement !== input) return;
|
|
1002
|
+
const end = input.value.length;
|
|
1003
|
+
input.setRangeText(
|
|
1004
|
+
event.key,
|
|
1005
|
+
input.selectionStart ?? end,
|
|
1006
|
+
input.selectionEnd ?? end,
|
|
1007
|
+
'end'
|
|
1008
|
+
);
|
|
1009
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
1010
|
+
};
|
|
991
1011
|
const onTypeaheadKey = async (event: KeyboardEvent) => {
|
|
992
|
-
if (
|
|
1012
|
+
if (!isPrintableTypeaheadKey(event)) return;
|
|
1013
|
+
if (computedAllowWriting.value) {
|
|
1014
|
+
if (keyboardOpen) await typeIntoSearchAfterOpen(event);
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
993
1017
|
if (!canProcessKeyboardBindings.value) return;
|
|
994
1018
|
event.preventDefault();
|
|
995
1019
|
if (!shown.value) {
|