@svadmin/core 0.31.0 → 0.32.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 +3 -3
- package/src/auth-hooks.svelte.ts +42 -25
- package/src/context.svelte.ts +185 -46
- package/src/data-transfer.svelte.ts +5 -3
- package/src/form-hooks.svelte.ts +322 -58
- package/src/helpers.ts +11 -7
- package/src/hook-utils.svelte.ts +13 -12
- package/src/hooks.svelte.ts +29 -23
- package/src/http.ts +114 -33
- package/src/i18n.svelte.ts +181 -35
- package/src/index.ts +7 -6
- package/src/mutation-hooks.svelte.ts +27 -16
- package/src/query-hooks.svelte.ts +16 -13
- package/src/router.ts +28 -8
- package/src/routing-hooks.svelte.ts +13 -18
- package/src/table-hooks.svelte.ts +5 -3
- package/src/table-state.svelte.ts +6 -3
- package/src/task-hooks.svelte.ts +27 -14
- package/src/theme.svelte.ts +248 -154
- package/src/toast.svelte.ts +1 -2
- package/src/types.ts +26 -4
- package/src/unsaved-changes.svelte.ts +4 -38
- package/src/url-sync.ts +47 -8
- package/src/useParsed.svelte.ts +16 -11
- package/src/utility-hooks.svelte.ts +8 -7
package/src/i18n.svelte.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Minimal i18n — simple key-value translation
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
import { createContext } from 'svelte';
|
|
4
4
|
|
|
5
5
|
const baseLocales: Record<string, Record<string, string>> = {
|
|
6
6
|
'zh-CN': {
|
|
@@ -994,52 +994,189 @@ export interface I18nProvider {
|
|
|
994
994
|
getAvailableLocales?: () => string[];
|
|
995
995
|
}
|
|
996
996
|
|
|
997
|
+
/** Owner-controlled values for one component-tree i18n scope. */
|
|
998
|
+
export interface I18nScopeOptions {
|
|
999
|
+
locale?: string;
|
|
1000
|
+
provider?: I18nProvider;
|
|
1001
|
+
onLocaleChange?: (locale: string) => void;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
/**
|
|
1005
|
+
* Tree-local i18n state. `updateOwner` synchronizes props without emitting a
|
|
1006
|
+
* change event, while `setLocale` represents a change requested inside the tree.
|
|
1007
|
+
*/
|
|
1008
|
+
export interface I18nScope {
|
|
1009
|
+
readonly locale: string;
|
|
1010
|
+
readonly provider: I18nProvider | undefined;
|
|
1011
|
+
translate: (key: string, params?: Record<string, string | number>) => string;
|
|
1012
|
+
setLocale: (locale: string) => void;
|
|
1013
|
+
setProvider: (provider: I18nProvider | undefined) => void;
|
|
1014
|
+
getAvailableLocales: () => string[];
|
|
1015
|
+
updateOwner: (options: I18nScopeOptions) => void;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
const [readI18nScopeContext, writeI18nScopeContext] = createContext<I18nScope>();
|
|
1019
|
+
|
|
1020
|
+
function translateFromTables(
|
|
1021
|
+
locale: string,
|
|
1022
|
+
key: string,
|
|
1023
|
+
params?: Record<string, string | number>,
|
|
1024
|
+
): string {
|
|
1025
|
+
const localeTable = locales[locale];
|
|
1026
|
+
let text = localeTable?.[key] ?? locales.en?.[key] ?? key;
|
|
1027
|
+
|
|
1028
|
+
if (params) {
|
|
1029
|
+
for (const [param, value] of Object.entries(params)) {
|
|
1030
|
+
text = text.replaceAll(`{${param}}`, String(value));
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
return text;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
function readCurrentI18nScope(): I18nScope | undefined {
|
|
1038
|
+
try {
|
|
1039
|
+
return readI18nScopeContext();
|
|
1040
|
+
} catch {
|
|
1041
|
+
// Context is only readable while Svelte owns the current render/effect.
|
|
1042
|
+
// Public facade calls made outside a component intentionally use legacy state.
|
|
1043
|
+
return undefined;
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
/** Create reactive i18n state that can be owned by one AdminApp tree. */
|
|
1048
|
+
export function createI18nScope(options: I18nScopeOptions = {}): I18nScope {
|
|
1049
|
+
const initialProviderLocale = options.provider?.getLocale();
|
|
1050
|
+
const initialLocale = options.locale ?? initialProviderLocale ?? detectLocale();
|
|
1051
|
+
let scopeLocale = $state(initialLocale);
|
|
1052
|
+
let scopeProvider = $state.raw<I18nProvider | undefined>(options.provider);
|
|
1053
|
+
let onLocaleChange = $state.raw<((locale: string) => void) | undefined>(options.onLocaleChange);
|
|
1054
|
+
let ownerControlsLocale = options.locale !== undefined;
|
|
1055
|
+
|
|
1056
|
+
function synchronizeProviderLocale(locale: string): void {
|
|
1057
|
+
if (scopeProvider && scopeProvider.getLocale() !== locale) {
|
|
1058
|
+
scopeProvider.setLocale(locale);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
if (options.locale !== undefined) {
|
|
1063
|
+
synchronizeProviderLocale(options.locale);
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
const scope: I18nScope = {
|
|
1067
|
+
get locale() {
|
|
1068
|
+
return scopeLocale;
|
|
1069
|
+
},
|
|
1070
|
+
get provider() {
|
|
1071
|
+
return scopeProvider;
|
|
1072
|
+
},
|
|
1073
|
+
translate(key, params) {
|
|
1074
|
+
// Establish the locale dependency even when a provider owns translation.
|
|
1075
|
+
const locale = scopeLocale;
|
|
1076
|
+
return scopeProvider?.translate(key, params) ?? translateFromTables(locale, key, params);
|
|
1077
|
+
},
|
|
1078
|
+
setLocale(locale) {
|
|
1079
|
+
const providerLocale = scopeProvider?.getLocale();
|
|
1080
|
+
const changed = scopeLocale !== locale || (scopeProvider !== undefined && providerLocale !== locale);
|
|
1081
|
+
|
|
1082
|
+
scopeLocale = locale;
|
|
1083
|
+
synchronizeProviderLocale(locale);
|
|
1084
|
+
if (changed) onLocaleChange?.(locale);
|
|
1085
|
+
},
|
|
1086
|
+
setProvider(provider) {
|
|
1087
|
+
if (scopeProvider === provider) return;
|
|
1088
|
+
scopeProvider = provider;
|
|
1089
|
+
// Provider replacement is owner configuration, not a tree locale action.
|
|
1090
|
+
scopeLocale = provider?.getLocale() || detectLocale();
|
|
1091
|
+
},
|
|
1092
|
+
getAvailableLocales() {
|
|
1093
|
+
return scopeProvider?.getAvailableLocales?.() ?? Object.keys(locales);
|
|
1094
|
+
},
|
|
1095
|
+
updateOwner(nextOptions) {
|
|
1096
|
+
const providerChanged = scopeProvider !== nextOptions.provider;
|
|
1097
|
+
const clearedControlledLocale = ownerControlsLocale && nextOptions.locale === undefined;
|
|
1098
|
+
scopeProvider = nextOptions.provider;
|
|
1099
|
+
onLocaleChange = nextOptions.onLocaleChange;
|
|
1100
|
+
ownerControlsLocale = nextOptions.locale !== undefined;
|
|
1101
|
+
|
|
1102
|
+
if (nextOptions.locale !== undefined) {
|
|
1103
|
+
scopeLocale = nextOptions.locale;
|
|
1104
|
+
synchronizeProviderLocale(nextOptions.locale);
|
|
1105
|
+
} else if (providerChanged || clearedControlledLocale) {
|
|
1106
|
+
scopeLocale = scopeProvider?.getLocale() || detectLocale();
|
|
1107
|
+
}
|
|
1108
|
+
},
|
|
1109
|
+
};
|
|
1110
|
+
|
|
1111
|
+
return scope;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
/** Attach an i18n scope to the current component tree. */
|
|
1115
|
+
export function provideI18nScope(scope: I18nScope): I18nScope {
|
|
1116
|
+
writeI18nScopeContext(scope);
|
|
1117
|
+
return scope;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
/** Return the current tree scope when called during a Svelte render/effect. */
|
|
1121
|
+
export function getI18nScope(): I18nScope | undefined {
|
|
1122
|
+
return readCurrentI18nScope();
|
|
1123
|
+
}
|
|
1124
|
+
|
|
997
1125
|
let i18nProvider: I18nProvider | undefined;
|
|
998
1126
|
|
|
1127
|
+
function setLegacyLocale(locale: string): void {
|
|
1128
|
+
i18nProvider?.setLocale(locale);
|
|
1129
|
+
currentLocale = locale;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
function getLegacyLocale(): string {
|
|
1133
|
+
// Reading the rune keeps legacy consumers reactive.
|
|
1134
|
+
const locale = currentLocale;
|
|
1135
|
+
return i18nProvider?.getLocale() ?? locale;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
function getLegacyAvailableLocales(): string[] {
|
|
1139
|
+
return i18nProvider?.getAvailableLocales?.() ?? Object.keys(locales);
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
function translateLegacy(key: string, params?: Record<string, string | number>): string {
|
|
1143
|
+
const locale = currentLocale;
|
|
1144
|
+
return i18nProvider?.translate(key, params) ?? translateFromTables(locale, key, params);
|
|
1145
|
+
}
|
|
1146
|
+
|
|
999
1147
|
export function setI18nProvider(provider: I18nProvider | undefined): void {
|
|
1148
|
+
const scope = readCurrentI18nScope();
|
|
1149
|
+
if (scope) {
|
|
1150
|
+
scope.setProvider(provider);
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1000
1153
|
i18nProvider = provider;
|
|
1001
1154
|
}
|
|
1002
1155
|
|
|
1003
1156
|
export function getI18nProvider(): I18nProvider | undefined {
|
|
1004
|
-
|
|
1157
|
+
const scope = readCurrentI18nScope();
|
|
1158
|
+
return scope ? scope.provider : i18nProvider;
|
|
1005
1159
|
}
|
|
1006
1160
|
|
|
1007
1161
|
export function setLocale(locale: string): void {
|
|
1008
|
-
|
|
1009
|
-
|
|
1162
|
+
const scope = readCurrentI18nScope();
|
|
1163
|
+
if (scope) {
|
|
1164
|
+
scope.setLocale(locale);
|
|
1165
|
+
return;
|
|
1010
1166
|
}
|
|
1011
|
-
|
|
1012
|
-
currentLocale = locale;
|
|
1167
|
+
setLegacyLocale(locale);
|
|
1013
1168
|
}
|
|
1014
1169
|
|
|
1015
1170
|
export function getLocale(): string {
|
|
1016
|
-
|
|
1017
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
|
1018
|
-
currentLocale;
|
|
1019
|
-
if (i18nProvider) return i18nProvider.getLocale();
|
|
1020
|
-
return currentLocale;
|
|
1171
|
+
return readCurrentI18nScope()?.locale ?? getLegacyLocale();
|
|
1021
1172
|
}
|
|
1022
1173
|
|
|
1023
1174
|
export function getAvailableLocales(): string[] {
|
|
1024
|
-
|
|
1025
|
-
return Object.keys(locales);
|
|
1175
|
+
return readCurrentI18nScope()?.getAvailableLocales() ?? getLegacyAvailableLocales();
|
|
1026
1176
|
}
|
|
1027
1177
|
|
|
1028
1178
|
export function t(key: string, params?: Record<string, string | number>): string {
|
|
1029
|
-
|
|
1030
|
-
// 确保无论是否使用 provider,语言切换都能触发模板刷新。
|
|
1031
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
|
1032
|
-
currentLocale;
|
|
1033
|
-
if (i18nProvider) return i18nProvider.translate(key, params);
|
|
1034
|
-
const locale = locales[currentLocale];
|
|
1035
|
-
let text = locale?.[key] ?? locales['en']?.[key] ?? key;
|
|
1036
|
-
|
|
1037
|
-
if (params) {
|
|
1038
|
-
for (const [k, v] of Object.entries(params)) {
|
|
1039
|
-
text = text.replaceAll(`{${k}}`, String(v));
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1042
|
-
return text;
|
|
1179
|
+
return readCurrentI18nScope()?.translate(key, params) ?? translateLegacy(key, params);
|
|
1043
1180
|
}
|
|
1044
1181
|
|
|
1045
1182
|
export function addTranslations(locale: string, translations: Record<string, string>): void {
|
|
@@ -1048,7 +1185,7 @@ export function addTranslations(locale: string, translations: Record<string, str
|
|
|
1048
1185
|
|
|
1049
1186
|
export function resetI18n(): void {
|
|
1050
1187
|
currentLocale = detectLocale();
|
|
1051
|
-
locales = initialLocales;
|
|
1188
|
+
locales = structuredClone(initialLocales);
|
|
1052
1189
|
i18nProvider = undefined;
|
|
1053
1190
|
}
|
|
1054
1191
|
|
|
@@ -1057,14 +1194,23 @@ export function resetI18n(): void {
|
|
|
1057
1194
|
* Tracks locale changes natively.
|
|
1058
1195
|
*/
|
|
1059
1196
|
export function useTranslation() {
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1197
|
+
// Capture context while the component is initializing. Event handlers and
|
|
1198
|
+
// async continuations cannot safely call getContext later.
|
|
1199
|
+
const scope = readCurrentI18nScope();
|
|
1200
|
+
const translate = scope
|
|
1201
|
+
? (key: string, params?: Record<string, string | number>) => scope.translate(key, params)
|
|
1202
|
+
: translateLegacy;
|
|
1203
|
+
const changeLocale = scope
|
|
1204
|
+
? (locale: string) => scope.setLocale(locale)
|
|
1205
|
+
: setLegacyLocale;
|
|
1206
|
+
const availableLocales = scope
|
|
1207
|
+
? () => scope.getAvailableLocales()
|
|
1208
|
+
: getLegacyAvailableLocales;
|
|
1209
|
+
|
|
1064
1210
|
return {
|
|
1065
1211
|
get t() { return translate; },
|
|
1066
|
-
get locale() { return
|
|
1067
|
-
setLocale,
|
|
1068
|
-
getAvailableLocales
|
|
1212
|
+
get locale() { return scope?.locale ?? getLegacyLocale(); },
|
|
1213
|
+
setLocale: changeLocale,
|
|
1214
|
+
getAvailableLocales: availableLocales,
|
|
1069
1215
|
};
|
|
1070
1216
|
}
|
package/src/index.ts
CHANGED
|
@@ -7,9 +7,10 @@ export {
|
|
|
7
7
|
setRouterProvider, getRouterProvider,
|
|
8
8
|
setLiveProvider, getLiveProvider,
|
|
9
9
|
setTaskProvider, getTaskProvider,
|
|
10
|
+
createAdminContext, provideAdminContext, getAdminContext, captureAdminContext,
|
|
10
11
|
resetContext,
|
|
11
12
|
} from './context.svelte';
|
|
12
|
-
export type { DataProviderInput } from './context.svelte';
|
|
13
|
+
export type { DataProviderInput, AdminContextSource, AdminContextValue, AdminContextAccessor } from './context.svelte';
|
|
13
14
|
export {
|
|
14
15
|
useList, useInfiniteList,
|
|
15
16
|
useOne, useShow,
|
|
@@ -36,14 +37,14 @@ export { setAccessControlProvider, getAccessControlProvider, getAccessControlOpt
|
|
|
36
37
|
export { useLive, useSubscription, usePublish } from './live.svelte';
|
|
37
38
|
export { toast, getToastQueue, consumeToastQueue, getPromiseQueue, consumePromiseQueue, resetToast } from './toast.svelte';
|
|
38
39
|
export { notify, closeNotification, setNotificationProvider, getNotificationProvider } from './notification.svelte';
|
|
39
|
-
export { t, setLocale, getLocale, getAvailableLocales, addTranslations, useTranslation, setI18nProvider, getI18nProvider, resetI18n } from './i18n.svelte';
|
|
40
|
-
export type { I18nProvider } from './i18n.svelte';
|
|
40
|
+
export { t, setLocale, getLocale, getAvailableLocales, addTranslations, useTranslation, setI18nProvider, getI18nProvider, createI18nScope, provideI18nScope, getI18nScope, resetI18n } from './i18n.svelte';
|
|
41
|
+
export type { I18nProvider, I18nScope, I18nScopeOptions } from './i18n.svelte';
|
|
41
42
|
export { audit, setAuditHandler, setAuditLogProvider, getAuditLogProvider } from './audit';
|
|
42
43
|
export type { AuditLogProvider } from './audit';
|
|
43
44
|
export { setChatProvider, getChatProvider, setChatContext, getChatContext, setAgentProvider, getAgentProvider, registerApproval, resolveApproval, hasPendingApprovals, resetChatProvider } from './chatProvider.svelte';
|
|
44
45
|
export type { ChatProvider, ChatMessage, ChatContext, ChatAction, AgentProvider, AgentEvent, AgentOptions, AdminTool, AdminToolParameter, ToolResult } from './chatProvider.svelte';
|
|
45
|
-
export { getTheme, setTheme, toggleTheme, getResolvedTheme, getColorTheme, setColorTheme, getColorThemes, configureTheme, getThemeConfig, clearCssOverrides, builtinPresets, registerColorPreset, getColorPresets, resetTheme } from './theme.svelte';
|
|
46
|
-
export type { ThemeMode, ColorTheme, ThemeStrategy, ThemeConfig, ColorPreset } from './theme.svelte';
|
|
46
|
+
export { getTheme, setTheme, toggleTheme, getResolvedTheme, getColorTheme, setColorTheme, getColorThemes, configureTheme, getThemeConfig, clearCssOverrides, builtinPresets, registerColorPreset, getColorPresets, registerThemeOwner, updateThemeOwner, unregisterThemeOwner, resetTheme } from './theme.svelte';
|
|
47
|
+
export type { ThemeMode, ColorTheme, ThemeStrategy, ThemeConfig, ColorPreset, ThemeOwnerOptions, ThemeOwnerToken } from './theme.svelte';
|
|
47
48
|
export { setUnsavedChanges, getUnsavedChanges, initUnsavedChangesNotifier, resetUnsavedChanges } from './unsaved-changes.svelte';
|
|
48
49
|
export { setAdminOptions, getAdminOptions, getTextTransformers } from './options.svelte';
|
|
49
50
|
export type { AdminOptions, TextTransformers, OvertimeConfig } from './options.svelte';
|
|
@@ -54,7 +55,7 @@ export * from './query-keys';
|
|
|
54
55
|
export { HttpError, UndoError } from './types';
|
|
55
56
|
export type {
|
|
56
57
|
DataProvider, AuthProvider, NotificationProvider, MutationMode,
|
|
57
|
-
ValidationErrors, CrudOperator, LogicalFilter, FieldFilter,
|
|
58
|
+
ValidationErrors, HttpErrorOptions, CrudOperator, LogicalFilter, FieldFilter,
|
|
58
59
|
CustomParams, CustomResult,
|
|
59
60
|
GetListParams, GetListResult,
|
|
60
61
|
GetOneParams, GetOneResult,
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
2
|
import { createMutation, useQueryClient } from '@tanstack/svelte-query';
|
|
3
3
|
import { getAdminOptions } from './options.svelte';
|
|
4
|
-
import {
|
|
4
|
+
import { captureAdminContext } from './context.svelte';
|
|
5
|
+
import type { AdminContextAccessor } from './context.svelte';
|
|
5
6
|
import { useParsed } from './useParsed.svelte';
|
|
6
7
|
import { audit } from './audit';
|
|
7
8
|
import { UndoError } from './types';
|
|
@@ -12,9 +13,14 @@ import { toast } from './toast.svelte';
|
|
|
12
13
|
|
|
13
14
|
// ─── Helper: publish live events after mutations ────────────────
|
|
14
15
|
|
|
15
|
-
export function publishLiveEvent(
|
|
16
|
+
export function publishLiveEvent(
|
|
17
|
+
resource: string,
|
|
18
|
+
type: 'created' | 'updated' | 'deleted',
|
|
19
|
+
ids?: (string | number)[],
|
|
20
|
+
adminContext: AdminContextAccessor = captureAdminContext(),
|
|
21
|
+
) {
|
|
16
22
|
try {
|
|
17
|
-
const liveProvider =
|
|
23
|
+
const liveProvider = adminContext.liveProvider;
|
|
18
24
|
if (liveProvider?.publish) {
|
|
19
25
|
liveProvider.publish({
|
|
20
26
|
type: type === 'created' ? 'INSERT' : type === 'updated' ? 'UPDATE' : 'DELETE',
|
|
@@ -42,7 +48,9 @@ export function invalidateByScopes(
|
|
|
42
48
|
): void {
|
|
43
49
|
if (scopes === false) return;
|
|
44
50
|
const effectiveScopes = (scopes && scopes.length > 0) ? scopes : defaults;
|
|
45
|
-
const dpMatch =
|
|
51
|
+
const dpMatch = dataProviderName === undefined
|
|
52
|
+
? () => true
|
|
53
|
+
: (q: { queryKey: readonly unknown[] }) => q.queryKey[0] === dataProviderName;
|
|
46
54
|
for (const scope of effectiveScopes) {
|
|
47
55
|
if (scope === 'list') queryClient.invalidateQueries({ predicate: (q) => dpMatch(q) && q.queryKey[1] === resource && (q.queryKey[2] === 'list' || q.queryKey[2] === 'infiniteList' || q.queryKey[2] === 'select' || q.queryKey[2] === 'select-defaults') });
|
|
48
56
|
else if (scope === 'many') queryClient.invalidateQueries({ predicate: (q) => dpMatch(q) && q.queryKey[1] === resource && q.queryKey[2] === 'many' });
|
|
@@ -99,6 +107,7 @@ export interface UseCreateMutateParams<TVariables> {
|
|
|
99
107
|
}
|
|
100
108
|
|
|
101
109
|
export function useCreate<TData extends BaseRecord = BaseRecord, TError = HttpError, TVariables = Record<string, unknown>>(options: UseCreateOptions = {}) {
|
|
110
|
+
const adminContext = captureAdminContext();
|
|
102
111
|
const parsed = useParsed();
|
|
103
112
|
const defaultResource = options.resource ?? parsed.resource ?? '';
|
|
104
113
|
const adminOptions = getAdminOptions();
|
|
@@ -114,7 +123,7 @@ export function useCreate<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
114
123
|
...options.mutationOptions,
|
|
115
124
|
mutationFn: async (params) => {
|
|
116
125
|
const resName = params.resource ?? defaultResource;
|
|
117
|
-
const provider = getDataProviderForResource(resName, params.dataProviderName);
|
|
126
|
+
const provider = adminContext.getDataProviderForResource(resName, params.dataProviderName);
|
|
118
127
|
return await provider.create<TData, TVariables>({
|
|
119
128
|
resource: resName,
|
|
120
129
|
variables: params.variables,
|
|
@@ -125,11 +134,11 @@ export function useCreate<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
125
134
|
onSuccess: (data, params, context) => {
|
|
126
135
|
const resName = params.resource ?? defaultResource;
|
|
127
136
|
fireSuccessNotification(params.successNotification, 'Created successfully', data.data, params.variables, resName);
|
|
128
|
-
const res = getResource(resName);
|
|
137
|
+
const res = adminContext.getResource(resName);
|
|
129
138
|
const pk = res.primaryKey ?? 'id';
|
|
130
139
|
const newId = (data.data as Record<string, unknown>)[pk];
|
|
131
140
|
audit({ action: 'create', resource: resName, recordId: String(newId) });
|
|
132
|
-
publishLiveEvent(resName, 'created', newId != null ? [newId as string | number] : undefined);
|
|
141
|
+
publishLiveEvent(resName, 'created', newId != null ? [newId as string | number] : undefined, adminContext);
|
|
133
142
|
// Invalidation in onSuccess for create (refine pattern — no optimistic data to reconcile on error)
|
|
134
143
|
invalidateByScopes(queryClient, resName, params.invalidates, ['list', 'many'], undefined, params.dataProviderName);
|
|
135
144
|
if (typeof options.mutationOptions?.onSuccess === 'function') {
|
|
@@ -137,7 +146,7 @@ export function useCreate<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
137
146
|
}
|
|
138
147
|
},
|
|
139
148
|
onError: (error, params, context) => {
|
|
140
|
-
checkError(error);
|
|
149
|
+
checkError(error, adminContext);
|
|
141
150
|
fireErrorNotification(params.errorNotification, 'Create failed', error, params.resource ?? defaultResource);
|
|
142
151
|
if (typeof options.mutationOptions?.onError === 'function') {
|
|
143
152
|
(options.mutationOptions.onError as (...a: unknown[]) => unknown)(error, params, context);
|
|
@@ -177,6 +186,7 @@ export interface UseUpdateMutateParams<TVariables> {
|
|
|
177
186
|
}
|
|
178
187
|
|
|
179
188
|
export function useUpdate<TData extends BaseRecord = BaseRecord, TError = HttpError, TVariables = Record<string, unknown>>(options: UseUpdateOptions = {}) {
|
|
189
|
+
const adminContext = captureAdminContext();
|
|
180
190
|
const parsed = useParsed();
|
|
181
191
|
const defaultResource = options.resource ?? parsed.resource ?? '';
|
|
182
192
|
const defaultId = options.id ?? parsed.id;
|
|
@@ -207,7 +217,7 @@ export function useUpdate<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
207
217
|
});
|
|
208
218
|
}
|
|
209
219
|
|
|
210
|
-
const provider = getDataProviderForResource(resName, params.dataProviderName);
|
|
220
|
+
const provider = adminContext.getDataProviderForResource(resName, params.dataProviderName);
|
|
211
221
|
return await provider.update<TData, TVariables>({
|
|
212
222
|
resource: resName,
|
|
213
223
|
id: targetId,
|
|
@@ -229,7 +239,7 @@ export function useUpdate<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
229
239
|
|
|
230
240
|
const previousQueries = queryClient.getQueriesData({ predicate: (q) => dp(q) && q.queryKey[1] === resName });
|
|
231
241
|
|
|
232
|
-
const pk = getResource(resName).primaryKey ?? 'id';
|
|
242
|
+
const pk = adminContext.getResource(resName).primaryKey ?? 'id';
|
|
233
243
|
const updater = params.optimisticUpdateMap ?? { list: true, many: true, detail: true };
|
|
234
244
|
|
|
235
245
|
if (updater.detail !== false) {
|
|
@@ -267,7 +277,7 @@ export function useUpdate<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
267
277
|
const targetId = params.id ?? defaultId;
|
|
268
278
|
fireSuccessNotification(params.successNotification, 'Updated successfully', data.data, params.variables, resName);
|
|
269
279
|
audit({ action: 'update', resource: resName, recordId: String(targetId) });
|
|
270
|
-
publishLiveEvent(resName, 'updated', targetId != null ? [targetId] : undefined);
|
|
280
|
+
publishLiveEvent(resName, 'updated', targetId != null ? [targetId] : undefined, adminContext);
|
|
271
281
|
if (typeof options.mutationOptions?.onSuccess === 'function') {
|
|
272
282
|
(options.mutationOptions.onSuccess as (...a: unknown[]) => unknown)(data, params, extractedCtx);
|
|
273
283
|
}
|
|
@@ -289,7 +299,7 @@ export function useUpdate<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
289
299
|
}
|
|
290
300
|
}
|
|
291
301
|
if (error instanceof UndoError) return;
|
|
292
|
-
checkError(error);
|
|
302
|
+
checkError(error, adminContext);
|
|
293
303
|
const extractedCtx = ctx?._svadmin_ctx ? ctx.userContext : context;
|
|
294
304
|
fireErrorNotification(params.errorNotification, 'Update failed', error, params.resource ?? defaultResource);
|
|
295
305
|
if (typeof options.mutationOptions?.onError === 'function') {
|
|
@@ -325,6 +335,7 @@ export interface UseDeleteMutateParams<TVariables> {
|
|
|
325
335
|
}
|
|
326
336
|
|
|
327
337
|
export function useDelete<TData extends BaseRecord = BaseRecord, TError = HttpError, TVariables = Record<string, unknown>>(options: UseDeleteOptions = {}) {
|
|
338
|
+
const adminContext = captureAdminContext();
|
|
328
339
|
const parsed = useParsed();
|
|
329
340
|
const defaultResource = options.resource ?? parsed.resource ?? '';
|
|
330
341
|
const defaultId = options.id ?? parsed.id;
|
|
@@ -355,7 +366,7 @@ export function useDelete<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
355
366
|
});
|
|
356
367
|
}
|
|
357
368
|
|
|
358
|
-
const provider = getDataProviderForResource(resName, params.dataProviderName);
|
|
369
|
+
const provider = adminContext.getDataProviderForResource(resName, params.dataProviderName);
|
|
359
370
|
return await provider.deleteOne<TData, TVariables>({
|
|
360
371
|
resource: resName,
|
|
361
372
|
id: targetId,
|
|
@@ -377,7 +388,7 @@ export function useDelete<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
377
388
|
|
|
378
389
|
const previousQueries = queryClient.getQueriesData({ predicate: (q) => dp(q) && q.queryKey[1] === resName });
|
|
379
390
|
|
|
380
|
-
const pk = getResource(resName).primaryKey ?? 'id';
|
|
391
|
+
const pk = adminContext.getResource(resName).primaryKey ?? 'id';
|
|
381
392
|
|
|
382
393
|
if (targetId != null) queryClient.removeQueries({ predicate: (q) => dp(q) && q.queryKey[1] === resName && q.queryKey[2] === 'one' && q.queryKey[3] === targetId });
|
|
383
394
|
|
|
@@ -408,7 +419,7 @@ export function useDelete<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
408
419
|
|
|
409
420
|
fireSuccessNotification(params.successNotification, 'Deleted successfully', data.data, params.variables, resName);
|
|
410
421
|
audit({ action: 'delete', resource: resName, recordId: String(targetId) });
|
|
411
|
-
publishLiveEvent(resName, 'deleted', targetId != null ? [targetId] : undefined);
|
|
422
|
+
publishLiveEvent(resName, 'deleted', targetId != null ? [targetId] : undefined, adminContext);
|
|
412
423
|
if (typeof options.mutationOptions?.onSuccess === 'function') {
|
|
413
424
|
(options.mutationOptions.onSuccess as (...a: unknown[]) => unknown)(data, params, extractedCtx);
|
|
414
425
|
}
|
|
@@ -430,7 +441,7 @@ export function useDelete<TData extends BaseRecord = BaseRecord, TError = HttpEr
|
|
|
430
441
|
}
|
|
431
442
|
}
|
|
432
443
|
if (error instanceof UndoError) return;
|
|
433
|
-
checkError(error);
|
|
444
|
+
checkError(error, adminContext);
|
|
434
445
|
const extractedCtx = ctx?._svadmin_ctx ? ctx.userContext : context;
|
|
435
446
|
fireErrorNotification(params.errorNotification, 'Delete failed', error, params.resource ?? defaultResource);
|
|
436
447
|
if (typeof options.mutationOptions?.onError === 'function') {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createQuery } from '@tanstack/svelte-query';
|
|
2
2
|
import { getAdminOptions } from './options.svelte';
|
|
3
|
-
import {
|
|
3
|
+
import { captureAdminContext } from './context.svelte';
|
|
4
4
|
import { useParsed } from './useParsed.svelte';
|
|
5
5
|
import {
|
|
6
6
|
checkError,
|
|
@@ -11,8 +11,8 @@ import {
|
|
|
11
11
|
} from './hook-utils.svelte';
|
|
12
12
|
import type { NotificationConfig, OvertimeOptions, LiveSubscriptionParams } from './hook-utils.svelte';
|
|
13
13
|
import type {
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
GetListResult, Pagination, Sort, Filter, BaseRecord, HttpError,
|
|
15
|
+
GetOneResult, GetManyResult,
|
|
16
16
|
KnownResources,
|
|
17
17
|
} from './types';
|
|
18
18
|
import type { LiveMode, LiveEvent } from './live.svelte';
|
|
@@ -74,6 +74,7 @@ export interface UseListOptions<_TData extends BaseRecord = BaseRecord, _TError
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
export function useList<TData extends BaseRecord = BaseRecord, TError = HttpError>(optionsOrGetter: MaybeGetter<UseListOptions<TData, TError>> = {}) {
|
|
77
|
+
const adminContext = captureAdminContext();
|
|
77
78
|
const parsed = useParsed();
|
|
78
79
|
const adminOptions = getAdminOptions();
|
|
79
80
|
|
|
@@ -83,7 +84,7 @@ export function useList<TData extends BaseRecord = BaseRecord, TError = HttpErro
|
|
|
83
84
|
const query = createQuery<GetListResult<TData>, TError>(() => {
|
|
84
85
|
const opts = getOptions();
|
|
85
86
|
const resource = getResource();
|
|
86
|
-
const provider = getDataProviderForResource(resource, opts.dataProviderName);
|
|
87
|
+
const provider = adminContext.getDataProviderForResource(resource, opts.dataProviderName);
|
|
87
88
|
const queryOptions = opts.queryOptions;
|
|
88
89
|
const pagination = cloneQueryKeyPart(opts.pagination);
|
|
89
90
|
const sorters = cloneQueryKeyPart(opts.sorters);
|
|
@@ -129,7 +130,7 @@ export function useList<TData extends BaseRecord = BaseRecord, TError = HttpErro
|
|
|
129
130
|
const opts = getOptions();
|
|
130
131
|
return {
|
|
131
132
|
resource: getResource(),
|
|
132
|
-
liveProvider:
|
|
133
|
+
liveProvider: adminContext.liveProvider,
|
|
133
134
|
liveMode: opts.liveMode ?? adminOptions.liveMode,
|
|
134
135
|
onLiveEvent: (e: LiveEvent) => {
|
|
135
136
|
opts.onLiveEvent?.(e);
|
|
@@ -153,7 +154,7 @@ export function useList<TData extends BaseRecord = BaseRecord, TError = HttpErro
|
|
|
153
154
|
}
|
|
154
155
|
} else if (query.isError && query.errorUpdatedAt > lastErrorAt) {
|
|
155
156
|
lastErrorAt = query.errorUpdatedAt;
|
|
156
|
-
checkError(query.error);
|
|
157
|
+
checkError(query.error, adminContext);
|
|
157
158
|
fireErrorNotification(opts.errorNotification, 'Fetch failed', query.error, getResource());
|
|
158
159
|
}
|
|
159
160
|
});
|
|
@@ -178,6 +179,7 @@ export interface UseOneOptions<_TData extends BaseRecord = BaseRecord, _TError =
|
|
|
178
179
|
}
|
|
179
180
|
|
|
180
181
|
export function useOne<TData extends BaseRecord = BaseRecord, TError = HttpError>(optionsOrGetter: MaybeGetter<UseOneOptions<TData, TError>> = {}) {
|
|
182
|
+
const adminContext = captureAdminContext();
|
|
181
183
|
const parsed = useParsed();
|
|
182
184
|
const adminOptions = getAdminOptions();
|
|
183
185
|
|
|
@@ -189,7 +191,7 @@ export function useOne<TData extends BaseRecord = BaseRecord, TError = HttpError
|
|
|
189
191
|
const opts = getOptions();
|
|
190
192
|
const resource = getResource();
|
|
191
193
|
const id = getId();
|
|
192
|
-
const provider = getDataProviderForResource(resource, opts.dataProviderName);
|
|
194
|
+
const provider = adminContext.getDataProviderForResource(resource, opts.dataProviderName);
|
|
193
195
|
const queryOptions = opts.queryOptions;
|
|
194
196
|
|
|
195
197
|
return {
|
|
@@ -217,7 +219,7 @@ export function useOne<TData extends BaseRecord = BaseRecord, TError = HttpError
|
|
|
217
219
|
const resource = getResource();
|
|
218
220
|
return {
|
|
219
221
|
resource,
|
|
220
|
-
liveProvider:
|
|
222
|
+
liveProvider: adminContext.liveProvider,
|
|
221
223
|
liveMode: opts.liveMode ?? adminOptions.liveMode,
|
|
222
224
|
onLiveEvent: (e: LiveEvent) => {
|
|
223
225
|
opts.onLiveEvent?.(e);
|
|
@@ -240,7 +242,7 @@ export function useOne<TData extends BaseRecord = BaseRecord, TError = HttpError
|
|
|
240
242
|
}
|
|
241
243
|
} else if (query.isError && query.errorUpdatedAt > lastErrorAt) {
|
|
242
244
|
lastErrorAt = query.errorUpdatedAt;
|
|
243
|
-
checkError(query.error);
|
|
245
|
+
checkError(query.error, adminContext);
|
|
244
246
|
fireErrorNotification(opts.errorNotification, 'Fetch failed', query.error, getResource());
|
|
245
247
|
}
|
|
246
248
|
});
|
|
@@ -288,13 +290,14 @@ export interface UseManyOptions<_TData extends BaseRecord = BaseRecord, _TError
|
|
|
288
290
|
}
|
|
289
291
|
|
|
290
292
|
export function useMany<TData extends BaseRecord = BaseRecord, TError = HttpError>(optionsOrGetter: MaybeGetter<UseManyOptions<TData, TError>>) {
|
|
293
|
+
const adminContext = captureAdminContext();
|
|
291
294
|
const adminOptions = getAdminOptions();
|
|
292
295
|
const getOptions = () => typeof optionsOrGetter === 'function' ? optionsOrGetter() : optionsOrGetter;
|
|
293
296
|
|
|
294
297
|
const query = createQuery<GetManyResult<TData>, TError>(() => {
|
|
295
298
|
const opts = getOptions();
|
|
296
299
|
const { resource, ids, meta, dataProviderName, queryOptions } = opts;
|
|
297
|
-
const provider = getDataProviderForResource(resource, dataProviderName);
|
|
300
|
+
const provider = adminContext.getDataProviderForResource(resource, dataProviderName);
|
|
298
301
|
|
|
299
302
|
return {
|
|
300
303
|
queryKey: [dataProviderName, resource, 'many', ids, meta],
|
|
@@ -319,7 +322,7 @@ export function useMany<TData extends BaseRecord = BaseRecord, TError = HttpErro
|
|
|
319
322
|
const opts = getOptions();
|
|
320
323
|
return {
|
|
321
324
|
resource: opts.resource,
|
|
322
|
-
liveProvider:
|
|
325
|
+
liveProvider: adminContext.liveProvider,
|
|
323
326
|
liveMode: opts.liveMode ?? adminOptions.liveMode,
|
|
324
327
|
onLiveEvent: (e: LiveEvent) => {
|
|
325
328
|
opts.onLiveEvent?.(e);
|
|
@@ -342,7 +345,7 @@ export function useMany<TData extends BaseRecord = BaseRecord, TError = HttpErro
|
|
|
342
345
|
}
|
|
343
346
|
} else if (query.isError && query.errorUpdatedAt > lastErrorAt) {
|
|
344
347
|
lastErrorAt = query.errorUpdatedAt;
|
|
345
|
-
checkError(query.error);
|
|
348
|
+
checkError(query.error, adminContext);
|
|
346
349
|
fireErrorNotification(opts.errorNotification, 'Fetch failed', query.error, opts.resource);
|
|
347
350
|
}
|
|
348
351
|
});
|
|
@@ -351,5 +354,5 @@ export function useMany<TData extends BaseRecord = BaseRecord, TError = HttpErro
|
|
|
351
354
|
}
|
|
352
355
|
|
|
353
356
|
export function useApiUrl(dataProviderName?: string): string {
|
|
354
|
-
return getDataProvider(dataProviderName).getApiUrl();
|
|
357
|
+
return captureAdminContext().getDataProvider(dataProviderName).getApiUrl();
|
|
355
358
|
}
|
package/src/router.ts
CHANGED
|
@@ -67,14 +67,22 @@ export function matchRoute(
|
|
|
67
67
|
return null;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
/**
|
|
71
|
+
* Navigate with an explicit tree-scoped provider while preserving the same
|
|
72
|
+
* guards, path synchronization, and after hooks as the legacy active router.
|
|
73
|
+
*/
|
|
74
|
+
export async function navigateWithProvider(
|
|
75
|
+
provider: RouterProvider | undefined,
|
|
76
|
+
path: string,
|
|
77
|
+
options?: { replaceState?: boolean },
|
|
78
|
+
): Promise<void> {
|
|
79
|
+
const from = currentPathWithProvider(provider);
|
|
72
80
|
for (const guard of _beforeGuards) {
|
|
73
81
|
const allowed = await guard(path, from);
|
|
74
82
|
if (!allowed) return;
|
|
75
83
|
}
|
|
76
|
-
if (
|
|
77
|
-
|
|
84
|
+
if (provider) {
|
|
85
|
+
provider.go({ to: path, type: options?.replaceState ? 'replace' : 'push' });
|
|
78
86
|
} else if (typeof window !== 'undefined') {
|
|
79
87
|
window.location.hash = '#' + path.replace(/^#/, '');
|
|
80
88
|
}
|
|
@@ -84,19 +92,31 @@ export async function navigate(path: string, options?: { replaceState?: boolean
|
|
|
84
92
|
}
|
|
85
93
|
}
|
|
86
94
|
|
|
95
|
+
export async function navigate(path: string, options?: { replaceState?: boolean }): Promise<void> {
|
|
96
|
+
await navigateWithProvider(_routerProvider, path, options);
|
|
97
|
+
}
|
|
98
|
+
|
|
87
99
|
/**
|
|
88
100
|
* Format a path into an href suitable for <a> tags.
|
|
89
101
|
*/
|
|
90
102
|
export function formatLink(path: string): string {
|
|
91
|
-
|
|
92
|
-
|
|
103
|
+
return formatLinkWithProvider(_routerProvider, path);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function formatLinkWithProvider(provider: RouterProvider | undefined, path: string): string {
|
|
107
|
+
if (provider?.formatLink) {
|
|
108
|
+
return provider.formatLink(path);
|
|
93
109
|
}
|
|
94
110
|
return '#' + path.replace(/^#/, '');
|
|
95
111
|
}
|
|
96
112
|
|
|
97
113
|
export function currentPath(): string {
|
|
98
|
-
|
|
99
|
-
|
|
114
|
+
return currentPathWithProvider(_routerProvider);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function currentPathWithProvider(provider: RouterProvider | undefined): string {
|
|
118
|
+
if (provider) {
|
|
119
|
+
const parsed = provider.parse();
|
|
100
120
|
const pathname = parsed.pathname || '/';
|
|
101
121
|
let qs = '';
|
|
102
122
|
if (parsed.params && Object.keys(parsed.params).length > 0) {
|