@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
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { captureAdminContext } from './context.svelte';
|
|
2
|
+
import { definedOptions } from './defined-options';
|
|
3
|
+
import { HttpError } from './types';
|
|
4
|
+
import { contractKey, parseContractRecord, type ResourceContract, type ContractSchemas, type ContractRecord } from './resource-contract';
|
|
5
|
+
import type { ContractFilter, ContractSort } from './strict-hooks.svelte';
|
|
6
|
+
import { captureQueryProvider, snapshotListParams } from './query-snapshot';
|
|
7
|
+
import { decodeBaseRecord, decodeListResult } from './record-decoder';
|
|
8
|
+
import { snapshotPlainData } from './plain-data';
|
|
9
|
+
import { downloadData, type ExportFormat } from './export-format';
|
|
10
|
+
|
|
11
|
+
export interface UseExportOptions<S extends ContractSchemas> {
|
|
12
|
+
resource: ResourceContract<S>;
|
|
13
|
+
mapData?: (item: NoInfer<ContractRecord<S>>) => Record<string, unknown>;
|
|
14
|
+
sorters?: ContractSort<NoInfer<ContractRecord<S>>>[];
|
|
15
|
+
filters?: ContractFilter<NoInfer<ContractRecord<S>>>[];
|
|
16
|
+
maxItemCount?: number;
|
|
17
|
+
pageSize?: number;
|
|
18
|
+
download?: boolean;
|
|
19
|
+
format?: ExportFormat;
|
|
20
|
+
meta?: Record<string, unknown>;
|
|
21
|
+
dataProviderName?: string;
|
|
22
|
+
onError?: (error: HttpError) => void | Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function cancelled(): HttpError {
|
|
26
|
+
return new HttpError('Export scope changed', 409, undefined, { code: 'EXPORT_CANCELLED' });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function invalidInput(): never {
|
|
30
|
+
throw new HttpError('Invalid export input', 422, undefined, { code: 'INVALID_RESOURCE_INPUT' });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function exportFailure(cause: unknown): HttpError {
|
|
34
|
+
let code: unknown;
|
|
35
|
+
try {
|
|
36
|
+
if (typeof cause === 'object' && cause !== null) code = Object.getOwnPropertyDescriptor(cause, 'code')?.value;
|
|
37
|
+
} catch {
|
|
38
|
+
// Revoked proxies and accessor-backed errors are not trusted diagnostic sources.
|
|
39
|
+
}
|
|
40
|
+
if (code === 'INVALID_RESOURCE_INPUT') {
|
|
41
|
+
return new HttpError('Invalid export input', 422, undefined, { code: 'INVALID_RESOURCE_INPUT' });
|
|
42
|
+
}
|
|
43
|
+
if (code === 'INVALID_PROVIDER_RESPONSE') {
|
|
44
|
+
return new HttpError('Invalid provider response', 502, undefined, { code: 'INVALID_PROVIDER_RESPONSE' });
|
|
45
|
+
}
|
|
46
|
+
return new HttpError('Export failed', 500, undefined, { code: 'EXPORT_FAILED' });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Export only records proven by the bound business schema, within one captured scope. */
|
|
50
|
+
export function useExport<S extends ContractSchemas>(options: UseExportOptions<S>) {
|
|
51
|
+
const context = captureAdminContext();
|
|
52
|
+
contractKey(options.resource);
|
|
53
|
+
let isLoading = $state(false);
|
|
54
|
+
let error = $state<HttpError | null>(null);
|
|
55
|
+
let mounted = true;
|
|
56
|
+
|
|
57
|
+
function captureScope() {
|
|
58
|
+
const contract = options.resource;
|
|
59
|
+
const key = contractKey(contract);
|
|
60
|
+
const captured = captureQueryProvider(context, {
|
|
61
|
+
resource: contract.name, contract,
|
|
62
|
+
...definedOptions({ dataProviderName: options.dataProviderName, meta: options.meta }),
|
|
63
|
+
});
|
|
64
|
+
return {
|
|
65
|
+
contract, ...captured,
|
|
66
|
+
key: JSON.stringify([key, captured.source, context.tenantCacheKey?.__svadminTenant, captured.meta]),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
type Scope = ReturnType<typeof captureScope>;
|
|
70
|
+
let current: { token: object; scope: Scope; promise: Promise<ContractRecord<S>[]> } | undefined;
|
|
71
|
+
|
|
72
|
+
function isActive(token: object, scope: Scope): boolean {
|
|
73
|
+
if (!mounted || current?.token !== token) return false;
|
|
74
|
+
try { return captureScope().key === scope.key; }
|
|
75
|
+
catch { return false; }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let initialized = false;
|
|
79
|
+
let previousScope: string | undefined;
|
|
80
|
+
$effect(() => {
|
|
81
|
+
let nextScope: string | undefined;
|
|
82
|
+
try { nextScope = captureScope().key; }
|
|
83
|
+
catch { nextScope = undefined; }
|
|
84
|
+
if (initialized && previousScope !== nextScope) {
|
|
85
|
+
current = undefined;
|
|
86
|
+
isLoading = false;
|
|
87
|
+
error = null;
|
|
88
|
+
}
|
|
89
|
+
previousScope = nextScope;
|
|
90
|
+
initialized = true;
|
|
91
|
+
});
|
|
92
|
+
$effect(() => () => {
|
|
93
|
+
mounted = false;
|
|
94
|
+
current = undefined;
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
function report(failure: HttpError, handler: UseExportOptions<S>['onError']): void {
|
|
98
|
+
error = failure;
|
|
99
|
+
try {
|
|
100
|
+
void Promise.resolve(handler?.(failure)).catch(() => {
|
|
101
|
+
// Async notification failures must not become unhandled rejections.
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// Consumer callback failures must not replace the sanitized export failure.
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function triggerExport(): Promise<ContractRecord<S>[]> {
|
|
110
|
+
if (!mounted) return Promise.reject(cancelled());
|
|
111
|
+
let onError: UseExportOptions<S>['onError'];
|
|
112
|
+
try {
|
|
113
|
+
const scope = captureScope();
|
|
114
|
+
if (current?.scope.key === scope.key) return current.promise;
|
|
115
|
+
previousScope = scope.key;
|
|
116
|
+
initialized = true;
|
|
117
|
+
const batchSize = options.pageSize ?? 20;
|
|
118
|
+
const maxItems = options.maxItemCount ?? Infinity;
|
|
119
|
+
const format = options.format ?? 'csv';
|
|
120
|
+
const download = options.download ?? true;
|
|
121
|
+
const mapData = options.mapData;
|
|
122
|
+
onError = options.onError;
|
|
123
|
+
if (!Number.isSafeInteger(batchSize) || batchSize <= 0 ||
|
|
124
|
+
(maxItems !== Infinity && (!Number.isSafeInteger(maxItems) || maxItems < 0)) ||
|
|
125
|
+
!['csv', 'json', 'xlsx'].includes(format) || typeof download !== 'boolean' ||
|
|
126
|
+
(mapData !== undefined && typeof mapData !== 'function') ||
|
|
127
|
+
(onError !== undefined && typeof onError !== 'function')) invalidInput();
|
|
128
|
+
const handler = onError;
|
|
129
|
+
const params = snapshotListParams({
|
|
130
|
+
resource: scope.contract.name,
|
|
131
|
+
...definedOptions({
|
|
132
|
+
pagination: { current: 1, pageSize: batchSize },
|
|
133
|
+
filters: options.filters, sorters: options.sorters, meta: scope.meta,
|
|
134
|
+
}),
|
|
135
|
+
});
|
|
136
|
+
const token = {};
|
|
137
|
+
const ensureActive = () => { if (!isActive(token, scope)) throw cancelled(); };
|
|
138
|
+
isLoading = true;
|
|
139
|
+
error = null;
|
|
140
|
+
// Install the invocation before transport code or user callbacks can re-enter it.
|
|
141
|
+
const promise = Promise.resolve().then(async () => {
|
|
142
|
+
try {
|
|
143
|
+
ensureActive();
|
|
144
|
+
const records: ContractRecord<S>[] = [];
|
|
145
|
+
let page = 1;
|
|
146
|
+
while (records.length < maxItems) {
|
|
147
|
+
const response = await scope.provider.getList(snapshotListParams({
|
|
148
|
+
...params, pagination: { current: page, pageSize: batchSize },
|
|
149
|
+
}));
|
|
150
|
+
ensureActive();
|
|
151
|
+
const result = decodeListResult(response, value => parseContractRecord(scope.contract, value));
|
|
152
|
+
records.push(...result.data.slice(0, maxItems - records.length));
|
|
153
|
+
if (result.data.length < batchSize || records.length >= result.total) break;
|
|
154
|
+
page++;
|
|
155
|
+
}
|
|
156
|
+
ensureActive();
|
|
157
|
+
const mapped = mapData ? records.map(record => {
|
|
158
|
+
ensureActive();
|
|
159
|
+
// Mapping receives its own checked copy, never the records returned to the caller.
|
|
160
|
+
return decodeBaseRecord(snapshotPlainData(mapData(parseContractRecord(scope.contract, record))));
|
|
161
|
+
}) : records;
|
|
162
|
+
ensureActive();
|
|
163
|
+
if (download && mapped.length > 0) downloadData(mapped, scope.contract.name, format);
|
|
164
|
+
return records;
|
|
165
|
+
} catch (cause) {
|
|
166
|
+
if (!isActive(token, scope)) throw cancelled();
|
|
167
|
+
const failure = exportFailure(cause);
|
|
168
|
+
current = undefined;
|
|
169
|
+
isLoading = false;
|
|
170
|
+
report(failure, handler);
|
|
171
|
+
throw failure;
|
|
172
|
+
} finally {
|
|
173
|
+
if (current?.token === token) {
|
|
174
|
+
current = undefined;
|
|
175
|
+
isLoading = false;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
current = { token, scope, promise };
|
|
180
|
+
return promise;
|
|
181
|
+
} catch (cause) {
|
|
182
|
+
current = undefined;
|
|
183
|
+
isLoading = false;
|
|
184
|
+
const failure = exportFailure(cause);
|
|
185
|
+
report(failure, typeof onError === 'function' ? onError : undefined);
|
|
186
|
+
return Promise.reject(failure);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return { triggerExport, get isLoading() { return isLoading; }, get error() { return error; } };
|
|
191
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Type } from '@sinclair/typebox';
|
|
2
|
+
import { checkExact } from './schema-validation';
|
|
3
|
+
import { snapshotPlainData } from './plain-data';
|
|
4
|
+
import { HttpError } from './types';
|
|
5
|
+
import type { FeatureGateConfig, FeatureGateUser } from './permissions.svelte';
|
|
6
|
+
|
|
7
|
+
const name = Type.String({ minLength: 1, pattern: '\\S' });
|
|
8
|
+
const constraints = {
|
|
9
|
+
roles: Type.Optional(Type.Array(name)),
|
|
10
|
+
permissions: Type.Optional(Type.Array(name)),
|
|
11
|
+
};
|
|
12
|
+
const configuration = Type.Union([
|
|
13
|
+
Type.Object(constraints, { additionalProperties: false }),
|
|
14
|
+
Type.Object({
|
|
15
|
+
...constraints,
|
|
16
|
+
minRole: name,
|
|
17
|
+
roleHierarchy: Type.Array(name, { minItems: 1, uniqueItems: true }),
|
|
18
|
+
}, { additionalProperties: false }),
|
|
19
|
+
]);
|
|
20
|
+
const user = Type.Object({
|
|
21
|
+
role: name,
|
|
22
|
+
permissions: Type.Array(name),
|
|
23
|
+
}, { additionalProperties: false });
|
|
24
|
+
|
|
25
|
+
export function snapshotFeatureGateConfig(value: unknown): FeatureGateConfig {
|
|
26
|
+
try {
|
|
27
|
+
const candidate = snapshotPlainData(value);
|
|
28
|
+
if (checkExact(configuration, candidate) &&
|
|
29
|
+
(!('minRole' in candidate) || candidate.roleHierarchy.includes(candidate.minRole))) {
|
|
30
|
+
if (candidate.roles) Object.freeze(candidate.roles);
|
|
31
|
+
if (candidate.permissions) Object.freeze(candidate.permissions);
|
|
32
|
+
if ('roleHierarchy' in candidate) Object.freeze(candidate.roleHierarchy);
|
|
33
|
+
return Object.freeze(candidate);
|
|
34
|
+
}
|
|
35
|
+
} catch {
|
|
36
|
+
// Configuration errors never retain caller data or reflection diagnostics.
|
|
37
|
+
}
|
|
38
|
+
throw new HttpError('Invalid feature gate configuration', 422, undefined, { code: 'INVALID_FEATURE_GATE_CONFIG' });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Invalid presentation hints deny visibility instead of throwing during rendering. */
|
|
42
|
+
export function parseFeatureGateUser(value: unknown): FeatureGateUser | undefined {
|
|
43
|
+
try {
|
|
44
|
+
const candidate = snapshotPlainData(value);
|
|
45
|
+
if (checkExact(user, candidate)) {
|
|
46
|
+
Object.freeze(candidate.permissions);
|
|
47
|
+
return Object.freeze(candidate);
|
|
48
|
+
}
|
|
49
|
+
} catch {
|
|
50
|
+
// Never invoke user getters or custom permission-collection methods.
|
|
51
|
+
}
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
package/src/hook-utils.svelte.ts
CHANGED
|
@@ -1,41 +1,25 @@
|
|
|
1
|
+
import { definedOptions } from './defined-options';
|
|
1
2
|
// Hook Utilities — shared reactive helpers for all hooks
|
|
2
3
|
// Uses Svelte 5 runes ($state, $effect) for automatic lifecycle management
|
|
3
4
|
|
|
4
5
|
import { useQueryClient } from '@tanstack/svelte-query';
|
|
5
|
-
import type { LiveProvider, LiveEvent, LiveMode } from './live.svelte';
|
|
6
6
|
import { captureAdminContext } from './context.svelte';
|
|
7
7
|
import type { AdminContextAccessor } from './context.svelte';
|
|
8
|
-
import {
|
|
8
|
+
import { createCheckedLiveSubscription, type LiveSubscriptionParams } from './live-subscription.svelte';
|
|
9
|
+
export type { LiveSubscriptionParams } from './live-subscription.svelte';
|
|
9
10
|
import { notifyWithProvider } from './notification.svelte';
|
|
10
11
|
import type { NotificationProvider } from './types';
|
|
12
|
+
import { handleAuthError } from './auth-hooks.svelte';
|
|
13
|
+
import { untrack } from 'svelte';
|
|
11
14
|
|
|
12
15
|
// ─── Auth Error Delegate ────────────────────────────────────────
|
|
13
16
|
// Delegate auth errors to authProvider.onError() — refine pattern
|
|
14
17
|
|
|
15
18
|
export async function checkError(
|
|
16
19
|
error: unknown,
|
|
17
|
-
adminContext: AdminContextAccessor
|
|
20
|
+
adminContext: AdminContextAccessor=captureAdminContext(),
|
|
18
21
|
): Promise<void> {
|
|
19
|
-
|
|
20
|
-
const authProvider = adminContext.authProvider;
|
|
21
|
-
if (authProvider?.onError) {
|
|
22
|
-
const result = await authProvider.onError(error);
|
|
23
|
-
if (!result) return;
|
|
24
|
-
if (result.logout) {
|
|
25
|
-
await authProvider.logout?.();
|
|
26
|
-
await adminContext.navigate(result.redirectTo ?? '/login');
|
|
27
|
-
} else if (result.redirectTo) {
|
|
28
|
-
await adminContext.navigate(result.redirectTo);
|
|
29
|
-
}
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
if (error && typeof error === 'object' && 'statusCode' in error) {
|
|
33
|
-
const status = (error as { statusCode: number }).statusCode;
|
|
34
|
-
if (status === 401) {
|
|
35
|
-
await adminContext.navigate('/login');
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
} catch { /* auth check failed silently */ }
|
|
22
|
+
await untrack(() => handleAuthError(error,adminContext));
|
|
39
23
|
}
|
|
40
24
|
|
|
41
25
|
// ─── Overtime Tracker ───────────────────────────────────────────
|
|
@@ -52,21 +36,21 @@ export interface OvertimeResult {
|
|
|
52
36
|
|
|
53
37
|
export function createOvertimeTracker(
|
|
54
38
|
isLoadingFn: () => boolean,
|
|
55
|
-
options: OvertimeOptions
|
|
39
|
+
options: OvertimeOptions={},
|
|
56
40
|
): OvertimeResult {
|
|
57
|
-
const { interval
|
|
58
|
-
let elapsedTime
|
|
41
|
+
const { interval=1000,onInterval }=options;
|
|
42
|
+
let elapsedTime=$state(0);
|
|
59
43
|
|
|
60
44
|
$effect(() => {
|
|
61
|
-
if
|
|
62
|
-
elapsedTime
|
|
63
|
-
const timer
|
|
64
|
-
elapsedTime
|
|
45
|
+
if(isLoadingFn()) {
|
|
46
|
+
elapsedTime=0;
|
|
47
|
+
const timer=setInterval(() => {
|
|
48
|
+
elapsedTime+=interval;
|
|
65
49
|
onInterval?.(elapsedTime);
|
|
66
|
-
},
|
|
50
|
+
},interval);
|
|
67
51
|
return () => clearInterval(timer);
|
|
68
52
|
} else {
|
|
69
|
-
elapsedTime
|
|
53
|
+
elapsedTime=0;
|
|
70
54
|
}
|
|
71
55
|
});
|
|
72
56
|
|
|
@@ -78,63 +62,23 @@ export function createOvertimeTracker(
|
|
|
78
62
|
// ─── Live Subscription ──────────────────────────────────────────
|
|
79
63
|
// Auto-subscribes to realtime events, invalidates queries on 'auto' mode
|
|
80
64
|
|
|
81
|
-
export interface LiveSubscriptionParams {
|
|
82
|
-
resource: string;
|
|
83
|
-
liveProvider?: LiveProvider;
|
|
84
|
-
liveMode?: LiveMode;
|
|
85
|
-
onLiveEvent?: (event: LiveEvent) => void;
|
|
86
|
-
liveParams?: Record<string, unknown>;
|
|
87
|
-
enabled?: boolean;
|
|
88
|
-
dataProviderName?: string;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
65
|
export function createLiveSubscription(paramsFn: () => LiveSubscriptionParams): void {
|
|
92
|
-
const queryClient
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
$effect(() => {
|
|
96
|
-
const params = paramsFn();
|
|
97
|
-
const liveProvider = params.liveProvider;
|
|
98
|
-
const liveMode = params.liveMode ?? 'off';
|
|
99
|
-
const enabled = params.enabled ?? true;
|
|
100
|
-
|
|
101
|
-
if (!liveProvider || liveMode === 'off' || !enabled) return;
|
|
102
|
-
|
|
103
|
-
let unsubscribe: (() => void) | undefined;
|
|
104
|
-
try {
|
|
105
|
-
unsubscribe = liveProvider.subscribe({
|
|
106
|
-
resource: params.resource,
|
|
107
|
-
liveParams: params.liveParams,
|
|
108
|
-
callback: (event: LiveEvent) => {
|
|
109
|
-
params.onLiveEvent?.(event);
|
|
110
|
-
if (liveMode === 'auto') {
|
|
111
|
-
const matcher = adminContext.queryKeyMatcher(params.resource, params.dataProviderName);
|
|
112
|
-
queryClient.invalidateQueries({
|
|
113
|
-
predicate: (q) => dataQueryMatches(q.queryKey, { ...matcher, resource: params.resource }),
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
},
|
|
117
|
-
});
|
|
118
|
-
} catch (e) {
|
|
119
|
-
console.warn('[svadmin] LiveProvider.subscribe failed:', e);
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
return unsubscribe;
|
|
123
|
-
});
|
|
66
|
+
const queryClient=useQueryClient();
|
|
67
|
+
createCheckedLiveSubscription(paramsFn,queryClient);
|
|
124
68
|
}
|
|
125
69
|
|
|
126
70
|
// ─── Notification Helpers ───────────────────────────────────────
|
|
127
71
|
|
|
128
|
-
export type NotificationConfig
|
|
129
|
-
|
|
|
130
|
-
|
|
|
131
|
-
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
|
72
|
+
export type NotificationConfig=
|
|
73
|
+
|string
|
|
74
|
+
|false
|
|
75
|
+
|((data?: unknown,values?: unknown,resource?: string) => {
|
|
76
|
+
message: string;
|
|
77
|
+
description?: string;
|
|
78
|
+
type?: 'success'|'error';
|
|
79
|
+
key?: string;
|
|
80
|
+
})
|
|
81
|
+
|undefined;
|
|
138
82
|
|
|
139
83
|
export interface SuccessNotificationRequest {
|
|
140
84
|
config: NotificationConfig;
|
|
@@ -142,7 +86,7 @@ export interface SuccessNotificationRequest {
|
|
|
142
86
|
data?: unknown;
|
|
143
87
|
values?: unknown;
|
|
144
88
|
resource?: string;
|
|
145
|
-
provider?: NotificationProvider
|
|
89
|
+
provider?: NotificationProvider|null;
|
|
146
90
|
}
|
|
147
91
|
|
|
148
92
|
export interface ErrorNotificationRequest {
|
|
@@ -150,40 +94,44 @@ export interface ErrorNotificationRequest {
|
|
|
150
94
|
defaultMessage: string;
|
|
151
95
|
error?: unknown;
|
|
152
96
|
resource?: string;
|
|
153
|
-
provider?: NotificationProvider
|
|
97
|
+
provider?: NotificationProvider|null;
|
|
154
98
|
}
|
|
155
99
|
|
|
156
100
|
export function fireSuccessNotification(request: SuccessNotificationRequest): void {
|
|
157
|
-
const { config,
|
|
158
|
-
if
|
|
159
|
-
if
|
|
160
|
-
if
|
|
161
|
-
const result
|
|
101
|
+
const { config,defaultMessage,data,values,resource,provider }=request;
|
|
102
|
+
if(config===false) return;
|
|
103
|
+
if(!config&&!defaultMessage) return;
|
|
104
|
+
if(typeof config==='function') {
|
|
105
|
+
const result=config(data,values,resource);
|
|
162
106
|
notifyWithProvider({
|
|
163
107
|
type: 'success',
|
|
164
108
|
message: result.message,
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
109
|
+
...definedOptions({
|
|
110
|
+
description: result.description,
|
|
111
|
+
key: result.key,
|
|
112
|
+
}),
|
|
113
|
+
},provider);
|
|
168
114
|
return;
|
|
169
115
|
}
|
|
170
|
-
notifyWithProvider({ type: 'success',
|
|
116
|
+
notifyWithProvider({ type: 'success',message: config||defaultMessage },provider);
|
|
171
117
|
}
|
|
172
118
|
|
|
173
119
|
export function fireErrorNotification(request: ErrorNotificationRequest): void {
|
|
174
|
-
const { config,
|
|
175
|
-
if
|
|
176
|
-
if
|
|
177
|
-
if
|
|
178
|
-
const result
|
|
120
|
+
const { config,defaultMessage,error,resource,provider }=request;
|
|
121
|
+
if(config===false) return;
|
|
122
|
+
if(!config&&!defaultMessage) return;
|
|
123
|
+
if(typeof config==='function') {
|
|
124
|
+
const result=config(error,undefined,resource);
|
|
179
125
|
notifyWithProvider({
|
|
180
126
|
type: 'error',
|
|
181
127
|
message: result.message,
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
128
|
+
...definedOptions({
|
|
129
|
+
description: result.description,
|
|
130
|
+
key: result.key,
|
|
131
|
+
}),
|
|
132
|
+
},provider);
|
|
185
133
|
return;
|
|
186
134
|
}
|
|
187
|
-
const errMsg
|
|
188
|
-
notifyWithProvider({ type: 'error',
|
|
135
|
+
const errMsg=error instanceof Error? error.message:String(error??'');
|
|
136
|
+
notifyWithProvider({ type: 'error',message: config||`${defaultMessage}${errMsg? ': '+errMsg:''}` },provider);
|
|
189
137
|
}
|