@svadmin/core 0.50.0 → 0.52.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/package.json +21 -4
- package/src/access-control-contract.ts +250 -0
- package/src/adapters/casbin.ts +20 -14
- package/src/adapters/casl.ts +22 -19
- package/src/admin-tool.ts +7 -7
- package/src/audit-contract.ts +105 -0
- package/src/audit.ts +88 -128
- package/src/auth-hooks.svelte.ts +593 -261
- package/src/auth-mutation-contract.ts +143 -0
- package/src/auth-query-contract.ts +97 -0
- package/src/captured-mutation.svelte.ts +45 -0
- package/src/chatProvider.svelte.ts +1 -1
- package/src/command-contract.ts +195 -0
- package/src/context.svelte.ts +217 -205
- package/src/contract-context.svelte.ts +37 -0
- package/src/contract-form.svelte.ts +566 -0
- package/src/create-contract.ts +56 -0
- package/src/create-hooks.svelte.ts +347 -0
- package/src/create-many-contract.ts +90 -0
- package/src/create-many-hooks.svelte.ts +396 -0
- package/src/data-transfer.svelte.ts +1 -215
- package/src/defined-options.ts +50 -0
- package/src/delete-contract.ts +50 -0
- package/src/delete-hooks.svelte.ts +403 -0
- package/src/delete-many-contract.ts +60 -0
- package/src/delete-many-hooks.svelte.ts +402 -0
- package/src/enterprise.ts +4 -4
- package/src/export-format.ts +16 -14
- package/src/export-hooks.svelte.ts +191 -0
- package/src/feature-gate-contract.ts +53 -0
- package/src/hook-utils.svelte.ts +53 -105
- package/src/hooks.svelte.ts +19 -776
- package/src/http.ts +88 -87
- package/src/i18n.svelte.ts +56 -2
- package/src/import-contract.ts +106 -0
- package/src/import-hooks.svelte.ts +310 -0
- package/src/index.ts +77 -30
- package/src/inferencer-graphql.ts +7 -6
- package/src/infinite-query.svelte.ts +228 -0
- package/src/invalidation-contract.ts +26 -0
- package/src/live-sse.ts +91 -127
- package/src/live-subscription.svelte.ts +130 -0
- package/src/live-transport.ts +127 -0
- package/src/live-websocket.ts +85 -118
- package/src/live.svelte.ts +66 -68
- package/src/notification.svelte.ts +17 -15
- package/src/permission-catalog.ts +121 -0
- package/src/permission-hints-contract.ts +48 -0
- package/src/permissions.svelte.ts +112 -59
- package/src/plain-data.ts +61 -0
- package/src/provider-bundle.ts +85 -79
- package/src/provider-response.ts +35 -0
- package/src/query-hooks.svelte.ts +241 -288
- package/src/query-invalidation.ts +172 -0
- package/src/query-keys.ts +221 -110
- package/src/query-session.svelte.ts +216 -0
- package/src/query-snapshot.ts +144 -0
- package/src/reactive-projection.ts +53 -0
- package/src/record-decoder.ts +63 -0
- package/src/resource-binding.svelte.ts +19 -0
- package/src/resource-contract.ts +595 -0
- package/src/resource-mutation-types.ts +30 -0
- package/src/resource-schemas.ts +371 -0
- package/src/resource-transport-config.ts +156 -0
- package/src/route-parsing.ts +11 -11
- package/src/router-provider.ts +64 -59
- package/src/router.ts +8 -3
- package/src/routing-hooks.svelte.ts +90 -62
- package/src/schema-validation.ts +92 -0
- package/src/schema.ts +13 -0
- package/src/select-options.ts +95 -0
- package/src/select-query.svelte.ts +191 -0
- package/src/session-query.svelte.ts +88 -0
- package/src/strict-command-hooks.svelte.ts +333 -0
- package/src/strict-hooks.svelte.ts +198 -0
- package/src/table-hooks.svelte.ts +180 -174
- package/src/table-state.svelte.ts +117 -117
- package/src/task-contract.ts +87 -0
- package/src/task-hooks.svelte.ts +193 -149
- package/src/task-provider.ts +115 -0
- package/src/task-subscription.ts +132 -0
- package/src/theme.svelte.ts +155 -154
- package/src/toast.svelte.ts +38 -37
- package/src/types.ts +55 -108
- package/src/unsafe.ts +7 -0
- package/src/update-contract.ts +65 -0
- package/src/update-hooks.svelte.ts +347 -0
- package/src/update-many-contract.ts +79 -0
- package/src/update-many-hooks.svelte.ts +373 -0
- package/src/url-sync.ts +11 -10
- package/src/useCan.ts +96 -31
- package/src/useParsed.svelte.ts +18 -14
- package/src/useStepsForm.svelte.ts +123 -63
- package/src/utility-hooks.svelte.ts +101 -158
- package/src/form-hooks.svelte.ts +0 -889
- package/src/mutation-hooks.svelte.ts +0 -561
- package/src/schema-form.ts +0 -31
|
@@ -1,77 +1,137 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import
|
|
1
|
+
import { Type } from '@sinclair/typebox';
|
|
2
|
+
import { useContractForm, type ContractFormOptions } from './contract-form.svelte';
|
|
3
|
+
import { getContractFormFields, type ContractSchemas, type ContractFormValues } from './resource-contract';
|
|
4
|
+
import { snapshotPlainData } from './plain-data';
|
|
5
|
+
import { checkExact } from './schema-validation';
|
|
6
|
+
import { replaceReactiveMembers } from './reactive-projection';
|
|
7
|
+
import { HttpError } from './types';
|
|
8
|
+
import { captureAdminContext } from './context.svelte';
|
|
9
|
+
import { captureAuthLiveScope } from './auth-hooks.svelte';
|
|
4
10
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
/** Default step index. Starts at 0. */
|
|
13
|
-
defaultStep?: number;
|
|
14
|
-
/** Validate the form when navigating back? */
|
|
15
|
-
isBackValidate?: boolean;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface UseStepsFormReturn<
|
|
19
|
-
TVariables extends Record<string, unknown> = Record<string, unknown>,
|
|
20
|
-
TData extends BaseRecord = BaseRecord,
|
|
21
|
-
> extends UseFormReturn<TVariables, TData> {
|
|
22
|
-
steps: {
|
|
23
|
-
currentStep: number;
|
|
24
|
-
totalSteps: number;
|
|
25
|
-
canGoNext: boolean;
|
|
26
|
-
canGoPrev: boolean;
|
|
27
|
-
gotoStep: (step: number) => void;
|
|
28
|
-
nextStep: () => void;
|
|
29
|
-
prevStep: () => void;
|
|
11
|
+
type Action = 'create' | 'edit';
|
|
12
|
+
type Field<S extends ContractSchemas, A extends Action> = Extract<keyof ContractFormValues<S, A>, string>;
|
|
13
|
+
export type UseStepsFormOptions<S extends ContractSchemas = ContractSchemas, A extends Action = Action> =
|
|
14
|
+
ContractFormOptions<S, A> & {
|
|
15
|
+
steps: readonly { readonly fields: readonly NoInfer<Field<S, A>>[] }[];
|
|
16
|
+
defaultStep?: number;
|
|
17
|
+
isBackValidate?: boolean;
|
|
30
18
|
};
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* useStepsForm
|
|
35
|
-
* Extends `useForm` with a nested `steps` object tracking navigation state.
|
|
36
|
-
*/
|
|
37
|
-
export function useStepsForm<
|
|
38
|
-
TVariables extends Record<string, unknown> = Record<string, unknown>,
|
|
39
|
-
TData extends BaseRecord = BaseRecord,
|
|
40
|
-
TError = HttpError,
|
|
41
|
-
>(options: UseStepsFormOptions<TVariables, TData, TError> = {} as UseStepsFormOptions<TVariables, TData, TError>): UseStepsFormReturn<TVariables, TData> {
|
|
42
|
-
const form = useForm<TVariables, TData, TError>(options);
|
|
43
19
|
|
|
44
|
-
|
|
45
|
-
|
|
20
|
+
const stepsSchema = Type.Array(Type.Object({
|
|
21
|
+
fields: Type.Array(Type.String(), { uniqueItems: true }),
|
|
22
|
+
}, { additionalProperties: false }), { minItems: 1 });
|
|
23
|
+
function invalid(): HttpError {
|
|
24
|
+
return new HttpError('Invalid form steps', 422, undefined, { code: 'INVALID_RESOURCE_INPUT' });
|
|
25
|
+
}
|
|
46
26
|
|
|
47
|
-
|
|
48
|
-
|
|
27
|
+
/** Step navigation shares the form's validation, request lifecycle and checked receipts. */
|
|
28
|
+
export function useStepsForm<S extends ContractSchemas, A extends Action>(options: UseStepsFormOptions<S, A>) {
|
|
29
|
+
const context = captureAdminContext();
|
|
30
|
+
const layout = $derived.by(() => {
|
|
31
|
+
try {
|
|
32
|
+
const steps = snapshotPlainData(options.steps);
|
|
33
|
+
if (!checkExact(stepsSchema, steps)) throw invalid();
|
|
34
|
+
const names: readonly string[] = getContractFormFields(options.resource, options.action);
|
|
35
|
+
if (steps.some(step => step.fields.some(field => !names.includes(field)))) throw invalid();
|
|
36
|
+
const defaultStep = options.defaultStep ?? 0;
|
|
37
|
+
if (!Number.isInteger(defaultStep) || defaultStep < 0 || defaultStep >= steps.length ||
|
|
38
|
+
(options.isBackValidate !== undefined && typeof options.isBackValidate !== 'boolean')) throw invalid();
|
|
39
|
+
return { ok: true as const, steps, defaultStep, key: JSON.stringify([steps, defaultStep]) };
|
|
40
|
+
} catch {
|
|
41
|
+
return { ok: false as const, error: invalid() };
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
let currentStep = $state(0);
|
|
45
|
+
let epoch = 0;
|
|
46
|
+
let navigation: object | undefined;
|
|
47
|
+
let submission: object | undefined;
|
|
48
|
+
let mounted = true;
|
|
49
|
+
function resetNavigation() {
|
|
50
|
+
currentStep = layout.ok ? layout.defaultStep : 0;
|
|
51
|
+
epoch++;
|
|
52
|
+
navigation = undefined;
|
|
53
|
+
submission = undefined;
|
|
49
54
|
}
|
|
55
|
+
const form = useContractForm<S, A>(options, {
|
|
56
|
+
get enabled() { return layout.ok; },
|
|
57
|
+
get key() { return layout.ok ? layout.key : 'invalid'; },
|
|
58
|
+
reset: resetNavigation,
|
|
59
|
+
});
|
|
60
|
+
$effect(() => () => { mounted = false; resetNavigation(); });
|
|
50
61
|
|
|
51
|
-
function
|
|
52
|
-
|
|
62
|
+
function capture() {
|
|
63
|
+
const revision = epoch;
|
|
64
|
+
const auth = context.authProvider;
|
|
65
|
+
const session = captureAuthLiveScope(auth);
|
|
66
|
+
const router = context.routerProvider;
|
|
67
|
+
const key = layout.ok ? layout.key : undefined;
|
|
68
|
+
return () => mounted && revision === epoch && session.isCurrent() && context.authProvider === auth &&
|
|
69
|
+
context.routerProvider === router && layout.ok && layout.key === key && form.ready;
|
|
53
70
|
}
|
|
54
71
|
|
|
55
|
-
function
|
|
56
|
-
if (
|
|
72
|
+
function focusErrorStep(current: () => boolean): void {
|
|
73
|
+
if (!current() || !layout.ok) return;
|
|
74
|
+
const first = layout.steps.findIndex(step => step.fields.some(field => Boolean(form.errors[field])));
|
|
75
|
+
if (first >= 0 && current()) currentStep = first;
|
|
76
|
+
}
|
|
77
|
+
function validateTo(step: number, current: () => boolean): boolean {
|
|
78
|
+
if (!current() || !layout.ok) return false;
|
|
79
|
+
const fields = step > currentStep
|
|
80
|
+
? layout.steps.slice(0, step).flatMap(item => item.fields)
|
|
81
|
+
: layout.steps[currentStep]?.fields ?? [];
|
|
82
|
+
const selected = getContractFormFields(options.resource, options.action).filter(field => fields.includes(field));
|
|
83
|
+
const valid = form.validateFields(selected);
|
|
84
|
+
if (!current()) return false;
|
|
85
|
+
if (!valid) focusErrorStep(current);
|
|
86
|
+
return valid;
|
|
87
|
+
}
|
|
88
|
+
function gotoStep(step: number): boolean {
|
|
89
|
+
if (!layout.ok || !form.ready || form.submitting) return false;
|
|
90
|
+
if (!Number.isInteger(step) || step < 0 || step >= layout.steps.length) throw invalid();
|
|
91
|
+
const token = {};
|
|
92
|
+
navigation = token;
|
|
93
|
+
const active = capture();
|
|
94
|
+
const current = () => navigation === token && active();
|
|
95
|
+
if (step === currentStep) return true;
|
|
96
|
+
if ((step > currentStep || options.isBackValidate === true) && !validateTo(step, current)) return false;
|
|
97
|
+
if (!current()) return false;
|
|
98
|
+
currentStep = step;
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
function submit(overrides?: Parameters<typeof form.submit>[0]) {
|
|
102
|
+
const current = capture();
|
|
103
|
+
const navigationAtSubmit = navigation;
|
|
104
|
+
const token = {};
|
|
105
|
+
submission = token;
|
|
106
|
+
const source = form.submit(overrides);
|
|
107
|
+
// Observe errors without adding another result handoff after the base form's final check.
|
|
108
|
+
void source.catch(() => {
|
|
109
|
+
try {
|
|
110
|
+
if (submission === token && navigation === navigationAtSubmit && current() && !form.submitting) {
|
|
111
|
+
focusErrorStep(() => submission === token && navigation === navigationAtSubmit && current());
|
|
112
|
+
}
|
|
113
|
+
} catch { /* Workflow presentation cannot replace a checked submission result. */ }
|
|
114
|
+
});
|
|
115
|
+
return source;
|
|
116
|
+
}
|
|
117
|
+
function reset() {
|
|
118
|
+
form.reset();
|
|
119
|
+
resetNavigation();
|
|
57
120
|
}
|
|
58
|
-
|
|
59
|
-
const result = form as unknown as UseStepsFormReturn<TVariables, TData>;
|
|
60
|
-
|
|
61
121
|
const steps = {
|
|
62
|
-
get currentStep() { return currentStep; },
|
|
63
|
-
get totalSteps() { return
|
|
64
|
-
get canGoNext() { return currentStep <
|
|
65
|
-
get canGoPrev() { return currentStep > 0; },
|
|
122
|
+
get currentStep() { return mounted && form.ready ? currentStep : 0; },
|
|
123
|
+
get totalSteps() { return layout.ok ? layout.steps.length : 0; },
|
|
124
|
+
get canGoNext() { return form.ready && !form.submitting && layout.ok && currentStep < layout.steps.length - 1; },
|
|
125
|
+
get canGoPrev() { return form.ready && !form.submitting && currentStep > 0; },
|
|
66
126
|
gotoStep,
|
|
67
|
-
nextStep,
|
|
68
|
-
prevStep,
|
|
127
|
+
nextStep() { return layout.ok && currentStep < layout.steps.length - 1 ? gotoStep(currentStep + 1) : false; },
|
|
128
|
+
prevStep() { return currentStep > 0 ? gotoStep(currentStep - 1) : false; },
|
|
69
129
|
};
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
get: ()
|
|
73
|
-
enumerable: true
|
|
130
|
+
return replaceReactiveMembers(form, {
|
|
131
|
+
steps, submit, reset,
|
|
132
|
+
get error() { return layout.ok ? form.error : invalid(); },
|
|
74
133
|
});
|
|
75
|
-
|
|
76
|
-
return result;
|
|
77
134
|
}
|
|
135
|
+
|
|
136
|
+
export type UseStepsFormReturn<S extends ContractSchemas = ContractSchemas, A extends Action = Action> =
|
|
137
|
+
ReturnType<typeof useStepsForm<S, A>>;
|
|
@@ -1,83 +1,24 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { useQueryClient } from '@tanstack/svelte-query';
|
|
1
|
+
import { definedOptions } from './defined-options';
|
|
3
2
|
import { useParsed } from './useParsed.svelte';
|
|
4
3
|
import { captureAdminContext } from './context.svelte';
|
|
5
|
-
import { dataQueryMatches } from './query-keys';
|
|
6
4
|
import { getAdminOptions } from './options.svelte';
|
|
7
5
|
import { t } from './i18n.svelte';
|
|
8
|
-
import type { BaseRecord,
|
|
6
|
+
import type { BaseRecord,HttpError,Filter,KnownResources,ResourceDefinition } from './types';
|
|
9
7
|
import { useList } from './query-hooks.svelte';
|
|
10
8
|
import { toast } from './toast.svelte';
|
|
11
|
-
import type
|
|
12
|
-
import {
|
|
9
|
+
import { createSelectQuery as useSelectImpl, type UseSelectOptions } from './select-query.svelte';
|
|
10
|
+
import { decodeBaseRecord } from './record-decoder';
|
|
13
11
|
|
|
14
12
|
// ─── useModal ─────────────────────────────────────────────────
|
|
15
13
|
export function useModal(options?: { defaultVisible?: boolean }) {
|
|
16
|
-
let visible
|
|
14
|
+
let visible=$state(options?.defaultVisible??false);
|
|
17
15
|
return {
|
|
18
16
|
get visible() { return visible; },
|
|
19
|
-
show() { visible
|
|
20
|
-
close() { visible
|
|
17
|
+
show() { visible=true; },
|
|
18
|
+
close() { visible=false; },
|
|
21
19
|
};
|
|
22
20
|
}
|
|
23
21
|
|
|
24
|
-
// ─── useModalForm ─────────────────────────────────────────────
|
|
25
|
-
|
|
26
|
-
export interface UseModalFormOptions<
|
|
27
|
-
TVariables extends Record<string, unknown> = Record<string, unknown>,
|
|
28
|
-
TData extends BaseRecord = BaseRecord,
|
|
29
|
-
TError = HttpError,
|
|
30
|
-
> extends UseFormOptions<TVariables, TData, TError> {
|
|
31
|
-
defaultVisible?: boolean;
|
|
32
|
-
autoSave?: UseFormOptions<TVariables, TData, TError>['autoSave'] & {
|
|
33
|
-
invalidateOnClose?: boolean;
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function useModalForm<
|
|
38
|
-
TVariables extends Record<string, unknown> = Record<string, unknown>,
|
|
39
|
-
TData extends BaseRecord = BaseRecord,
|
|
40
|
-
TError = HttpError,
|
|
41
|
-
>(options: UseModalFormOptions<TVariables, TData, TError> = {} as UseModalFormOptions<TVariables, TData, TError>) {
|
|
42
|
-
const queryClient = useQueryClient();
|
|
43
|
-
const adminContext = captureAdminContext();
|
|
44
|
-
const formState = useForm<TVariables, TData, TError>(options);
|
|
45
|
-
let visible = $state(options.defaultVisible ?? false);
|
|
46
|
-
|
|
47
|
-
function show(id?: string | number) {
|
|
48
|
-
if (id !== undefined) {
|
|
49
|
-
formState.setAction('edit');
|
|
50
|
-
formState.setId(id);
|
|
51
|
-
} else {
|
|
52
|
-
formState.setAction('create');
|
|
53
|
-
formState.setId(undefined);
|
|
54
|
-
}
|
|
55
|
-
visible = true;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function close() {
|
|
59
|
-
visible = false;
|
|
60
|
-
formState.reset();
|
|
61
|
-
if (options.autoSave?.invalidateOnClose) {
|
|
62
|
-
const resource = formState.resource;
|
|
63
|
-
const matcher = adminContext.queryKeyMatcher(resource, options.dataProviderName);
|
|
64
|
-
queryClient.invalidateQueries({
|
|
65
|
-
predicate: (q) => dataQueryMatches(q.queryKey, { ...matcher, resource }),
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return {
|
|
71
|
-
get visible() { return visible; },
|
|
72
|
-
show,
|
|
73
|
-
close,
|
|
74
|
-
get modalProps() { return { visible, close }; },
|
|
75
|
-
formState,
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export const useDrawerForm = useModalForm; // Alias for UI variations
|
|
80
|
-
|
|
81
22
|
// ─── useMenu ──────────────────────────────────────────────────
|
|
82
23
|
|
|
83
24
|
export interface MenuConfig {
|
|
@@ -90,31 +31,33 @@ export interface MenuConfig {
|
|
|
90
31
|
}
|
|
91
32
|
|
|
92
33
|
export function useMenu() {
|
|
93
|
-
const adminContext
|
|
94
|
-
const parsed
|
|
95
|
-
|
|
96
|
-
const menuItems
|
|
97
|
-
const adminOptions
|
|
98
|
-
const resources
|
|
99
|
-
let items: MenuConfig[]
|
|
100
|
-
.filter((r: ResourceDefinition) => r.showInMenu
|
|
34
|
+
const adminContext=captureAdminContext();
|
|
35
|
+
const parsed=useParsed();
|
|
36
|
+
|
|
37
|
+
const menuItems=$derived.by<MenuConfig[]>(() => {
|
|
38
|
+
const adminOptions=getAdminOptions();
|
|
39
|
+
const resources=adminContext.resources;
|
|
40
|
+
let items: MenuConfig[]=resources
|
|
41
|
+
.filter((r: ResourceDefinition) => r.showInMenu!==false)
|
|
101
42
|
.map((r: ResourceDefinition): MenuConfig => ({
|
|
102
43
|
name: r.name,
|
|
103
44
|
route: `/${r.name}`,
|
|
104
|
-
label: r.label
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
45
|
+
label: r.label||r.name, // Usually handled by i18n label mapping
|
|
46
|
+
...definedOptions({
|
|
47
|
+
icon: r.icon,
|
|
48
|
+
parentName: r.parentName,
|
|
49
|
+
}),
|
|
50
|
+
order: r.menuOrder??999,
|
|
108
51
|
}));
|
|
109
52
|
|
|
110
|
-
if
|
|
111
|
-
items.sort((a,
|
|
53
|
+
if(adminOptions.menuItems) items=adminOptions.menuItems(items);
|
|
54
|
+
items.sort((a,b) => (a.order??999)-(b.order??999));
|
|
112
55
|
return items;
|
|
113
56
|
});
|
|
114
57
|
|
|
115
58
|
return {
|
|
116
59
|
get menuItems() { return menuItems; },
|
|
117
|
-
get selectedKey() { return parsed.resource
|
|
60
|
+
get selectedKey() { return parsed.resource??'/'; },
|
|
118
61
|
};
|
|
119
62
|
}
|
|
120
63
|
|
|
@@ -127,32 +70,32 @@ export interface BreadcrumbItem {
|
|
|
127
70
|
}
|
|
128
71
|
|
|
129
72
|
export function useBreadcrumb() {
|
|
130
|
-
const adminContext
|
|
131
|
-
const parsed
|
|
132
|
-
|
|
133
|
-
const items
|
|
134
|
-
const list: BreadcrumbItem[]
|
|
135
|
-
if
|
|
73
|
+
const adminContext=captureAdminContext();
|
|
74
|
+
const parsed=useParsed();
|
|
75
|
+
|
|
76
|
+
const items=$derived.by<BreadcrumbItem[]>(() => {
|
|
77
|
+
const list: BreadcrumbItem[]=[];
|
|
78
|
+
if(!parsed.resource) return list;
|
|
136
79
|
|
|
137
80
|
try {
|
|
138
|
-
const res
|
|
139
|
-
const chain: { label: string; link: string; icon?: string }[]
|
|
140
|
-
let current: typeof res
|
|
141
|
-
const visited
|
|
142
|
-
while
|
|
143
|
-
if
|
|
81
|
+
const res=adminContext.getResource(parsed.resource);
|
|
82
|
+
const chain: { label: string; link: string; icon?: string }[]=[];
|
|
83
|
+
let current: typeof res|undefined=res;
|
|
84
|
+
const visited=new Set<string>();
|
|
85
|
+
while(current) {
|
|
86
|
+
if(visited.has(current.name)) break;
|
|
144
87
|
visited.add(current.name);
|
|
145
|
-
chain.unshift({ label: current.label
|
|
146
|
-
current
|
|
88
|
+
chain.unshift({ label: current.label||current.name,link: `/${current.name}`,...definedOptions({ icon: current.icon }) });
|
|
89
|
+
current=current.parentName? adminContext.getResource(current.parentName):undefined;
|
|
147
90
|
}
|
|
148
91
|
list.push(...chain);
|
|
149
92
|
|
|
150
|
-
if
|
|
93
|
+
if(parsed.action==='create') {
|
|
151
94
|
list.push({ label: t('common.create') });
|
|
152
|
-
} else if
|
|
95
|
+
} else if(parsed.action==='edit'&&parsed.id) {
|
|
153
96
|
list.push({ label: t('common.edit') });
|
|
154
97
|
list.push({ label: String(parsed.id) });
|
|
155
|
-
} else if
|
|
98
|
+
} else if(parsed.action==='show'&&parsed.id) {
|
|
156
99
|
list.push({ label: t('common.show') });
|
|
157
100
|
list.push({ label: String(parsed.id) });
|
|
158
101
|
}
|
|
@@ -167,48 +110,48 @@ export function useBreadcrumb() {
|
|
|
167
110
|
|
|
168
111
|
// ─── useRelation ───────────────────────────────────────────
|
|
169
112
|
|
|
170
|
-
export interface UseRelationOptions<_TData extends BaseRecord
|
|
113
|
+
export interface UseRelationOptions<_TData extends BaseRecord=BaseRecord,_TError=HttpError> {
|
|
171
114
|
resource: KnownResources;
|
|
172
|
-
id?: string
|
|
115
|
+
id?: string|number;
|
|
173
116
|
pagination?: { current?: number; pageSize?: number };
|
|
174
|
-
sorters?: { field: string; order: 'asc'
|
|
117
|
+
sorters?: { field: string; order: 'asc'|'desc' }[];
|
|
175
118
|
filters?: Filter[];
|
|
176
119
|
queryOptions?: { enabled?: boolean; staleTime?: number };
|
|
177
120
|
}
|
|
178
121
|
|
|
179
|
-
export function useRelation
|
|
180
|
-
return useList
|
|
122
|
+
export function useRelation(options: UseRelationOptions) {
|
|
123
|
+
return useList({
|
|
181
124
|
...options
|
|
182
125
|
});
|
|
183
126
|
}
|
|
184
127
|
|
|
185
128
|
// ─── useNotification ────────────────────────────────────────
|
|
186
129
|
export function useNotification() {
|
|
187
|
-
const adminContext
|
|
130
|
+
const adminContext=captureAdminContext();
|
|
188
131
|
|
|
189
132
|
function sendToast(
|
|
190
|
-
type: 'success'
|
|
133
|
+
type: 'success'|'error'|'warning'|'info',
|
|
191
134
|
message: string,
|
|
192
135
|
duration?: number,
|
|
193
136
|
options?: { key?: string },
|
|
194
137
|
): void {
|
|
195
|
-
const provider
|
|
196
|
-
if
|
|
197
|
-
provider.open({ type,
|
|
138
|
+
const provider=adminContext.notificationProvider;
|
|
139
|
+
if(provider) {
|
|
140
|
+
provider.open({ type,message,...definedOptions({ key: options?.key }) });
|
|
198
141
|
return;
|
|
199
142
|
}
|
|
200
|
-
toast[type](message,
|
|
143
|
+
toast[type](message,duration,options);
|
|
201
144
|
}
|
|
202
145
|
|
|
203
146
|
return {
|
|
204
|
-
open: (message: string,
|
|
205
|
-
sendToast('info',
|
|
206
|
-
success: (message: string,
|
|
207
|
-
sendToast('success',
|
|
208
|
-
error: (message: string,
|
|
209
|
-
sendToast('error',
|
|
210
|
-
warning: (message: string,
|
|
211
|
-
sendToast('warning',
|
|
147
|
+
open: (message: string,duration?: number,options?: { key?: string }) =>
|
|
148
|
+
sendToast('info',message,duration,options),
|
|
149
|
+
success: (message: string,duration?: number,options?: { key?: string }) =>
|
|
150
|
+
sendToast('success',message,duration,options),
|
|
151
|
+
error: (message: string,duration?: number,options?: { key?: string }) =>
|
|
152
|
+
sendToast('error',message,duration,options),
|
|
153
|
+
warning: (message: string,duration?: number,options?: { key?: string }) =>
|
|
154
|
+
sendToast('warning',message,duration,options),
|
|
212
155
|
close: (key: string) => adminContext.notificationProvider?.close(key),
|
|
213
156
|
};
|
|
214
157
|
}
|
|
@@ -217,34 +160,34 @@ export function useNotification() {
|
|
|
217
160
|
// useCheckboxGroup / useRadioGroup / useAutocomplete are convenience
|
|
218
161
|
// wrappers around useSelect for common form field patterns.
|
|
219
162
|
|
|
220
|
-
export interface UseCheckboxGroupOptions<TData extends BaseRecord
|
|
163
|
+
export interface UseCheckboxGroupOptions<TData extends BaseRecord=BaseRecord> extends Omit<UseSelectOptions<TData>,'optionLabel'|'optionValue'|'defaultValue'> {
|
|
221
164
|
/** Field on the record used as the option label (default: 'title') */
|
|
222
|
-
optionLabel?: string
|
|
165
|
+
optionLabel?: string|((item: TData) => string);
|
|
223
166
|
/** Field on the record used as the checkbox value (default: 'id') */
|
|
224
|
-
optionValue?: string
|
|
167
|
+
optionValue?: string|((item: TData) => string|number);
|
|
225
168
|
/** Currently selected values — controlled mode */
|
|
226
|
-
defaultValue?: (string
|
|
169
|
+
defaultValue?: (string|number)[];
|
|
227
170
|
}
|
|
228
171
|
|
|
229
172
|
/**
|
|
230
173
|
* useCheckboxGroup — multi-select field state backed by a resource query.
|
|
231
174
|
* Provides a list of checkbox options plus helpers to toggle individual values.
|
|
232
175
|
*/
|
|
233
|
-
export function useCheckboxGroup
|
|
234
|
-
const select
|
|
235
|
-
let selected
|
|
236
|
-
|
|
237
|
-
function toggle(value: string
|
|
238
|
-
const idx
|
|
239
|
-
if
|
|
240
|
-
selected
|
|
176
|
+
export function useCheckboxGroup(options: UseCheckboxGroupOptions) {
|
|
177
|
+
const select=useSelectImpl(options, () => decodeBaseRecord);
|
|
178
|
+
let selected=$state<(string|number)[]>(options.defaultValue??[]);
|
|
179
|
+
|
|
180
|
+
function toggle(value: string|number) {
|
|
181
|
+
const idx=selected.findIndex(v => String(v)===String(value));
|
|
182
|
+
if(idx>=0) {
|
|
183
|
+
selected=selected.filter(v => String(v)!==String(value));
|
|
241
184
|
} else {
|
|
242
|
-
selected
|
|
185
|
+
selected=[...selected,value];
|
|
243
186
|
}
|
|
244
187
|
}
|
|
245
188
|
|
|
246
|
-
function isSelected(value: string
|
|
247
|
-
return selected.some(v => String(v)
|
|
189
|
+
function isSelected(value: string|number): boolean {
|
|
190
|
+
return selected.some(v => String(v)===String(value));
|
|
248
191
|
}
|
|
249
192
|
|
|
250
193
|
return {
|
|
@@ -254,70 +197,70 @@ export function useCheckboxGroup<TData extends BaseRecord = BaseRecord>(options:
|
|
|
254
197
|
get selected() { return selected; },
|
|
255
198
|
toggle,
|
|
256
199
|
isSelected,
|
|
257
|
-
setSelected: (values: (string
|
|
200
|
+
setSelected: (values: (string|number)[]) => { selected=values; },
|
|
258
201
|
};
|
|
259
202
|
}
|
|
260
203
|
|
|
261
|
-
export interface UseRadioGroupOptions<TData extends BaseRecord
|
|
262
|
-
optionLabel?: string
|
|
263
|
-
optionValue?: string
|
|
264
|
-
defaultValue?: string
|
|
204
|
+
export interface UseRadioGroupOptions<TData extends BaseRecord=BaseRecord> extends Omit<UseSelectOptions<TData>,'optionLabel'|'optionValue'|'defaultValue'> {
|
|
205
|
+
optionLabel?: string|((item: TData) => string);
|
|
206
|
+
optionValue?: string|((item: TData) => string|number);
|
|
207
|
+
defaultValue?: string|number;
|
|
265
208
|
}
|
|
266
209
|
|
|
267
210
|
/**
|
|
268
211
|
* useRadioGroup — single-select field state backed by a resource query.
|
|
269
212
|
* Provides a list of radio options plus a setter for the currently selected value.
|
|
270
213
|
*/
|
|
271
|
-
export function useRadioGroup
|
|
272
|
-
const { defaultValue: _dv
|
|
273
|
-
// useSelect
|
|
274
|
-
const select
|
|
214
|
+
export function useRadioGroup(options: UseRadioGroupOptions) {
|
|
215
|
+
const { defaultValue: _dv,...selectOpts }=options;
|
|
216
|
+
// useSelect defaultValue is an array; pass the scalar value through so remote options can restore the selection.
|
|
217
|
+
const select=useSelectImpl({
|
|
275
218
|
...selectOpts,
|
|
276
|
-
defaultValue: _dv
|
|
277
|
-
});
|
|
278
|
-
let value
|
|
219
|
+
defaultValue: _dv!==undefined? [_dv]:[],
|
|
220
|
+
}, () => decodeBaseRecord);
|
|
221
|
+
let value=$state<string|number|undefined>(_dv);
|
|
279
222
|
|
|
280
223
|
return {
|
|
281
224
|
query: select.query,
|
|
282
225
|
get options() { return select.options; },
|
|
283
226
|
onSearchChange: select.onSearchChange,
|
|
284
227
|
get value() { return value; },
|
|
285
|
-
setValue: (v: string
|
|
228
|
+
setValue: (v: string|number) => { value=v; },
|
|
286
229
|
};
|
|
287
230
|
}
|
|
288
231
|
|
|
289
|
-
export interface UseAutocompleteOptions<TData extends BaseRecord
|
|
290
|
-
optionLabel?: string
|
|
291
|
-
optionValue?: string
|
|
232
|
+
export interface UseAutocompleteOptions<TData extends BaseRecord=BaseRecord> extends Omit<UseSelectOptions<TData>,'onSearch'|'defaultValue'> {
|
|
233
|
+
optionLabel?: string|((item: TData) => string);
|
|
234
|
+
optionValue?: string|((item: TData) => string|number);
|
|
292
235
|
/** Field name to search against (default: optionLabel) */
|
|
293
236
|
searchField?: string;
|
|
294
|
-
defaultValue?: string
|
|
237
|
+
defaultValue?: string|number;
|
|
295
238
|
}
|
|
296
239
|
|
|
297
240
|
/**
|
|
298
241
|
* useAutocomplete — typeahead search field backed by a resource query.
|
|
299
242
|
* Builds a 'contains' filter on the search field and debounces input.
|
|
300
243
|
*/
|
|
301
|
-
export function useAutocomplete
|
|
302
|
-
const { searchField,
|
|
303
|
-
const labelField
|
|
244
|
+
export function useAutocomplete(options: UseAutocompleteOptions) {
|
|
245
|
+
const { searchField,defaultValue: _dv,...selectOpts }=options;
|
|
246
|
+
const labelField=typeof options.optionLabel==='string'? options.optionLabel:'title';
|
|
304
247
|
|
|
305
|
-
const select
|
|
248
|
+
const select=useSelectImpl({
|
|
306
249
|
...selectOpts,
|
|
307
|
-
defaultValue: _dv
|
|
250
|
+
defaultValue: _dv!==undefined? [_dv]:[],
|
|
308
251
|
onSearch: (term) => [{
|
|
309
|
-
field: searchField
|
|
252
|
+
field: searchField??labelField,
|
|
310
253
|
operator: 'contains' as const,
|
|
311
254
|
value: term,
|
|
312
255
|
}],
|
|
313
|
-
});
|
|
314
|
-
let value
|
|
256
|
+
}, () => decodeBaseRecord);
|
|
257
|
+
let value=$state<string|number|undefined>(_dv);
|
|
315
258
|
|
|
316
259
|
return {
|
|
317
260
|
query: select.query,
|
|
318
261
|
get options() { return select.options; },
|
|
319
262
|
onSearchChange: select.onSearchChange,
|
|
320
263
|
get value() { return value; },
|
|
321
|
-
setValue: (v: string
|
|
264
|
+
setValue: (v: string|number) => { value=v; },
|
|
322
265
|
};
|
|
323
266
|
}
|