@svadmin/lite 0.3.27 → 0.6.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/README.md +106 -1
- package/dist/compatibility.d.ts +23 -0
- package/dist/compatibility.js +95 -0
- package/dist/components/LiteForm.svelte +1 -1
- package/dist/components/LiteForm.svelte.d.ts +1 -1
- package/dist/components/LiteShowField.svelte +32 -22
- package/dist/components/LiteShowField.svelte.d.ts +3 -2
- package/dist/components/LiteTable.svelte +90 -37
- package/dist/components/LiteTable.svelte.d.ts +3 -2
- package/dist/components/compatibility/LiteCapabilityBoundary.svelte +51 -0
- package/dist/components/compatibility/LiteCapabilityBoundary.svelte.d.ts +13 -0
- package/dist/components/compatibility/LiteClipboardFallback.svelte +16 -0
- package/dist/components/compatibility/LiteClipboardFallback.svelte.d.ts +8 -0
- package/dist/components/compatibility/LiteComputeFallback.svelte +50 -0
- package/dist/components/compatibility/LiteComputeFallback.svelte.d.ts +14 -0
- package/dist/components/compatibility/LiteDirectoryUpload.svelte +44 -0
- package/dist/components/compatibility/LiteDirectoryUpload.svelte.d.ts +11 -0
- package/dist/components/compatibility/LiteOrderedList.svelte +47 -0
- package/dist/components/compatibility/LiteOrderedList.svelte.d.ts +14 -0
- package/dist/components/compatibility/LiteRealtimeStatus.svelte +43 -0
- package/dist/components/compatibility/LiteRealtimeStatus.svelte.d.ts +11 -0
- package/dist/components/compatibility/LiteVisualFallback.svelte +80 -0
- package/dist/components/compatibility/LiteVisualFallback.svelte.d.ts +18 -0
- package/dist/components/compatibility/index.d.ts +7 -0
- package/dist/components/compatibility/index.js +7 -0
- package/dist/components/pages/LiteCreatePage.svelte +15 -6
- package/dist/components/pages/LiteCreatePage.svelte.d.ts +1 -1
- package/dist/components/pages/LiteEditPage.svelte +18 -9
- package/dist/components/pages/LiteEditPage.svelte.d.ts +1 -1
- package/dist/components/pages/LiteListPage.svelte +86 -16
- package/dist/components/pages/LiteListPage.svelte.d.ts +4 -2
- package/dist/components/pages/LiteShowPage.svelte +19 -10
- package/dist/components/pages/LiteShowPage.svelte.d.ts +1 -1
- package/dist/index.d.ts +7 -3
- package/dist/index.js +8 -3
- package/dist/lite.css +269 -0
- package/dist/schema-generator.d.ts +49 -13
- package/dist/schema-generator.js +381 -199
- package/dist/server-adapter.d.ts +25 -1
- package/dist/server-adapter.js +132 -33
- package/dist/value-normalization.d.ts +1 -0
- package/dist/value-normalization.js +18 -0
- package/package.json +10 -10
package/dist/server-adapter.d.ts
CHANGED
|
@@ -24,6 +24,7 @@ export interface ListLoaderResult {
|
|
|
24
24
|
currentSort?: string;
|
|
25
25
|
currentOrder?: 'asc' | 'desc';
|
|
26
26
|
currentSearch?: string;
|
|
27
|
+
currentFilters?: Record<string, string>;
|
|
27
28
|
resource: ResourceDefinition;
|
|
28
29
|
}
|
|
29
30
|
/**
|
|
@@ -92,6 +93,13 @@ export declare function createCrudActions(dp: DataProvider, resource: ResourceDe
|
|
|
92
93
|
success: boolean;
|
|
93
94
|
error?: undefined;
|
|
94
95
|
}>;
|
|
96
|
+
batchDelete: ({ request }: RequestEvent) => Promise<{
|
|
97
|
+
success: boolean;
|
|
98
|
+
error: string;
|
|
99
|
+
} | {
|
|
100
|
+
success: boolean;
|
|
101
|
+
error?: undefined;
|
|
102
|
+
}>;
|
|
95
103
|
};
|
|
96
104
|
/**
|
|
97
105
|
* Creates a SvelteKit server hook that checks auth via AuthProvider
|
|
@@ -150,11 +158,27 @@ export declare function createAuthActions(authProvider: AuthProvider): {
|
|
|
150
158
|
* This helper does not imply that Svelte 5 or the consuming app supports IE11.
|
|
151
159
|
*/
|
|
152
160
|
export declare function isLegacyBrowser(userAgent: string): boolean;
|
|
161
|
+
export interface LegacyRedirectOptions {
|
|
162
|
+
/** Route prefix that contains the standalone SSR Lite application. */
|
|
163
|
+
litePrefix?: string;
|
|
164
|
+
/** Optional route prefix occupied by the modern SPA, for example `/admin`. */
|
|
165
|
+
spaPrefix?: string;
|
|
166
|
+
/** Paths that must remain outside automatic legacy routing. */
|
|
167
|
+
exclude?: string[];
|
|
168
|
+
/** Override the SPA-to-Lite path mapping without changing SPA code. */
|
|
169
|
+
mapPath?: (pathname: string) => string;
|
|
170
|
+
status?: 302 | 307;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Computes a legacy redirect without importing or modifying the modern SPA.
|
|
174
|
+
* This pure helper can be used from non-SvelteKit server middleware.
|
|
175
|
+
*/
|
|
176
|
+
export declare function getLegacyRedirectLocation(request: Pick<Request, 'method' | 'headers'>, url: URL, options?: string | LegacyRedirectOptions): string | undefined;
|
|
153
177
|
/**
|
|
154
178
|
* Creates an opt-in SvelteKit hook that redirects detected IE11 user agents.
|
|
155
179
|
* Consumers remain responsible for their own transpilation and browser support.
|
|
156
180
|
*/
|
|
157
|
-
export declare function createLegacyRedirectHook(
|
|
181
|
+
export declare function createLegacyRedirectHook(options?: string | LegacyRedirectOptions): ({ event, resolve }: {
|
|
158
182
|
event: RequestEvent;
|
|
159
183
|
resolve: (event: RequestEvent) => Promise<Response>;
|
|
160
184
|
}) => Promise<Response>;
|
package/dist/server-adapter.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { redirect, isRedirect } from '@sveltejs/kit';
|
|
2
|
-
import {
|
|
2
|
+
import { resourceToTypeBoxSchema } from './schema-generator';
|
|
3
3
|
import { parseExplicitBoolean } from './value-normalization';
|
|
4
4
|
function listRequestState(url, resource) {
|
|
5
5
|
const requestedPage = Number(url.searchParams.get('page'));
|
|
@@ -19,19 +19,45 @@ function listRequestState(url, resource) {
|
|
|
19
19
|
: resource.defaultSort?.order ?? 'asc';
|
|
20
20
|
return { page, pageSize, sort, order, search: url.searchParams.get('q') ?? undefined };
|
|
21
21
|
}
|
|
22
|
-
function
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
22
|
+
function listRequestFilterValues(resource, url) {
|
|
23
|
+
const values = {};
|
|
24
|
+
for (const field of resource.fields) {
|
|
25
|
+
const raw = url.searchParams.get(`filter_${field.key}`) ?? (field.key !== "q" && field.key !== "sort" && field.key !== "order" && field.key !== "page" ? url.searchParams.get(field.key) : null);
|
|
26
|
+
if (raw != null && raw.trim() !== "") {
|
|
27
|
+
values[field.key] = raw.trim();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return values;
|
|
31
|
+
}
|
|
32
|
+
function listRequestFilters(resource, url) {
|
|
33
|
+
const filters = [];
|
|
34
|
+
const search = url.searchParams.get('q') ?? undefined;
|
|
35
|
+
if (search) {
|
|
36
|
+
const searchFilters = resource.fields
|
|
37
|
+
.filter((field) => field.searchable)
|
|
38
|
+
.map((field) => ({
|
|
39
|
+
field: field.key,
|
|
40
|
+
operator: 'contains',
|
|
41
|
+
value: search,
|
|
42
|
+
}));
|
|
43
|
+
if (searchFilters.length === 1) {
|
|
44
|
+
filters.push(searchFilters[0]);
|
|
45
|
+
}
|
|
46
|
+
else if (searchFilters.length > 1) {
|
|
47
|
+
filters.push({ operator: 'or', value: searchFilters });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
for (const field of resource.fields) {
|
|
51
|
+
const rawVal = url.searchParams.get(`filter_${field.key}`) ?? (field.key !== 'q' && field.key !== 'sort' && field.key !== 'order' && field.key !== 'page' ? url.searchParams.get(field.key) : null);
|
|
52
|
+
if (rawVal != null && rawVal.trim() !== '') {
|
|
53
|
+
filters.push({
|
|
54
|
+
field: field.key,
|
|
55
|
+
operator: 'eq',
|
|
56
|
+
value: field.type === 'number' && !Number.isNaN(Number(rawVal)) ? Number(rawVal) : rawVal,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return filters;
|
|
35
61
|
}
|
|
36
62
|
/**
|
|
37
63
|
* Creates a SvelteKit `load` function that fetches a resource list
|
|
@@ -45,7 +71,7 @@ export function createListLoader(dp, resource) {
|
|
|
45
71
|
resource: resource.name,
|
|
46
72
|
pagination: { current: page, pageSize },
|
|
47
73
|
sorters,
|
|
48
|
-
filters:
|
|
74
|
+
filters: listRequestFilters(resource, url),
|
|
49
75
|
});
|
|
50
76
|
return {
|
|
51
77
|
records: listResponse.data,
|
|
@@ -60,6 +86,7 @@ export function createListLoader(dp, resource) {
|
|
|
60
86
|
currentSort: sort,
|
|
61
87
|
currentOrder: order,
|
|
62
88
|
currentSearch: search,
|
|
89
|
+
currentFilters: listRequestFilterValues(resource, url),
|
|
63
90
|
resource,
|
|
64
91
|
};
|
|
65
92
|
};
|
|
@@ -155,6 +182,32 @@ export function createCrudActions(dp, resource) {
|
|
|
155
182
|
return { success: false, error: 'Delete failed' };
|
|
156
183
|
}
|
|
157
184
|
},
|
|
185
|
+
batchDelete: async ({ request }) => {
|
|
186
|
+
if (resource.canDelete === false) {
|
|
187
|
+
return { success: false, error: 'Delete is disabled for this resource' };
|
|
188
|
+
}
|
|
189
|
+
const formData = await request.formData();
|
|
190
|
+
const ids = formData.getAll('ids').map(v => String(v).trim()).filter(Boolean);
|
|
191
|
+
if (ids.length === 0)
|
|
192
|
+
return { success: false, error: 'No records selected' };
|
|
193
|
+
const redirectTo = toSafeLocalRedirect(formData.get('redirect'));
|
|
194
|
+
try {
|
|
195
|
+
if (dp.deleteMany) {
|
|
196
|
+
await dp.deleteMany({ resource: resource.name, ids });
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
await Promise.all(ids.map(id => dp.deleteOne({ resource: resource.name, id })));
|
|
200
|
+
}
|
|
201
|
+
if (redirectTo)
|
|
202
|
+
throw redirect(303, redirectTo);
|
|
203
|
+
return { success: true };
|
|
204
|
+
}
|
|
205
|
+
catch (caughtError) {
|
|
206
|
+
if (isRedirect(caughtError))
|
|
207
|
+
throw caughtError;
|
|
208
|
+
return { success: false, error: 'Batch delete failed' };
|
|
209
|
+
}
|
|
210
|
+
},
|
|
158
211
|
};
|
|
159
212
|
}
|
|
160
213
|
// ─── Auth Helpers ─────────────────────────────────────────────
|
|
@@ -301,28 +354,74 @@ export function createAuthActions(authProvider) {
|
|
|
301
354
|
export function isLegacyBrowser(userAgent) {
|
|
302
355
|
return /MSIE|Trident|rv:11/.test(userAgent);
|
|
303
356
|
}
|
|
357
|
+
function normalizeRoutePrefix(prefix) {
|
|
358
|
+
const segments = prefix.split('/').filter(Boolean);
|
|
359
|
+
return segments.length > 0 ? `/${segments.join('/')}` : '/';
|
|
360
|
+
}
|
|
361
|
+
function pathWithinPrefix(pathname, prefix) {
|
|
362
|
+
return prefix === '/' || pathname === prefix || pathname.startsWith(`${prefix}/`);
|
|
363
|
+
}
|
|
364
|
+
function redirectOptions(options) {
|
|
365
|
+
const configured = typeof options === 'string' ? { litePrefix: options } : options;
|
|
366
|
+
return {
|
|
367
|
+
litePrefix: normalizeRoutePrefix(configured.litePrefix ?? '/lite'),
|
|
368
|
+
spaPrefix: normalizeRoutePrefix(configured.spaPrefix ?? '/'),
|
|
369
|
+
exclude: (configured.exclude ?? []).map(normalizeRoutePrefix),
|
|
370
|
+
mapPath: configured.mapPath,
|
|
371
|
+
status: configured.status ?? 302,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Computes a legacy redirect without importing or modifying the modern SPA.
|
|
376
|
+
* This pure helper can be used from non-SvelteKit server middleware.
|
|
377
|
+
*/
|
|
378
|
+
export function getLegacyRedirectLocation(request, url, options = '/lite') {
|
|
379
|
+
const configured = redirectOptions(options);
|
|
380
|
+
const userAgent = request.headers.get('user-agent') ?? '';
|
|
381
|
+
const acceptsHtml = request.headers.get('accept')?.includes('text/html') === true;
|
|
382
|
+
const isDocumentRequest = (request.method === 'GET' || request.method === 'HEAD') && acceptsHtml;
|
|
383
|
+
if (!isLegacyBrowser(userAgent) || !isDocumentRequest)
|
|
384
|
+
return undefined;
|
|
385
|
+
if (pathWithinPrefix(url.pathname, configured.litePrefix))
|
|
386
|
+
return undefined;
|
|
387
|
+
if (configured.exclude.some((prefix) => pathWithinPrefix(url.pathname, prefix)))
|
|
388
|
+
return undefined;
|
|
389
|
+
if (!configured.mapPath
|
|
390
|
+
&& configured.spaPrefix !== '/'
|
|
391
|
+
&& !pathWithinPrefix(url.pathname, configured.spaPrefix))
|
|
392
|
+
return undefined;
|
|
393
|
+
let mappedPath;
|
|
394
|
+
if (configured.mapPath) {
|
|
395
|
+
mappedPath = configured.mapPath(url.pathname);
|
|
396
|
+
}
|
|
397
|
+
else if (configured.spaPrefix !== '/' && pathWithinPrefix(url.pathname, configured.spaPrefix)) {
|
|
398
|
+
mappedPath = url.pathname.slice(configured.spaPrefix.length) || '/';
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
mappedPath = url.pathname;
|
|
402
|
+
}
|
|
403
|
+
const normalizedMappedPath = mappedPath.startsWith('/') ? mappedPath : `/${mappedPath}`;
|
|
404
|
+
const location = configured.litePrefix === '/'
|
|
405
|
+
? normalizedMappedPath
|
|
406
|
+
: `${configured.litePrefix}${normalizedMappedPath === '/' ? '' : normalizedMappedPath}`;
|
|
407
|
+
return `${location}${url.search}`;
|
|
408
|
+
}
|
|
304
409
|
/**
|
|
305
410
|
* Creates an opt-in SvelteKit hook that redirects detected IE11 user agents.
|
|
306
411
|
* Consumers remain responsible for their own transpilation and browser support.
|
|
307
412
|
*/
|
|
308
|
-
export function createLegacyRedirectHook(
|
|
309
|
-
const
|
|
310
|
-
const normalizedPrefix = prefixSegments.length > 0 ? `/${prefixSegments.join('/')}` : '/';
|
|
413
|
+
export function createLegacyRedirectHook(options = '/lite') {
|
|
414
|
+
const configured = redirectOptions(options);
|
|
311
415
|
return async ({ event, resolve }) => {
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
const isDocumentRequest = (event.request.method === 'GET' || event.request.method === 'HEAD')
|
|
315
|
-
&& acceptsHtml;
|
|
316
|
-
const isWithinLite = normalizedPrefix === '/'
|
|
317
|
-
|| event.url.pathname === normalizedPrefix
|
|
318
|
-
|| event.url.pathname.startsWith(`${normalizedPrefix}/`);
|
|
319
|
-
if (isLegacyBrowser(ua) && isDocumentRequest && !isWithinLite) {
|
|
320
|
-
const targetPath = normalizedPrefix === '/'
|
|
321
|
-
? event.url.pathname
|
|
322
|
-
: `${normalizedPrefix}${event.url.pathname}`;
|
|
416
|
+
const location = getLegacyRedirectLocation(event.request, event.url, configured);
|
|
417
|
+
if (location) {
|
|
323
418
|
return new Response(null, {
|
|
324
|
-
status:
|
|
325
|
-
headers: {
|
|
419
|
+
status: configured.status,
|
|
420
|
+
headers: {
|
|
421
|
+
Location: location,
|
|
422
|
+
'Cache-Control': 'private, no-store',
|
|
423
|
+
Vary: 'User-Agent',
|
|
424
|
+
},
|
|
326
425
|
});
|
|
327
426
|
}
|
|
328
427
|
return resolve(event);
|
|
@@ -374,7 +473,7 @@ function formatValidationErrors(issues) {
|
|
|
374
473
|
return errors;
|
|
375
474
|
}
|
|
376
475
|
function validateFormVariables(resource, mode, values) {
|
|
377
|
-
const result =
|
|
476
|
+
const result = resourceToTypeBoxSchema(resource, mode).safeParse(values);
|
|
378
477
|
if (result.success)
|
|
379
478
|
return { success: true, data: result.data };
|
|
380
479
|
return {
|
|
@@ -471,7 +570,7 @@ function formDataToObject(formData, fields) {
|
|
|
471
570
|
obj[field.key] = strRaw ? JSON.parse(strRaw) : null;
|
|
472
571
|
}
|
|
473
572
|
catch {
|
|
474
|
-
obj[field.key] = strRaw; // Let
|
|
573
|
+
obj[field.key] = strRaw; // Let TypeBox handle validation errors
|
|
475
574
|
}
|
|
476
575
|
break;
|
|
477
576
|
default:
|
|
@@ -19,3 +19,21 @@ export function parseExplicitBoolean(rawBoolean) {
|
|
|
19
19
|
export function isExplicitBooleanTrue(rawBoolean) {
|
|
20
20
|
return parseExplicitBoolean(rawBoolean) === true;
|
|
21
21
|
}
|
|
22
|
+
export function getStatusBadgeClass(value) {
|
|
23
|
+
if (typeof value !== "string")
|
|
24
|
+
return "lite-badge";
|
|
25
|
+
const v = value.toLowerCase().trim();
|
|
26
|
+
if (["active", "completed", "published", "delivered", "paid", "approved", "success", "in_stock", "healthy"].includes(v)) {
|
|
27
|
+
return "lite-badge lite-badge-success";
|
|
28
|
+
}
|
|
29
|
+
if (["draft", "pending", "processing", "in_transit", "review", "waiting", "warning", "low_stock"].includes(v)) {
|
|
30
|
+
return "lite-badge lite-badge-warning";
|
|
31
|
+
}
|
|
32
|
+
if (["cancelled", "rejected", "failed", "suspended", "inactive", "banned", "error", "out_of_stock", "closed"].includes(v)) {
|
|
33
|
+
return "lite-badge lite-badge-danger";
|
|
34
|
+
}
|
|
35
|
+
if (["shipped", "submitted", "open", "new", "admin", "verified"].includes(v)) {
|
|
36
|
+
return "lite-badge lite-badge-info";
|
|
37
|
+
}
|
|
38
|
+
return "lite-badge";
|
|
39
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@svadmin/lite",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "SSR-first lightweight admin UI for @svadmin with optional progressive enhancement",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": [
|
|
@@ -48,28 +48,28 @@
|
|
|
48
48
|
}
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
|
-
"svelte": "^5.56.
|
|
52
|
-
"@svadmin/core": ">=0.34.2 <0.
|
|
53
|
-
"@sveltejs/kit": "^2.70.
|
|
51
|
+
"svelte": "^5.56.10",
|
|
52
|
+
"@svadmin/core": ">=0.34.2 <0.46.0",
|
|
53
|
+
"@sveltejs/kit": "^2.70.3"
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
|
-
"@lucide/svelte": "^1.
|
|
57
|
-
"
|
|
56
|
+
"@lucide/svelte": "^1.35.0",
|
|
57
|
+
"@sinclair/typebox": "^0.34.52"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@types/bun": "^1.4.0",
|
|
61
|
-
"@happy-dom/global-registrator": "^20.11.
|
|
61
|
+
"@happy-dom/global-registrator": "^20.11.12",
|
|
62
62
|
"@sveltejs/vite-plugin-svelte": "^7.3.0",
|
|
63
63
|
"@testing-library/dom": "^10.4.1",
|
|
64
64
|
"@testing-library/svelte": "^5.4.2",
|
|
65
|
-
"happy-dom": "^20.11.
|
|
66
|
-
"vitest": "^4.1.
|
|
65
|
+
"happy-dom": "^20.11.12",
|
|
66
|
+
"vitest": "^4.1.11"
|
|
67
67
|
},
|
|
68
68
|
"scripts": {
|
|
69
69
|
"build": "node ../../node_modules/@sveltejs/package/svelte-package.js -i src -o dist && bun ../../scripts/copy-package-assets.ts static/enhance.js dist/enhance.js",
|
|
70
70
|
"test": "bun run test:components && bun run test:server && bun run test:security",
|
|
71
71
|
"test:components": "vitest run --config ./vitest.config.ts",
|
|
72
|
-
"test:server": "bun test ./src/schema-generator.test.ts ./src/server-adapter.test.ts ./src/fragment-id.test.ts ./src/ie11-ssr-contract.test.ts",
|
|
72
|
+
"test:server": "bun test ./src/schema-generator.test.ts ./src/server-adapter.test.ts ./src/fragment-id.test.ts ./src/compatibility.test.ts ./src/ie11-ssr-contract.test.ts",
|
|
73
73
|
"test:security": "vitest run --config ./src/security.test.config.ts"
|
|
74
74
|
},
|
|
75
75
|
"license": "MIT",
|