@svadmin/lite 0.3.1 → 0.3.3

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svadmin/lite",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "SSR-compatible lightweight admin UI for @svadmin — zero client-side JS, works in IE11",
5
5
  "type": "module",
6
6
  "files": [
@@ -21,7 +21,7 @@
21
21
  },
22
22
  "peerDependencies": {
23
23
  "svelte": "^5.0.0",
24
- "@svadmin/core": "^0.21.1",
24
+ "@svadmin/core": "^0.22.1",
25
25
  "@sveltejs/kit": "^2.0.0"
26
26
  },
27
27
  "dependencies": {
@@ -79,11 +79,11 @@
79
79
  {#if totalPages > 1}
80
80
  <div class="lite-pagination" style="margin-top:12px;">
81
81
  {#if page > 1}
82
- <a href="{basePath}page={page - 1}" class="lite-btn lite-btn-sm">&laquo; Prev</a>
82
+ <a href={`${basePath}page={page - 1}`} class="lite-btn lite-btn-sm">&laquo; Prev</a>
83
83
  {/if}
84
84
  <span style="padding:0 12px;font-size:14px;color:#64748b;">Page {page} / {totalPages} ({total} total)</span>
85
85
  {#if page < totalPages}
86
- <a href="{basePath}page={page + 1}" class="lite-btn lite-btn-sm">Next &raquo;</a>
86
+ <a href={`${basePath}page={page + 1}`} class="lite-btn lite-btn-sm">Next &raquo;</a>
87
87
  {/if}
88
88
  </div>
89
89
  {/if}
@@ -81,7 +81,7 @@
81
81
  {:else}
82
82
  {#each menuResources as res}
83
83
  <a
84
- href="{basePath}/{res.name}"
84
+ href={`${basePath}/${res.name}`}
85
85
  class={res.name === currentResource ? 'active' : ''}
86
86
  >
87
87
  {res.label}
@@ -91,7 +91,7 @@
91
91
  {#if userName}
92
92
  <div style="position:absolute;bottom:0;left:0;right:0;padding:12px 16px;border-top:1px solid #334155;font-size:12px;color:#94a3b8;">
93
93
  {userName}
94
- <form method="POST" action="{basePath}/login?/logout" style="display:inline;margin-left:8px;">
94
+ <form method="POST" action={`${basePath}/login?/logout`} style="display:inline;margin-left:8px;">
95
95
  <button type="submit" class="lite-btn lite-btn-sm" style="color:#94a3b8;border-color:#475569;background:transparent;">Logout</button>
96
96
  </form>
97
97
  </div>
@@ -33,11 +33,11 @@
33
33
  <h1>{resource.label} #{id}</h1>
34
34
  <div>
35
35
  {#if canEdit}
36
- <a href="{basePath}/{resource.name}/edit/{id}" class="lite-btn lite-btn-primary">
36
+ <a href={`${basePath}/${resource.name}/edit/${id}`} class="lite-btn lite-btn-primary">
37
37
  {t('common.edit') || 'Edit'}
38
38
  </a>
39
39
  {/if}
40
- <a href="{basePath}/{resource.name}" class="lite-btn" style="margin-left:8px;">
40
+ <a href={`${basePath}/${resource.name}`} class="lite-btn" style="margin-left:8px;">
41
41
  {t('common.backToList') || 'Back to List'}
42
42
  </a>
43
43
  </div>
@@ -103,7 +103,7 @@
103
103
  {#if canEdit || canDelete}
104
104
  <td class="actions">
105
105
  {#if canEdit}
106
- <a href="{basePath}/{resource.name}/edit/{id}" class="lite-btn lite-btn-sm">{t('common.edit') || 'Edit'}</a>
106
+ <a href={`${basePath}/${resource.name}/edit/${id}`} class="lite-btn lite-btn-sm">{t('common.edit') || 'Edit'}</a>
107
107
  {/if}
108
108
  {#if canDelete}
109
109
  <!-- Delete uses <details> for no-JS confirmation -->
@@ -26,7 +26,7 @@
26
26
  const href = $derived(
27
27
  mode === 'create'
28
28
  ? `${basePath}/${resource.name}/create`
29
- : `${basePath}/${resource.name}/${recordId}/edit`
29
+ : `${basePath}/${resource.name}/edit/${recordId}`
30
30
  );
31
31
 
32
32
  const buttonLabel = $derived(
@@ -27,7 +27,7 @@
27
27
  const href = $derived(
28
28
  mode === 'create'
29
29
  ? `${basePath}/${resource.name}/create`
30
- : `${basePath}/${resource.name}/${recordId}/edit`
30
+ : `${basePath}/${resource.name}/edit/${recordId}`
31
31
  );
32
32
 
33
33
  const buttonLabel = $derived(
@@ -22,7 +22,7 @@
22
22
  </script>
23
23
 
24
24
  <a
25
- href="{basePath}/{resource}/create?cloneId={recordItemId}"
25
+ href={`${basePath}/${resource}/clone/${recordItemId}`}
26
26
  class="lite-btn {size === 'sm' ? 'lite-btn-sm' : ''} {className}"
27
27
  title={t('common.clone') || 'Clone'}
28
28
  >
@@ -20,7 +20,7 @@
20
20
  </script>
21
21
 
22
22
  <a
23
- href="{basePath}/{resource}/create"
23
+ href={`${basePath}/${resource}/create`}
24
24
  class="lite-btn lite-btn-primary {size === 'sm' ? 'lite-btn-sm' : ''} {className}"
25
25
  title={t('common.create') || 'Create'}
26
26
  >
@@ -37,7 +37,7 @@
37
37
  <p style="margin: 0 0 8px; font-size: 13px; color: #111827;">
38
38
  {t('common.areYouSure') || 'Are you sure?'}
39
39
  </p>
40
- <form method="POST" action="?/{resource}_delete" style="display:inline-flex; gap: 8px;">
40
+ <form method="POST" action="?/delete" style="display:inline-flex; gap: 8px;">
41
41
  <input type="hidden" name="id" value={String(recordItemId)} />
42
42
  {#if redirectUrl}
43
43
  <input type="hidden" name="redirect" value={redirectUrl} />
@@ -22,7 +22,7 @@
22
22
  </script>
23
23
 
24
24
  <a
25
- href="{basePath}/{resource}/edit/{recordItemId}"
25
+ href={`${basePath}/${resource}/edit/${recordItemId}`}
26
26
  class="lite-btn {size === 'sm' ? 'lite-btn-sm' : ''} {className}"
27
27
  title={t('common.edit') || 'Edit'}
28
28
  >
@@ -31,7 +31,7 @@
31
31
  </summary>
32
32
  <div class="lite-confirm-panel">
33
33
  <p style="margin: 0 0 8px; font-size: 13px;">{t('common.importData') || 'Import data (CSV/JSON)'}</p>
34
- <form method="POST" action="?/{resource}_import" enctype="multipart/form-data" style="display:flex; flex-direction: column; gap: 8px;">
34
+ <form method="POST" action="?/${resource}_import" enctype="multipart/form-data" style="display:flex; flex-direction: column; gap: 8px;">
35
35
  <input type="file" name="file" accept=".csv,.json" required style="font-size: 13px;" />
36
36
  <div style="display:flex; gap: 8px; justify-content: flex-end;">
37
37
  <button
@@ -20,7 +20,7 @@
20
20
  </script>
21
21
 
22
22
  <a
23
- href="{basePath}/{resource}"
23
+ href={`${basePath}/${resource}`}
24
24
  class="lite-btn {size === 'sm' ? 'lite-btn-sm' : ''} {className}"
25
25
  title={t('common.list') || 'List'}
26
26
  >
@@ -16,7 +16,7 @@
16
16
  </script>
17
17
 
18
18
  <a
19
- href="?"
19
+ href=""
20
20
  class="lite-btn {size === 'sm' ? 'lite-btn-sm' : ''} {className}"
21
21
  title={t('common.refresh') || 'Refresh'}
22
22
  >
@@ -22,7 +22,7 @@
22
22
  </script>
23
23
 
24
24
  <a
25
- href="{basePath}/{resource}/show/{recordItemId}"
25
+ href={`${basePath}/${resource}/show/${recordItemId}`}
26
26
  class="lite-btn {size === 'sm' ? 'lite-btn-sm' : ''} {className}"
27
27
  title={t('common.show') || 'Show'}
28
28
  >
@@ -75,7 +75,7 @@
75
75
  {:else}
76
76
  {#each menuResources as res}
77
77
  <a
78
- href="{basePath}/{res.name}"
78
+ href={`${basePath}/${res.name}`}
79
79
  class={res.name === currentResource ? 'active' : ''}
80
80
  >
81
81
  {res.label ?? res.name}
@@ -85,7 +85,7 @@
85
85
  {#if userName}
86
86
  <div style="position:absolute;bottom:0;left:0;right:0;padding:12px 16px;border-top:1px solid #334155;font-size:12px;color:#94a3b8;">
87
87
  {userName}
88
- <form method="POST" action="{basePath}/login?/logout" style="display:inline;margin-left:8px;">
88
+ <form method="POST" action={`${basePath}/login?/logout`} style="display:inline;margin-left:8px;">
89
89
  <button type="submit" class="lite-btn lite-btn-sm" style="color:#94a3b8;border-color:#475569;background:transparent;padding:2px 8px;">Logout</button>
90
90
  </form>
91
91
  </div>
@@ -33,7 +33,7 @@
33
33
  {resource}
34
34
  {errors}
35
35
  {values}
36
- action="?/{resource.name}_create"
37
- cancelUrl="{basePath}/{resource.name}"
36
+ action="?/create"
37
+ cancelUrl={`${basePath}/${resource.name}`}
38
38
  />
39
39
  </div>
@@ -37,7 +37,7 @@
37
37
  {/if}
38
38
  <LiteListButton resource={resource.name} {basePath} />
39
39
  {#if canDelete}
40
- <LiteDeleteButton resource={resource.name} recordItemId={idStr} redirectUrl="{basePath}/{resource.name}" {basePath} />
40
+ <LiteDeleteButton resource={resource.name} recordItemId={idStr} redirectUrl={`${basePath}/${resource.name}`} {basePath} />
41
41
  {/if}
42
42
  </div>
43
43
  </div>
@@ -48,7 +48,7 @@
48
48
  {resource}
49
49
  {errors}
50
50
  values={record}
51
- action="?/{resource.name}_update"
52
- cancelUrl="{basePath}/{resource.name}"
51
+ action="?/update"
52
+ cancelUrl={`${basePath}/${resource.name}`}
53
53
  />
54
54
  </div>
@@ -70,6 +70,11 @@
70
70
  <LitePagination
71
71
  page={pagination.page}
72
72
  totalPages={Math.ceil(total / pagination.perPage)}
73
+ preserveParams={{
74
+ ...(currentSort ? { sort: currentSort } : {}),
75
+ ...(currentOrder ? { order: currentOrder } : {}),
76
+ ...(currentSearch ? { q: currentSearch } : {})
77
+ }}
73
78
  />
74
79
  {/if}
75
80
  </div>
@@ -39,7 +39,7 @@
39
39
  {/if}
40
40
  <LiteListButton resource={resource.name} {basePath} />
41
41
  {#if canDelete}
42
- <LiteDeleteButton resource={resource.name} recordItemId={idStr} redirectUrl="{basePath}/{resource.name}" {basePath} />
42
+ <LiteDeleteButton resource={resource.name} recordItemId={idStr} redirectUrl={`${basePath}/${resource.name}`} {basePath} />
43
43
  {/if}
44
44
  </div>
45
45
  </div>
@@ -43,6 +43,8 @@ function fieldToZod(field: FieldDefinition): z.ZodTypeAny {
43
43
  }
44
44
  break;
45
45
  case 'multiselect':
46
+ schema = z.array(z.string()).default([]);
47
+ break;
46
48
  case 'tags':
47
49
  schema = z.string().transform((v: string) => v ? v.split(',').map((s: string) => s.trim()) : []);
48
50
  break;
@@ -9,7 +9,7 @@ import type {
9
9
  ResourceDefinition, FieldDefinition,
10
10
  Sort, Filter,
11
11
  } from '@svadmin/core';
12
- import type { RequestEvent } from '@sveltejs/kit';
12
+ import { fail, redirect, isRedirect, type RequestEvent } from '@sveltejs/kit';
13
13
 
14
14
  // ─── List Loader ──────────────────────────────────────────────
15
15
 
@@ -107,6 +107,7 @@ export function createCrudActions(
107
107
  const result = await dp.create({ resource: resource.name, variables });
108
108
  return { success: true, id: (result.data as Record<string, unknown>)[pk] };
109
109
  } catch (e) {
110
+ if (isRedirect(e)) throw e;
110
111
  return { success: false, error: (e as Error).message, values: variables };
111
112
  }
112
113
  },
@@ -120,6 +121,7 @@ export function createCrudActions(
120
121
  await dp.update({ resource: resource.name, id, variables });
121
122
  return { success: true };
122
123
  } catch (e) {
124
+ if (isRedirect(e)) throw e;
123
125
  return { success: false, error: (e as Error).message, values: variables };
124
126
  }
125
127
  },
@@ -127,10 +129,13 @@ export function createCrudActions(
127
129
  delete: async ({ request }: RequestEvent) => {
128
130
  const formData = await request.formData();
129
131
  const id = formData.get('id') as string;
132
+ const redirectTo = formData.get('redirect') as string | undefined;
130
133
  try {
131
134
  await dp.deleteOne({ resource: resource.name, id });
135
+ if (redirectTo) throw redirect(303, redirectTo);
132
136
  return { success: true };
133
137
  } catch (e) {
138
+ if (isRedirect(e)) throw e;
134
139
  return { success: false, error: (e as Error).message };
135
140
  }
136
141
  },
@@ -153,21 +158,31 @@ export function createAuthGuard(
153
158
  return resolve(event);
154
159
  }
155
160
 
161
+ // Local Lite Session Verify
162
+ const session = event.cookies.get('svadmin-session');
163
+ if (!session) {
164
+ return new Response(null, {
165
+ status: 302,
166
+ headers: { Location: loginPath },
167
+ });
168
+ }
169
+
156
170
  try {
157
171
  const check = await authProvider.check();
158
172
  if (!check.authenticated) {
173
+ event.cookies.delete('svadmin-session', { path: '/' });
159
174
  return new Response(null, {
160
175
  status: 302,
161
176
  headers: { Location: loginPath },
162
177
  });
163
178
  }
164
179
  } catch {
180
+ event.cookies.delete('svadmin-session', { path: '/' });
165
181
  return new Response(null, {
166
182
  status: 302,
167
183
  headers: { Location: loginPath },
168
184
  });
169
185
  }
170
-
171
186
  return resolve(event);
172
187
  };
173
188
  }
@@ -190,10 +205,11 @@ export function createAuthActions(authProvider: AuthProvider) {
190
205
  sameSite: 'lax',
191
206
  maxAge: 60 * 60 * 24 * 7, // 7 days
192
207
  });
193
- return { success: true, redirectTo: result.redirectTo ?? '/lite' };
208
+ throw redirect(303, result.redirectTo ?? '/lite');
194
209
  }
195
210
  return { success: false, error: result.error?.message ?? 'Login failed' };
196
211
  } catch (e) {
212
+ if (isRedirect(e)) throw e;
197
213
  return { success: false, error: (e as Error).message };
198
214
  }
199
215
  },