@svadmin/lite 0.5.0 → 0.7.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 +190 -9
- package/dist/compatibility.d.ts +23 -0
- package/dist/compatibility.js +95 -0
- package/dist/components/LiteDynamicFormList.svelte +169 -0
- package/dist/components/LiteDynamicFormList.svelte.d.ts +45 -0
- package/dist/components/LiteFilterBuilder.svelte +205 -0
- package/dist/components/LiteFilterBuilder.svelte.d.ts +19 -0
- package/dist/components/LiteShowField.svelte +52 -22
- package/dist/components/LiteShowField.svelte.d.ts +3 -2
- package/dist/components/LiteTable.svelte +89 -36
- package/dist/components/LiteTable.svelte.d.ts +3 -2
- package/dist/components/LiteTransfer.svelte +185 -0
- package/dist/components/LiteTransfer.svelte.d.ts +18 -0
- 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/fields/LiteAvatarField.svelte +127 -0
- package/dist/components/fields/LiteAvatarField.svelte.d.ts +21 -0
- package/dist/components/fields/LiteCascader.svelte +119 -0
- package/dist/components/fields/LiteCascader.svelte.d.ts +24 -0
- package/dist/components/fields/LiteCodeField.svelte +71 -0
- package/dist/components/fields/LiteCodeField.svelte.d.ts +16 -0
- package/dist/components/fields/LiteCopyField.svelte +74 -0
- package/dist/components/fields/LiteCopyField.svelte.d.ts +18 -0
- package/dist/components/fields/LiteCurrencyField.svelte +126 -0
- package/dist/components/fields/LiteCurrencyField.svelte.d.ts +21 -0
- package/dist/components/fields/LiteDateRangeField.svelte +147 -0
- package/dist/components/fields/LiteDateRangeField.svelte.d.ts +26 -0
- package/dist/components/fields/LitePercentField.svelte +124 -0
- package/dist/components/fields/LitePercentField.svelte.d.ts +17 -0
- package/dist/components/fields/LitePhoneField.svelte +81 -0
- package/dist/components/fields/LitePhoneField.svelte.d.ts +17 -0
- package/dist/components/fields/LiteRatingField.svelte +105 -0
- package/dist/components/fields/LiteRatingField.svelte.d.ts +16 -0
- package/dist/components/fields/LiteTreeSelect.svelte +143 -0
- package/dist/components/fields/LiteTreeSelect.svelte.d.ts +23 -0
- package/dist/components/fields/index.d.ts +12 -0
- package/dist/components/fields/index.js +10 -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 +99 -27
- 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 +10 -2
- package/dist/index.js +8 -1
- package/dist/lite.css +444 -0
- package/dist/schema-generator.js +15 -4
- package/dist/server-adapter.d.ts +18 -1
- package/dist/server-adapter.js +73 -16
- package/package.json +5 -4
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { CrudOperator, FieldDefinition, Filter } from '@svadmin/core';
|
|
3
|
+
import { t } from '@svadmin/core/i18n';
|
|
4
|
+
|
|
5
|
+
export interface FilterRuleItem {
|
|
6
|
+
id: string;
|
|
7
|
+
field: string;
|
|
8
|
+
operator: CrudOperator;
|
|
9
|
+
value: unknown;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
fields?: FieldDefinition[];
|
|
14
|
+
filters?: Filter[];
|
|
15
|
+
logicalOperator?: 'and' | 'or';
|
|
16
|
+
action?: string;
|
|
17
|
+
method?: 'GET' | 'POST';
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
onApply?: (filters: Filter[]) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let {
|
|
23
|
+
fields = [],
|
|
24
|
+
filters = [],
|
|
25
|
+
logicalOperator = 'and',
|
|
26
|
+
action = '',
|
|
27
|
+
disabled = false,
|
|
28
|
+
}: Props = $props();
|
|
29
|
+
|
|
30
|
+
const operatorOptions: { value: CrudOperator; label: string }[] = [
|
|
31
|
+
{ value: 'eq', label: '= 等于' },
|
|
32
|
+
{ value: 'ne', label: '!= 不等于' },
|
|
33
|
+
{ value: 'contains', label: '包含' },
|
|
34
|
+
{ value: 'ncontains', label: '不包含' },
|
|
35
|
+
{ value: 'gt', label: '> 大于' },
|
|
36
|
+
{ value: 'gte', label: '>= 大于等于' },
|
|
37
|
+
{ value: 'lt', label: '< 小于' },
|
|
38
|
+
{ value: 'lte', label: '<= 小于等于' },
|
|
39
|
+
{ value: 'null', label: '为空 (null)' },
|
|
40
|
+
{ value: 'nnull', label: '不为空' },
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
// Flatten initial filters or fallback to 1 empty rule
|
|
44
|
+
const initialRules = $derived.by(() => {
|
|
45
|
+
if (filters.length > 0) {
|
|
46
|
+
return filters.map((f, i) => ({
|
|
47
|
+
id: `rule_${i}`,
|
|
48
|
+
field: 'field' in f ? String(f.field) : (fields[0]?.key ?? ''),
|
|
49
|
+
operator: ('operator' in f ? f.operator : 'eq') as CrudOperator,
|
|
50
|
+
value: 'value' in f ? f.value : '',
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
return [
|
|
54
|
+
{
|
|
55
|
+
id: 'rule_0',
|
|
56
|
+
field: fields[0]?.key ?? '',
|
|
57
|
+
operator: 'eq' as CrudOperator,
|
|
58
|
+
value: '',
|
|
59
|
+
},
|
|
60
|
+
];
|
|
61
|
+
});
|
|
62
|
+
</script>
|
|
63
|
+
|
|
64
|
+
<div class="lite-filter-builder lite-form-group">
|
|
65
|
+
<div class="lite-filter-header">
|
|
66
|
+
<div class="lite-filter-title">
|
|
67
|
+
<strong>{t('common.filters') || '高级多条件筛选'}</strong>
|
|
68
|
+
<select
|
|
69
|
+
name="logical_op"
|
|
70
|
+
class="lite-select lite-select-sm"
|
|
71
|
+
style="width: 120px; display: inline-block; margin-left: 8px;"
|
|
72
|
+
value={logicalOperator}
|
|
73
|
+
{disabled}
|
|
74
|
+
>
|
|
75
|
+
<option value="and">符合全部 (AND)</option>
|
|
76
|
+
<option value="or">符合任一 (OR)</option>
|
|
77
|
+
</select>
|
|
78
|
+
</div>
|
|
79
|
+
<div>
|
|
80
|
+
<button type="submit" name="_action" value="add_filter_rule" class="lite-btn lite-btn-sm" {disabled}>
|
|
81
|
+
+ 添加条件
|
|
82
|
+
</button>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<div class="lite-filter-rules">
|
|
87
|
+
{#each initialRules as rule, idx (rule.id)}
|
|
88
|
+
<div class="lite-filter-row">
|
|
89
|
+
<!-- Field select -->
|
|
90
|
+
<select
|
|
91
|
+
name="filter_field_{idx}"
|
|
92
|
+
class="lite-select lite-filter-field"
|
|
93
|
+
value={rule.field}
|
|
94
|
+
{disabled}
|
|
95
|
+
>
|
|
96
|
+
{#each fields as fld (fld.key)}
|
|
97
|
+
<option value={fld.key}>{fld.label || fld.key}</option>
|
|
98
|
+
{/each}
|
|
99
|
+
</select>
|
|
100
|
+
|
|
101
|
+
<!-- Operator select -->
|
|
102
|
+
<select
|
|
103
|
+
name="filter_op_{idx}"
|
|
104
|
+
class="lite-select lite-filter-op"
|
|
105
|
+
value={rule.operator}
|
|
106
|
+
{disabled}
|
|
107
|
+
>
|
|
108
|
+
{#each operatorOptions as op (op.value)}
|
|
109
|
+
<option value={op.value}>{op.label}</option>
|
|
110
|
+
{/each}
|
|
111
|
+
</select>
|
|
112
|
+
|
|
113
|
+
<!-- Value input -->
|
|
114
|
+
{#if rule.operator !== 'null' && rule.operator !== 'nnull'}
|
|
115
|
+
<input
|
|
116
|
+
type="text"
|
|
117
|
+
name="filter_val_{idx}"
|
|
118
|
+
class="lite-input lite-filter-val"
|
|
119
|
+
value={String(rule.value ?? '')}
|
|
120
|
+
placeholder="输入筛选值..."
|
|
121
|
+
{disabled}
|
|
122
|
+
/>
|
|
123
|
+
{/if}
|
|
124
|
+
|
|
125
|
+
<!-- Remove row button -->
|
|
126
|
+
<button
|
|
127
|
+
type="submit"
|
|
128
|
+
name="_action"
|
|
129
|
+
value="remove_filter_rule_{idx}"
|
|
130
|
+
class="lite-btn lite-btn-sm lite-btn-danger"
|
|
131
|
+
{disabled}
|
|
132
|
+
title="删除此行条件"
|
|
133
|
+
>
|
|
134
|
+
✕
|
|
135
|
+
</button>
|
|
136
|
+
</div>
|
|
137
|
+
{/each}
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
<div class="lite-filter-footer">
|
|
141
|
+
<button type="submit" class="lite-btn lite-btn-primary">
|
|
142
|
+
应用筛选
|
|
143
|
+
</button>
|
|
144
|
+
<a href={action || '?'} class="lite-btn" style="margin-left: 8px;">
|
|
145
|
+
重置
|
|
146
|
+
</a>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<style>
|
|
151
|
+
.lite-filter-builder {
|
|
152
|
+
border: 1px solid #cbd5e1;
|
|
153
|
+
border-radius: 6px;
|
|
154
|
+
padding: 14px;
|
|
155
|
+
background: #f8fafc;
|
|
156
|
+
margin-bottom: 16px;
|
|
157
|
+
}
|
|
158
|
+
.lite-filter-header {
|
|
159
|
+
display: flex;
|
|
160
|
+
justify-content: space-between;
|
|
161
|
+
align-items: center;
|
|
162
|
+
margin-bottom: 12px;
|
|
163
|
+
}
|
|
164
|
+
.lite-filter-rules {
|
|
165
|
+
display: flex;
|
|
166
|
+
flex-direction: column;
|
|
167
|
+
margin-bottom: 12px;
|
|
168
|
+
}
|
|
169
|
+
.lite-filter-row {
|
|
170
|
+
display: flex;
|
|
171
|
+
align-items: center;
|
|
172
|
+
background: #ffffff;
|
|
173
|
+
padding: 8px;
|
|
174
|
+
border: 1px solid #e2e8f0;
|
|
175
|
+
border-radius: 4px;
|
|
176
|
+
margin-bottom: 8px;
|
|
177
|
+
}
|
|
178
|
+
.lite-filter-row:last-child {
|
|
179
|
+
margin-bottom: 0;
|
|
180
|
+
}
|
|
181
|
+
.lite-filter-row > * {
|
|
182
|
+
margin-right: 8px;
|
|
183
|
+
}
|
|
184
|
+
.lite-filter-row > *:last-child {
|
|
185
|
+
margin-right: 0;
|
|
186
|
+
}
|
|
187
|
+
.lite-filter-field {
|
|
188
|
+
flex: 2;
|
|
189
|
+
min-width: 130px;
|
|
190
|
+
}
|
|
191
|
+
.lite-filter-op {
|
|
192
|
+
flex: 2;
|
|
193
|
+
min-width: 120px;
|
|
194
|
+
}
|
|
195
|
+
.lite-filter-val {
|
|
196
|
+
flex: 3;
|
|
197
|
+
min-width: 140px;
|
|
198
|
+
}
|
|
199
|
+
.lite-filter-footer {
|
|
200
|
+
display: flex;
|
|
201
|
+
align-items: center;
|
|
202
|
+
border-top: 1px solid #e2e8f0;
|
|
203
|
+
padding-top: 10px;
|
|
204
|
+
}
|
|
205
|
+
</style>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { CrudOperator, FieldDefinition, Filter } from '@svadmin/core';
|
|
2
|
+
export interface FilterRuleItem {
|
|
3
|
+
id: string;
|
|
4
|
+
field: string;
|
|
5
|
+
operator: CrudOperator;
|
|
6
|
+
value: unknown;
|
|
7
|
+
}
|
|
8
|
+
interface Props {
|
|
9
|
+
fields?: FieldDefinition[];
|
|
10
|
+
filters?: Filter[];
|
|
11
|
+
logicalOperator?: 'and' | 'or';
|
|
12
|
+
action?: string;
|
|
13
|
+
method?: 'GET' | 'POST';
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
onApply?: (filters: Filter[]) => void;
|
|
16
|
+
}
|
|
17
|
+
declare const LiteFilterBuilder: import("svelte").Component<Props, {}, "">;
|
|
18
|
+
type LiteFilterBuilder = ReturnType<typeof LiteFilterBuilder>;
|
|
19
|
+
export default LiteFilterBuilder;
|
|
@@ -2,66 +2,96 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* LiteShowField — SSR-compatible field renderer for detail views.
|
|
4
4
|
* Renders a single field value based on its type definition.
|
|
5
|
-
|
|
6
|
-
import type { FieldDefinition } from
|
|
7
|
-
import { toSafeHref, toSafeText } from
|
|
8
|
-
import { isExplicitBooleanTrue, getStatusBadgeClass } from
|
|
9
|
-
import LiteMediaThumbnail from
|
|
5
|
+
*/
|
|
6
|
+
import type { FieldDefinition } from "@svadmin/core";
|
|
7
|
+
import { toSafeHref, toSafeText } from "../security";
|
|
8
|
+
import { isExplicitBooleanTrue, getStatusBadgeClass } from "../value-normalization";
|
|
9
|
+
import LiteMediaThumbnail from "./LiteMediaThumbnail.svelte";
|
|
10
|
+
import LiteAvatarField from "./fields/LiteAvatarField.svelte";
|
|
11
|
+
import LiteCurrencyField from "./fields/LiteCurrencyField.svelte";
|
|
12
|
+
import LitePhoneField from "./fields/LitePhoneField.svelte";
|
|
13
|
+
import LiteRatingField from "./fields/LiteRatingField.svelte";
|
|
14
|
+
import LiteCodeField from "./fields/LiteCodeField.svelte";
|
|
10
15
|
|
|
11
16
|
interface Props {
|
|
12
17
|
field: FieldDefinition;
|
|
13
18
|
value: unknown;
|
|
19
|
+
basePath?: string;
|
|
14
20
|
}
|
|
15
21
|
|
|
16
|
-
let { field, value }: Props = $props();
|
|
22
|
+
let { field, value, basePath = "/lite" }: Props = $props();
|
|
17
23
|
|
|
18
24
|
function formatValue(v: unknown, f: FieldDefinition): string {
|
|
19
|
-
if (v == null) return
|
|
20
|
-
if (f.type ===
|
|
25
|
+
if (v == null) return "—";
|
|
26
|
+
if (f.type === "date") {
|
|
21
27
|
try { return new Date(v as string).toLocaleString(); } catch { return String(v); }
|
|
22
28
|
}
|
|
23
|
-
if (f.type ===
|
|
29
|
+
if (f.type === "select" && f.options) {
|
|
24
30
|
const opt = f.options.find(o => String(o.value) === String(v));
|
|
25
31
|
return opt?.label ?? String(v);
|
|
26
32
|
}
|
|
27
|
-
if (f.type ===
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (
|
|
33
|
+
if (f.type === "relation" && typeof v === "object" && v !== null) {
|
|
34
|
+
return String((v as Record<string, unknown>)[f.optionLabel ?? "name"] ?? (v as Record<string, unknown>)[f.optionValue ?? "id"] ?? v);
|
|
35
|
+
}
|
|
36
|
+
if (f.type === "url") return String(v);
|
|
37
|
+
if (f.type === "email") return String(v);
|
|
38
|
+
if (Array.isArray(v)) return v.join(", ");
|
|
39
|
+
if (typeof v === "object") {
|
|
31
40
|
try { return JSON.stringify(v, null, 2); } catch { return String(v); }
|
|
32
41
|
}
|
|
33
42
|
return String(v);
|
|
34
43
|
}
|
|
35
44
|
</script>
|
|
36
45
|
|
|
37
|
-
{#if field.type ===
|
|
46
|
+
{#if field.type === "boolean"}
|
|
38
47
|
{@const checked = isExplicitBooleanTrue(value)}
|
|
39
|
-
<span class="lite-bool {checked ?
|
|
40
|
-
{checked ?
|
|
41
|
-
{:else if field.type ===
|
|
48
|
+
<span class="lite-bool {checked ? "lite-bool-true" : ""}"></span>
|
|
49
|
+
{checked ? "✓ Yes" : "✗ No"}
|
|
50
|
+
{:else if field.type === "avatar"}
|
|
51
|
+
<LiteAvatarField {field} value={value as unknown} mode="show" />
|
|
52
|
+
{:else if field.type === "rating"}
|
|
53
|
+
<LiteRatingField {field} value={value as string | number | null | undefined} mode="show" />
|
|
54
|
+
{:else if field.type === "code"}
|
|
55
|
+
<LiteCodeField {field} value={value as string | number | Record<string, unknown> | unknown[] | null | undefined} mode="show" />
|
|
56
|
+
{:else if field.type === "currency"}
|
|
57
|
+
<LiteCurrencyField {field} value={value as string | number | null | undefined} mode="show" />
|
|
58
|
+
{:else if field.type === "phone"}
|
|
59
|
+
<LitePhoneField {field} value={value as string | number | null | undefined} mode="show" />
|
|
60
|
+
{:else if field.type === "password"}
|
|
61
|
+
<span>••••••••</span>
|
|
62
|
+
{:else if field.type === "color" && value}
|
|
63
|
+
<span style="display:inline-flex;align-items:center;">
|
|
64
|
+
<span style="display:inline-block;width:14px;height:14px;border-radius:3px;background:{toSafeText(value)};border:1px solid #cbd5e1;margin-right:6px;"></span>
|
|
65
|
+
<span class="lite-font-mono">{toSafeText(value)}</span>
|
|
66
|
+
</span>
|
|
67
|
+
{:else if field.type === "url" && value}
|
|
42
68
|
{@const href = toSafeHref(value)}
|
|
43
69
|
{#if href}
|
|
44
70
|
<a {href} target="_blank" rel="noopener noreferrer">{toSafeText(value)}</a>
|
|
45
71
|
{:else}
|
|
46
72
|
{toSafeText(value)}
|
|
47
73
|
{/if}
|
|
48
|
-
{:else if field.type ===
|
|
74
|
+
{:else if field.type === "email" && value}
|
|
49
75
|
{@const href = toSafeHref(`mailto:${toSafeText(value)}`)}
|
|
50
76
|
{#if href}
|
|
51
77
|
<a {href}>{toSafeText(value)}</a>
|
|
52
78
|
{:else}
|
|
53
79
|
{toSafeText(value)}
|
|
54
80
|
{/if}
|
|
55
|
-
{:else if field.type ===
|
|
81
|
+
{:else if field.type === "image" && value}
|
|
56
82
|
<LiteMediaThumbnail src={String(value)} alt={field.label} height={200} />
|
|
57
|
-
{:else if field.type ===
|
|
83
|
+
{:else if field.type === "tags" && Array.isArray(value)}
|
|
58
84
|
{#each value as tag, _i (_i)}
|
|
59
85
|
<span class="lite-badge">{tag}</span>
|
|
60
86
|
{/each}
|
|
61
|
-
{:else if field.type ===
|
|
62
|
-
<pre style="margin:0;font-size:12px;background:#f8fafc;padding:12px;border-radius:6px;border:1px solid #e2e8f0;overflow-x:auto;">{typeof value ===
|
|
87
|
+
{:else if field.type === "json" && value}
|
|
88
|
+
<pre style="margin:0;font-size:12px;background:#f8fafc;padding:12px;border-radius:6px;border:1px solid #e2e8f0;overflow-x:auto;">{typeof value === "string" ? value : JSON.stringify(value, null, 2)}</pre>
|
|
63
89
|
{:else if field.type === "select" && field.options}
|
|
64
90
|
<span class={getStatusBadgeClass(value)}>{formatValue(value, field)}</span>
|
|
91
|
+
{:else if field.type === "relation" && field.resource && value != null}
|
|
92
|
+
<a href={`${basePath}/${field.resource}/show/${value}`} class="lite-badge lite-badge-info">
|
|
93
|
+
{formatValue(value, field)} →
|
|
94
|
+
</a>
|
|
65
95
|
{:else}
|
|
66
96
|
{formatValue(value, field)}
|
|
67
97
|
{/if}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* LiteShowField — SSR-compatible field renderer for detail views.
|
|
3
3
|
* Renders a single field value based on its type definition.
|
|
4
|
-
|
|
5
|
-
import type { FieldDefinition } from
|
|
4
|
+
*/
|
|
5
|
+
import type { FieldDefinition } from "@svadmin/core";
|
|
6
6
|
interface Props {
|
|
7
7
|
field: FieldDefinition;
|
|
8
8
|
value: unknown;
|
|
9
|
+
basePath?: string;
|
|
9
10
|
}
|
|
10
11
|
declare const LiteShowField: import("svelte").Component<Props, {}, "">;
|
|
11
12
|
type LiteShowField = ReturnType<typeof LiteShowField>;
|
|
@@ -3,77 +3,105 @@
|
|
|
3
3
|
* LiteTable — Pure HTML table with <a> sort links.
|
|
4
4
|
* No JavaScript required — sorting and pagination are URL-driven.
|
|
5
5
|
*/
|
|
6
|
-
import type { ResourceDefinition, FieldDefinition } from
|
|
7
|
-
import { t } from
|
|
8
|
-
import { isExplicitBooleanTrue, getStatusBadgeClass } from
|
|
9
|
-
import { liteFragmentId } from
|
|
6
|
+
import type { ResourceDefinition, FieldDefinition } from "@svadmin/core";
|
|
7
|
+
import { t } from "@svadmin/core/i18n";
|
|
8
|
+
import { isExplicitBooleanTrue, getStatusBadgeClass } from "../value-normalization";
|
|
9
|
+
import { liteFragmentId } from "../fragment-id";
|
|
10
10
|
|
|
11
11
|
interface Props {
|
|
12
12
|
records: Record<string, unknown>[];
|
|
13
13
|
resource: ResourceDefinition;
|
|
14
14
|
currentSort?: string;
|
|
15
|
-
currentOrder?:
|
|
15
|
+
currentOrder?: "asc" | "desc";
|
|
16
16
|
currentSearch?: string;
|
|
17
17
|
basePath?: string;
|
|
18
18
|
/** Show edit/delete action buttons */
|
|
19
19
|
canShow?: boolean;
|
|
20
20
|
canEdit?: boolean;
|
|
21
21
|
canDelete?: boolean;
|
|
22
|
+
enableBatch?: boolean;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
let {
|
|
25
26
|
records,
|
|
26
27
|
resource,
|
|
27
28
|
currentSort,
|
|
28
|
-
currentOrder =
|
|
29
|
+
currentOrder = "asc",
|
|
29
30
|
currentSearch,
|
|
30
|
-
basePath =
|
|
31
|
+
basePath = "/lite",
|
|
31
32
|
canShow,
|
|
32
33
|
canEdit,
|
|
33
34
|
canDelete,
|
|
35
|
+
enableBatch = false,
|
|
34
36
|
}: Props = $props();
|
|
35
37
|
|
|
36
38
|
const tableId = $props.id();
|
|
37
39
|
|
|
38
|
-
let pk = $derived(resource.primaryKey ??
|
|
40
|
+
let pk = $derived(resource.primaryKey ?? "id");
|
|
39
41
|
const showView = $derived(canShow ?? resource.canShow !== false);
|
|
40
42
|
const showEdit = $derived(canEdit ?? resource.canEdit !== false);
|
|
41
43
|
const showDelete = $derived(canDelete ?? resource.canDelete !== false);
|
|
44
|
+
const showBatch = $derived(enableBatch && showDelete);
|
|
42
45
|
const listFields = $derived(
|
|
43
46
|
resource.fields.filter(f => f.showInList !== false)
|
|
44
47
|
);
|
|
45
48
|
|
|
46
49
|
function sortUrl(field: FieldDefinition): string {
|
|
47
|
-
const newOrder = currentSort === field.key && currentOrder ===
|
|
50
|
+
const newOrder = currentSort === field.key && currentOrder === "asc" ? "desc" : "asc";
|
|
48
51
|
const params = new URLSearchParams({ sort: field.key, order: newOrder });
|
|
49
|
-
if (currentSearch) params.set(
|
|
50
|
-
return
|
|
52
|
+
if (currentSearch) params.set("q", currentSearch);
|
|
53
|
+
return "?" + params.toString();
|
|
51
54
|
}
|
|
52
55
|
|
|
53
56
|
function sortIndicator(field: FieldDefinition): string {
|
|
54
|
-
if (currentSort !== field.key) return
|
|
55
|
-
return currentOrder ===
|
|
57
|
+
if (currentSort !== field.key) return "⇅";
|
|
58
|
+
return currentOrder === "asc" ? "↑" : "↓";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function toggleAll(e: Event) {
|
|
62
|
+
const target = e.target as HTMLInputElement | null;
|
|
63
|
+
if (typeof document !== "undefined" && target) {
|
|
64
|
+
const checkboxes = document.querySelectorAll("input[name=ids]");
|
|
65
|
+
checkboxes.forEach((cb) => {
|
|
66
|
+
(cb as HTMLInputElement).checked = target.checked;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
56
69
|
}
|
|
57
70
|
|
|
58
71
|
function formatValue(value: unknown, field: FieldDefinition): string {
|
|
59
|
-
if (value == null) return
|
|
60
|
-
if (field.type ===
|
|
61
|
-
if (field.type ===
|
|
72
|
+
if (value == null) return "—";
|
|
73
|
+
if (field.type === "boolean") return "";
|
|
74
|
+
if (field.type === "date") {
|
|
62
75
|
try { return new Date(value as string).toLocaleDateString(); } catch { return String(value); }
|
|
63
76
|
}
|
|
64
|
-
if (field.type ===
|
|
77
|
+
if (field.type === "select" && field.options) {
|
|
65
78
|
const opt = field.options.find(o => String(o.value) === String(value));
|
|
66
79
|
return opt?.label ?? String(value);
|
|
67
80
|
}
|
|
68
|
-
if (
|
|
81
|
+
if (field.type === "relation" && typeof value === "object" && value !== null) {
|
|
82
|
+
return String((value as Record<string, unknown>)[field.optionLabel ?? "name"] ?? (value as Record<string, unknown>)[field.optionValue ?? "id"] ?? value);
|
|
83
|
+
}
|
|
84
|
+
if (Array.isArray(value)) return value.join(", ");
|
|
69
85
|
return String(value);
|
|
70
86
|
}
|
|
71
|
-
|
|
72
87
|
</script>
|
|
73
88
|
|
|
89
|
+
{#if showBatch}
|
|
90
|
+
<form id={"batch-delete-" + tableId} method="POST" action="?/batchDelete" style="display:none;"></form>
|
|
91
|
+
{/if}
|
|
92
|
+
|
|
74
93
|
<table class="lite-table">
|
|
75
94
|
<thead>
|
|
76
95
|
<tr>
|
|
96
|
+
{#if showBatch}
|
|
97
|
+
<th style="width: 36px; text-align: center;">
|
|
98
|
+
<input
|
|
99
|
+
type="checkbox"
|
|
100
|
+
title="Select all"
|
|
101
|
+
onchange={toggleAll}
|
|
102
|
+
/>
|
|
103
|
+
</th>
|
|
104
|
+
{/if}
|
|
77
105
|
{#each listFields as field, _i (_i)}
|
|
78
106
|
<th>
|
|
79
107
|
{#if field.sortable !== false}
|
|
@@ -87,7 +115,7 @@
|
|
|
87
115
|
</th>
|
|
88
116
|
{/each}
|
|
89
117
|
{#if showView || showEdit || showDelete}
|
|
90
|
-
<th style="text-align:right;">{t(
|
|
118
|
+
<th style="text-align:right;">{t("common.actions") || "Actions"}</th>
|
|
91
119
|
{/if}
|
|
92
120
|
</tr>
|
|
93
121
|
</thead>
|
|
@@ -95,16 +123,25 @@
|
|
|
95
123
|
{#each records as record, _i (_i)}
|
|
96
124
|
{@const id = record[pk]}
|
|
97
125
|
<tr>
|
|
126
|
+
{#if showBatch}
|
|
127
|
+
<td style="text-align: center;">
|
|
128
|
+
<input type="checkbox" name="ids" value={String(id)} form={"batch-delete-" + tableId} />
|
|
129
|
+
</td>
|
|
130
|
+
{/if}
|
|
98
131
|
{#each listFields as field, _i (_i)}
|
|
99
132
|
<td>
|
|
100
|
-
{#if field.type ===
|
|
101
|
-
<span class="lite-bool {isExplicitBooleanTrue(record[field.key]) ?
|
|
102
|
-
{:else if field.type ===
|
|
133
|
+
{#if field.type === "boolean"}
|
|
134
|
+
<span class="lite-bool {isExplicitBooleanTrue(record[field.key]) ? "lite-bool-true" : ""}"></span>
|
|
135
|
+
{:else if field.type === "tags" && Array.isArray(record[field.key])}
|
|
103
136
|
{#each (record[field.key] as string[]).slice(0, 3) as tag, _i (_i)}
|
|
104
137
|
<span class="lite-badge">{tag}</span>
|
|
105
138
|
{/each}
|
|
106
|
-
{:else if field.type ===
|
|
139
|
+
{:else if field.type === "select" && field.options}
|
|
107
140
|
<span class={getStatusBadgeClass(record[field.key])}>{formatValue(record[field.key], field)}</span>
|
|
141
|
+
{:else if field.type === "relation" && field.resource && record[field.key] != null}
|
|
142
|
+
<a href={basePath + "/" + field.resource + "/show/" + record[field.key]} class="lite-badge lite-badge-info">
|
|
143
|
+
{formatValue(record[field.key], field)}
|
|
144
|
+
</a>
|
|
108
145
|
{:else}
|
|
109
146
|
{formatValue(record[field.key], field)}
|
|
110
147
|
{/if}
|
|
@@ -113,28 +150,28 @@
|
|
|
113
150
|
{#if showView || showEdit || showDelete}
|
|
114
151
|
<td class="actions">
|
|
115
152
|
{#if showView}
|
|
116
|
-
<a href={
|
|
153
|
+
<a href={basePath + "/" + resource.name + "/show/" + id} class="lite-btn lite-btn-sm">{t("common.show") || "Show"}</a>
|
|
117
154
|
{/if}
|
|
118
155
|
{#if showEdit}
|
|
119
|
-
<a href={
|
|
156
|
+
<a href={basePath + "/" + resource.name + "/edit/" + id} class="lite-btn lite-btn-sm">{t("common.edit") || "Edit"}</a>
|
|
120
157
|
{/if}
|
|
121
158
|
{#if showDelete}
|
|
122
|
-
{@const confirmationId = liteFragmentId(
|
|
123
|
-
{@const confirmationTitleId =
|
|
159
|
+
{@const confirmationId = liteFragmentId("delete", tableId, resource.name, String(id))}
|
|
160
|
+
{@const confirmationTitleId = confirmationId + "-title"}
|
|
124
161
|
<div class="lite-confirm">
|
|
125
|
-
<span id={
|
|
162
|
+
<span id={confirmationId + "-closed"} class="lite-confirm-cancel-target" aria-hidden="true"></span>
|
|
126
163
|
<a
|
|
127
|
-
href={
|
|
164
|
+
href={"#" + confirmationId}
|
|
128
165
|
class="lite-btn lite-btn-sm lite-btn-danger"
|
|
129
166
|
aria-controls={confirmationId}
|
|
130
167
|
aria-haspopup="dialog"
|
|
131
|
-
>{t(
|
|
168
|
+
>{t("common.delete") || "Delete"}</a>
|
|
132
169
|
<div id={confirmationId} class="lite-confirm-panel lite-confirm-target" role="dialog" aria-labelledby={confirmationTitleId} tabindex="-1">
|
|
133
|
-
<p id={confirmationTitleId} style="margin:0 0 8px;font-size:13px;">{t(
|
|
170
|
+
<p id={confirmationTitleId} style="margin:0 0 8px;font-size:13px;">{t("common.areYouSure") || "Are you sure?"}</p>
|
|
134
171
|
<form method="POST" action="?/delete" class="lite-inline-actions">
|
|
135
172
|
<input type="hidden" name="id" value={String(id)} />
|
|
136
|
-
<a href={
|
|
137
|
-
<button type="submit" class="lite-btn lite-btn-sm lite-btn-danger">{t(
|
|
173
|
+
<a href={"#" + confirmationId + "-closed"} class="lite-btn lite-btn-sm">{t("common.cancel") || "Cancel"}</a>
|
|
174
|
+
<button type="submit" class="lite-btn lite-btn-sm lite-btn-danger">{t("common.confirm") || "Confirm"}</button>
|
|
138
175
|
</form>
|
|
139
176
|
</div>
|
|
140
177
|
</div>
|
|
@@ -144,10 +181,26 @@
|
|
|
144
181
|
</tr>
|
|
145
182
|
{:else}
|
|
146
183
|
<tr>
|
|
147
|
-
<td colspan={listFields.length + (showView || showEdit || showDelete ? 1 : 0)} style="text-align:center;padding:40px;color:#9ca3af;">
|
|
148
|
-
{t(
|
|
184
|
+
<td colspan={listFields.length + (showView || showEdit || showDelete ? 1 : 0) + (showBatch ? 1 : 0)} style="text-align:center;padding:40px;color:#9ca3af;">
|
|
185
|
+
{t("common.noData") || "No records found."}
|
|
149
186
|
</td>
|
|
150
187
|
</tr>
|
|
151
188
|
{/each}
|
|
152
189
|
</tbody>
|
|
153
190
|
</table>
|
|
191
|
+
|
|
192
|
+
{#if showBatch && records.length > 0}
|
|
193
|
+
<div class="lite-batch-bar">
|
|
194
|
+
<span style="font-size: 13px; color: #64748b;">
|
|
195
|
+
Select items above to perform batch actions
|
|
196
|
+
</span>
|
|
197
|
+
<button
|
|
198
|
+
type="submit"
|
|
199
|
+
form={"batch-delete-" + tableId}
|
|
200
|
+
class="lite-btn lite-btn-sm lite-btn-danger"
|
|
201
|
+
|
|
202
|
+
>
|
|
203
|
+
{t("common.delete") || "Delete Selected"}
|
|
204
|
+
</button>
|
|
205
|
+
</div>
|
|
206
|
+
{/if}
|
|
@@ -2,18 +2,19 @@
|
|
|
2
2
|
* LiteTable — Pure HTML table with <a> sort links.
|
|
3
3
|
* No JavaScript required — sorting and pagination are URL-driven.
|
|
4
4
|
*/
|
|
5
|
-
import type { ResourceDefinition } from
|
|
5
|
+
import type { ResourceDefinition } from "@svadmin/core";
|
|
6
6
|
interface Props {
|
|
7
7
|
records: Record<string, unknown>[];
|
|
8
8
|
resource: ResourceDefinition;
|
|
9
9
|
currentSort?: string;
|
|
10
|
-
currentOrder?:
|
|
10
|
+
currentOrder?: "asc" | "desc";
|
|
11
11
|
currentSearch?: string;
|
|
12
12
|
basePath?: string;
|
|
13
13
|
/** Show edit/delete action buttons */
|
|
14
14
|
canShow?: boolean;
|
|
15
15
|
canEdit?: boolean;
|
|
16
16
|
canDelete?: boolean;
|
|
17
|
+
enableBatch?: boolean;
|
|
17
18
|
}
|
|
18
19
|
declare const LiteTable: import("svelte").Component<Props, {}, "">;
|
|
19
20
|
type LiteTable = ReturnType<typeof LiteTable>;
|