@svadmin/ui 0.86.0 → 0.88.1
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 +3 -1
- package/dist/app.css +188 -5
- package/dist/app.theme.css +188 -5
- package/dist/component-registry.d.ts +1 -1
- package/dist/component-registry.js +2 -2
- package/dist/components/ApprovalActionCard.svelte +54 -31
- package/dist/components/ApprovalCenter.svelte +73 -31
- package/dist/components/AuthPageShell.svelte +13 -2
- package/dist/components/AutoTable.svelte +13 -3
- package/dist/components/CreatePage.svelte +22 -15
- package/dist/components/CreatePage.svelte.d.ts +4 -0
- package/dist/components/EditPage.svelte +23 -16
- package/dist/components/EditPage.svelte.d.ts +4 -0
- package/dist/components/ListPage.svelte +20 -14
- package/dist/components/ListPage.svelte.d.ts +2 -0
- package/dist/components/LoginPage.svelte +27 -11
- package/dist/components/LoginPage.svelte.d.ts +4 -0
- package/dist/components/NotificationsSettings.svelte +16 -8
- package/dist/components/PagePattern.svelte +2 -2
- package/dist/components/PagePattern.svelte.d.ts +1 -1
- package/dist/components/PasswordInput.svelte +36 -17
- package/dist/components/PasswordInput.svelte.d.ts +3 -0
- package/dist/components/ShowPage.svelte +20 -21
- package/dist/components/account/MemberDirectory.svelte +17 -4
- package/dist/components/account/SecurityLogPage.svelte +8 -1
- package/dist/components/content/AuditTimeline.svelte +32 -4
- package/dist/components/content/AuditTimeline.svelte.d.ts +9 -0
- package/dist/components/content/DataState.svelte +5 -4
- package/dist/components/content/DataState.svelte.d.ts +1 -1
- package/dist/components/content/FilterToolbar.svelte +7 -1
- package/dist/components/content/WorkspaceLayout.svelte +3 -1
- package/dist/components/content/WorkspaceLayout.svelte.d.ts +1 -0
- package/dist/components/network/TeamCrewTablePage.svelte +8 -1
- package/dist/components/network/UserCardsNFTPage.svelte +3 -1
- package/dist/components/profile/ProjectsGrid.svelte +1 -1
- package/dist/components/profile/TeamsShowcase.svelte +1 -1
- package/dist/design-contract.d.ts +1 -1
- package/dist/design-contract.js +1 -1
- package/dist/page-chrome.css +3 -3
- package/dist/recipes.d.ts +102 -69
- package/dist/recipes.js +40 -2
- package/package.json +1 -1
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { Button } from './ui/button/index.js';
|
|
5
5
|
import { Badge } from './ui/badge/index.js';
|
|
6
6
|
import * as Dialog from './ui/dialog/index.js';
|
|
7
|
+
import { useTranslation } from '@svadmin/core/i18n';
|
|
7
8
|
import { cn } from '../utils.js';
|
|
8
9
|
|
|
9
10
|
export type ApprovalStatus = 'pending' | 'approved' | 'rejected' | 'recalled';
|
|
@@ -28,7 +29,7 @@
|
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
let {
|
|
31
|
-
title
|
|
32
|
+
title,
|
|
32
33
|
status = 'pending',
|
|
33
34
|
applicant,
|
|
34
35
|
onapprove,
|
|
@@ -39,6 +40,10 @@
|
|
|
39
40
|
children,
|
|
40
41
|
}: Props = $props();
|
|
41
42
|
|
|
43
|
+
const controlId = $props.id();
|
|
44
|
+
const i18n = useTranslation();
|
|
45
|
+
const isZh = $derived(i18n.locale === 'zh-CN');
|
|
46
|
+
const resolvedTitle = $derived(title ?? (isZh ? '审批申请' : 'Approval Request'));
|
|
42
47
|
let approveDialogOpen = $state(false);
|
|
43
48
|
let rejectDialogOpen = $state(false);
|
|
44
49
|
let transferDialogOpen = $state(false);
|
|
@@ -47,26 +52,35 @@
|
|
|
47
52
|
let rejectReason = $state('');
|
|
48
53
|
let transferTarget = $state('');
|
|
49
54
|
let isSubmitting = $state(false);
|
|
55
|
+
let errorMessage = $state('');
|
|
50
56
|
|
|
51
57
|
const statusConfig = $derived.by(() => {
|
|
52
58
|
switch (status) {
|
|
53
59
|
case 'approved':
|
|
54
|
-
return { label: 'Approved', icon: CheckCircle2 };
|
|
60
|
+
return { label: isZh ? '已通过' : 'Approved', icon: CheckCircle2 };
|
|
55
61
|
case 'rejected':
|
|
56
|
-
return { label: 'Rejected', icon: XCircle };
|
|
62
|
+
return { label: isZh ? '已驳回' : 'Rejected', icon: XCircle };
|
|
57
63
|
case 'recalled':
|
|
58
|
-
return { label: 'Recalled', icon: Undo2 };
|
|
64
|
+
return { label: isZh ? '已撤回' : 'Recalled', icon: Undo2 };
|
|
59
65
|
default:
|
|
60
|
-
return { label: 'Pending Approval', icon: Clock };
|
|
66
|
+
return { label: isZh ? '待审批' : 'Pending Approval', icon: Clock };
|
|
61
67
|
}
|
|
62
68
|
});
|
|
63
69
|
|
|
70
|
+
function failureMessage(caught: unknown): string {
|
|
71
|
+
if (caught instanceof Error && caught.message.trim()) return caught.message;
|
|
72
|
+
return isZh ? '操作失败,请检查输入后重试。' : 'Action failed. Check the input and retry.';
|
|
73
|
+
}
|
|
74
|
+
|
|
64
75
|
async function handleApprove() {
|
|
65
76
|
isSubmitting = true;
|
|
77
|
+
errorMessage = '';
|
|
66
78
|
try {
|
|
67
79
|
await onapprove?.(approveComment || undefined);
|
|
68
80
|
approveDialogOpen = false;
|
|
69
81
|
approveComment = '';
|
|
82
|
+
} catch (caught) {
|
|
83
|
+
errorMessage = failureMessage(caught);
|
|
70
84
|
} finally {
|
|
71
85
|
isSubmitting = false;
|
|
72
86
|
}
|
|
@@ -75,10 +89,13 @@
|
|
|
75
89
|
async function handleReject() {
|
|
76
90
|
if (!rejectReason.trim()) return;
|
|
77
91
|
isSubmitting = true;
|
|
92
|
+
errorMessage = '';
|
|
78
93
|
try {
|
|
79
94
|
await onreject?.(rejectReason);
|
|
80
95
|
rejectDialogOpen = false;
|
|
81
96
|
rejectReason = '';
|
|
97
|
+
} catch (caught) {
|
|
98
|
+
errorMessage = failureMessage(caught);
|
|
82
99
|
} finally {
|
|
83
100
|
isSubmitting = false;
|
|
84
101
|
}
|
|
@@ -87,10 +104,13 @@
|
|
|
87
104
|
async function handleTransfer() {
|
|
88
105
|
if (!transferTarget.trim()) return;
|
|
89
106
|
isSubmitting = true;
|
|
107
|
+
errorMessage = '';
|
|
90
108
|
try {
|
|
91
109
|
await ontransfer?.(transferTarget);
|
|
92
110
|
transferDialogOpen = false;
|
|
93
111
|
transferTarget = '';
|
|
112
|
+
} catch (caught) {
|
|
113
|
+
errorMessage = failureMessage(caught);
|
|
94
114
|
} finally {
|
|
95
115
|
isSubmitting = false;
|
|
96
116
|
}
|
|
@@ -101,7 +121,7 @@
|
|
|
101
121
|
<div class="svadmin-approval-card__header">
|
|
102
122
|
<div class="svadmin-approval-card__heading">
|
|
103
123
|
<div class="svadmin-approval-card__title-row">
|
|
104
|
-
<h4 class="svadmin-approval-card__title">{
|
|
124
|
+
<h4 class="svadmin-approval-card__title">{resolvedTitle}</h4>
|
|
105
125
|
<Badge variant="outline" data-status={status}>
|
|
106
126
|
<statusConfig.icon class="svadmin-approval-card__status-icon" />
|
|
107
127
|
{statusConfig.label}
|
|
@@ -109,7 +129,7 @@
|
|
|
109
129
|
</div>
|
|
110
130
|
{#if applicant}
|
|
111
131
|
<p class="svadmin-approval-card__applicant">
|
|
112
|
-
Submitted by <strong>{applicant.name}</strong>
|
|
132
|
+
{isZh ? '提交人:' : 'Submitted by '}<strong>{applicant.name}</strong>
|
|
113
133
|
{#if applicant.department} ({applicant.department}){/if}
|
|
114
134
|
{#if applicant.time} · {applicant.time}{/if}
|
|
115
135
|
</p>
|
|
@@ -124,10 +144,10 @@
|
|
|
124
144
|
size="sm"
|
|
125
145
|
class="svadmin-approval-card__action"
|
|
126
146
|
{disabled}
|
|
127
|
-
onclick={() => { transferDialogOpen = true; }}
|
|
147
|
+
onclick={() => { transferDialogOpen = true; errorMessage = ''; }}
|
|
128
148
|
>
|
|
129
149
|
<ArrowRightLeft class="svadmin-approval-card__action-icon" />
|
|
130
|
-
Transfer
|
|
150
|
+
{isZh ? '转交' : 'Transfer'}
|
|
131
151
|
</Button>
|
|
132
152
|
{/if}
|
|
133
153
|
|
|
@@ -137,10 +157,10 @@
|
|
|
137
157
|
size="sm"
|
|
138
158
|
class="svadmin-approval-card__action svadmin-approval-card__action--reject"
|
|
139
159
|
{disabled}
|
|
140
|
-
onclick={() => { rejectDialogOpen = true; }}
|
|
160
|
+
onclick={() => { rejectDialogOpen = true; errorMessage = ''; }}
|
|
141
161
|
>
|
|
142
162
|
<X class="svadmin-approval-card__action-icon" />
|
|
143
|
-
Reject
|
|
163
|
+
{isZh ? '驳回' : 'Reject'}
|
|
144
164
|
</Button>
|
|
145
165
|
{/if}
|
|
146
166
|
|
|
@@ -149,10 +169,10 @@
|
|
|
149
169
|
size="sm"
|
|
150
170
|
class="svadmin-approval-card__action svadmin-approval-card__action--approve"
|
|
151
171
|
{disabled}
|
|
152
|
-
onclick={() => { approveDialogOpen = true; }}
|
|
172
|
+
onclick={() => { approveDialogOpen = true; errorMessage = ''; }}
|
|
153
173
|
>
|
|
154
174
|
<Check class="svadmin-approval-card__action-icon" />
|
|
155
|
-
Approve
|
|
175
|
+
{isZh ? '通过' : 'Approve'}
|
|
156
176
|
</Button>
|
|
157
177
|
{/if}
|
|
158
178
|
</div>
|
|
@@ -171,21 +191,22 @@
|
|
|
171
191
|
<Dialog.Dialog bind:open={approveDialogOpen}>
|
|
172
192
|
<Dialog.DialogContent class="svadmin-approval-card__dialog">
|
|
173
193
|
<Dialog.DialogHeader>
|
|
174
|
-
<Dialog.DialogTitle>Confirm Approval</Dialog.DialogTitle>
|
|
194
|
+
<Dialog.DialogTitle>{isZh ? '确认通过' : 'Confirm Approval'}</Dialog.DialogTitle>
|
|
175
195
|
</Dialog.DialogHeader>
|
|
176
196
|
<div class="svadmin-approval-card__dialog-form">
|
|
177
|
-
<label class="svadmin-approval-card__dialog-label" for=
|
|
197
|
+
<label class="svadmin-approval-card__dialog-label" for={`${controlId}-approve-comment`}>{isZh ? '审批意见(可选)' : 'Optional Approval Comment'}</label>
|
|
178
198
|
<textarea
|
|
179
|
-
id=
|
|
199
|
+
id={`${controlId}-approve-comment`}
|
|
180
200
|
bind:value={approveComment}
|
|
181
|
-
placeholder=
|
|
201
|
+
placeholder={isZh ? '例如:已核验,准予通过' : 'e.g. Verified and approved'}
|
|
182
202
|
class="svadmin-approval-card__textarea"
|
|
183
203
|
></textarea>
|
|
204
|
+
{#if errorMessage}<p role="alert" class="svadmin-approval-card__error text-destructive">{errorMessage}</p>{/if}
|
|
184
205
|
</div>
|
|
185
206
|
<Dialog.DialogFooter>
|
|
186
|
-
<Button variant="outline" size="sm" onclick={() => { approveDialogOpen = false; }}>Cancel</Button>
|
|
207
|
+
<Button variant="outline" size="sm" onclick={() => { approveDialogOpen = false; errorMessage = ''; }}>{isZh ? '取消' : 'Cancel'}</Button>
|
|
187
208
|
<Button size="sm" class="svadmin-approval-card__confirm svadmin-approval-card__confirm--approve" disabled={isSubmitting} onclick={handleApprove}>
|
|
188
|
-
Confirm Approval
|
|
209
|
+
{isSubmitting ? (isZh ? '处理中…' : 'Processing…') : (isZh ? '确认通过' : 'Confirm Approval')}
|
|
189
210
|
</Button>
|
|
190
211
|
</Dialog.DialogFooter>
|
|
191
212
|
</Dialog.DialogContent>
|
|
@@ -197,22 +218,23 @@
|
|
|
197
218
|
<Dialog.Dialog bind:open={rejectDialogOpen}>
|
|
198
219
|
<Dialog.DialogContent class="svadmin-approval-card__dialog">
|
|
199
220
|
<Dialog.DialogHeader>
|
|
200
|
-
<Dialog.DialogTitle class="svadmin-approval-card__reject-title">Reject Request</Dialog.DialogTitle>
|
|
221
|
+
<Dialog.DialogTitle class="svadmin-approval-card__reject-title">{isZh ? '驳回申请' : 'Reject Request'}</Dialog.DialogTitle>
|
|
201
222
|
</Dialog.DialogHeader>
|
|
202
223
|
<div class="svadmin-approval-card__dialog-form">
|
|
203
|
-
<label class="svadmin-approval-card__dialog-label" for=
|
|
224
|
+
<label class="svadmin-approval-card__dialog-label" for={`${controlId}-reject-reason`}>{isZh ? '驳回原因(必填)' : 'Rejection Reason (Required)'}</label>
|
|
204
225
|
<textarea
|
|
205
|
-
id=
|
|
226
|
+
id={`${controlId}-reject-reason`}
|
|
206
227
|
bind:value={rejectReason}
|
|
207
|
-
placeholder=
|
|
228
|
+
placeholder={isZh ? '请说明驳回原因…' : 'Please explain why this request is being rejected...'}
|
|
208
229
|
class="svadmin-approval-card__textarea svadmin-approval-card__textarea--reject"
|
|
209
230
|
required
|
|
210
231
|
></textarea>
|
|
232
|
+
{#if errorMessage}<p role="alert" class="svadmin-approval-card__error text-destructive">{errorMessage}</p>{/if}
|
|
211
233
|
</div>
|
|
212
234
|
<Dialog.DialogFooter>
|
|
213
|
-
<Button variant="outline" size="sm" onclick={() => { rejectDialogOpen = false; }}>Cancel</Button>
|
|
235
|
+
<Button variant="outline" size="sm" onclick={() => { rejectDialogOpen = false; errorMessage = ''; }}>{isZh ? '取消' : 'Cancel'}</Button>
|
|
214
236
|
<Button variant="destructive" size="sm" disabled={isSubmitting || !rejectReason.trim()} onclick={handleReject}>
|
|
215
|
-
Confirm Rejection
|
|
237
|
+
{isSubmitting ? (isZh ? '处理中…' : 'Processing…') : (isZh ? '确认驳回' : 'Confirm Rejection')}
|
|
216
238
|
</Button>
|
|
217
239
|
</Dialog.DialogFooter>
|
|
218
240
|
</Dialog.DialogContent>
|
|
@@ -224,23 +246,24 @@
|
|
|
224
246
|
<Dialog.Dialog bind:open={transferDialogOpen}>
|
|
225
247
|
<Dialog.DialogContent class="svadmin-approval-card__dialog">
|
|
226
248
|
<Dialog.DialogHeader>
|
|
227
|
-
<Dialog.DialogTitle>Transfer Approval</Dialog.DialogTitle>
|
|
249
|
+
<Dialog.DialogTitle>{isZh ? '转交审批' : 'Transfer Approval'}</Dialog.DialogTitle>
|
|
228
250
|
</Dialog.DialogHeader>
|
|
229
251
|
<div class="svadmin-approval-card__dialog-form">
|
|
230
|
-
<label class="svadmin-approval-card__dialog-label" for=
|
|
252
|
+
<label class="svadmin-approval-card__dialog-label" for={`${controlId}-transfer-target`}>{isZh ? '目标审批人邮箱或用户名' : 'Target Approver Email / Username'}</label>
|
|
231
253
|
<input
|
|
232
|
-
id=
|
|
254
|
+
id={`${controlId}-transfer-target`}
|
|
233
255
|
type="text"
|
|
234
256
|
bind:value={transferTarget}
|
|
235
|
-
placeholder=
|
|
257
|
+
placeholder={isZh ? '例如:manager@example.com' : 'e.g. manager@example.com'}
|
|
236
258
|
class="svadmin-approval-card__input"
|
|
237
259
|
required
|
|
238
260
|
/>
|
|
261
|
+
{#if errorMessage}<p role="alert" class="svadmin-approval-card__error text-destructive">{errorMessage}</p>{/if}
|
|
239
262
|
</div>
|
|
240
263
|
<Dialog.DialogFooter>
|
|
241
|
-
<Button variant="outline" size="sm" onclick={() => { transferDialogOpen = false; }}>Cancel</Button>
|
|
264
|
+
<Button variant="outline" size="sm" onclick={() => { transferDialogOpen = false; errorMessage = ''; }}>{isZh ? '取消' : 'Cancel'}</Button>
|
|
242
265
|
<Button size="sm" disabled={isSubmitting || !transferTarget.trim()} onclick={handleTransfer}>
|
|
243
|
-
Transfer
|
|
266
|
+
{isSubmitting ? (isZh ? '处理中…' : 'Processing…') : (isZh ? '转交' : 'Transfer')}
|
|
244
267
|
</Button>
|
|
245
268
|
</Dialog.DialogFooter>
|
|
246
269
|
</Dialog.DialogContent>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { ChevronLeft, ChevronRight,
|
|
2
|
+
import { ChevronLeft, ChevronRight, RefreshCw, Search } from '@lucide/svelte';
|
|
3
3
|
import { onDestroy, untrack } from 'svelte';
|
|
4
4
|
import { captureAdminContext, captureAuthSession, createEnterpriseRequestContext } from '@svadmin/core';
|
|
5
5
|
import {
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
import { Badge } from './ui/badge/index.js';
|
|
13
13
|
import { Button } from './ui/button/index.js';
|
|
14
14
|
import { Input } from './ui/input/index.js';
|
|
15
|
+
import * as Dialog from './ui/dialog/index.js';
|
|
16
|
+
import DataState from './content/DataState.svelte';
|
|
15
17
|
|
|
16
18
|
type ApprovalView = 'pending' | 'history';
|
|
17
19
|
interface Props {
|
|
@@ -37,6 +39,7 @@
|
|
|
37
39
|
let comment = $state('');
|
|
38
40
|
let targetId = $state('');
|
|
39
41
|
let actionDialog = $state<string>();
|
|
42
|
+
let actionOpen = $state(false);
|
|
40
43
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
41
44
|
let mounted = true;
|
|
42
45
|
let epoch = 0;
|
|
@@ -48,6 +51,15 @@
|
|
|
48
51
|
const activeAction = $derived(
|
|
49
52
|
activeRecord?.allowedActions.find(action => action.id === actionDialog),
|
|
50
53
|
);
|
|
54
|
+
const centerState = $derived(
|
|
55
|
+
loading
|
|
56
|
+
? 'loading'
|
|
57
|
+
: error && records.length === 0
|
|
58
|
+
? 'error'
|
|
59
|
+
: records.length === 0
|
|
60
|
+
? (appliedSearch ? 'filtered-empty' : 'empty')
|
|
61
|
+
: 'ready',
|
|
62
|
+
);
|
|
51
63
|
const normalizedPageSize = $derived(Number.isSafeInteger(pageSize) && pageSize > 0 && pageSize <= 200 ? pageSize : 20);
|
|
52
64
|
const totalPages = $derived(Math.max(1, Math.ceil(total / normalizedPageSize)));
|
|
53
65
|
|
|
@@ -74,6 +86,8 @@
|
|
|
74
86
|
records = result.data;
|
|
75
87
|
total = result.total;
|
|
76
88
|
selectedId = result.data.some(record => record.id === selectedId) ? selectedId : result.data[0]?.id;
|
|
89
|
+
actionDialog = undefined;
|
|
90
|
+
actionOpen = false;
|
|
77
91
|
} catch (cause) {
|
|
78
92
|
if (!mounted || currentEpoch !== epoch) return;
|
|
79
93
|
error = cause instanceof ApprovalContractError ? 'invalid' : 'load';
|
|
@@ -95,6 +109,13 @@
|
|
|
95
109
|
}, 250);
|
|
96
110
|
}
|
|
97
111
|
|
|
112
|
+
function clearSearch(): void {
|
|
113
|
+
clearTimeout(timer);
|
|
114
|
+
search = '';
|
|
115
|
+
appliedSearch = '';
|
|
116
|
+
page = 1;
|
|
117
|
+
}
|
|
118
|
+
|
|
98
119
|
function switchView(next: ApprovalView): void {
|
|
99
120
|
if (view === next) return;
|
|
100
121
|
view = next;
|
|
@@ -105,6 +126,7 @@
|
|
|
105
126
|
function openAction(action: string): void {
|
|
106
127
|
if (!activeRecord?.allowedActions.some(candidate => candidate.id === action)) return;
|
|
107
128
|
actionDialog = action;
|
|
129
|
+
actionOpen = true;
|
|
108
130
|
comment = '';
|
|
109
131
|
targetId = '';
|
|
110
132
|
}
|
|
@@ -139,6 +161,7 @@
|
|
|
139
161
|
return;
|
|
140
162
|
}
|
|
141
163
|
actionDialog = undefined;
|
|
164
|
+
actionOpen = false;
|
|
142
165
|
records = records.map(candidate => candidate.id === receipt.record.id ? receipt.record : candidate);
|
|
143
166
|
selectedId = receipt.record.id;
|
|
144
167
|
} catch (cause) {
|
|
@@ -166,14 +189,15 @@
|
|
|
166
189
|
records = [];
|
|
167
190
|
selectedId = undefined;
|
|
168
191
|
actionDialog = undefined;
|
|
192
|
+
actionOpen = false;
|
|
169
193
|
actionPending = false;
|
|
170
194
|
void load();
|
|
171
195
|
});
|
|
172
196
|
});
|
|
173
197
|
</script>
|
|
174
198
|
|
|
175
|
-
<section class="svadmin-approval-center" aria-label={title ?? i18n.t('approval.title')}>
|
|
176
|
-
<header class="svadmin-approval-center__header">
|
|
199
|
+
<section class="svadmin-approval-center" aria-label={title ?? i18n.t('approval.title')} data-svadmin-approval-center data-svadmin-approval-view={view} data-svadmin-approval-state={centerState}>
|
|
200
|
+
<header class="svadmin-approval-center__header" data-svadmin-approval-header>
|
|
177
201
|
<div>
|
|
178
202
|
<h2>{title ?? i18n.t('approval.title')}</h2>
|
|
179
203
|
</div>
|
|
@@ -181,7 +205,7 @@
|
|
|
181
205
|
<RefreshCw class={loading ? 'svadmin-approval-center__spin' : ''} />
|
|
182
206
|
</Button>
|
|
183
207
|
</header>
|
|
184
|
-
<div class="svadmin-approval-center__toolbar">
|
|
208
|
+
<div class="svadmin-approval-center__toolbar" data-svadmin-approval-toolbar>
|
|
185
209
|
<Button type="button" aria-pressed={view === 'pending'} variant={view === 'pending' ? 'default' : 'outline'} onclick={() => switchView('pending')}>{i18n.t('approval.pending')}</Button>
|
|
186
210
|
<Button type="button" aria-pressed={view === 'history'} variant={view === 'history' ? 'default' : 'outline'} onclick={() => switchView('history')}>{i18n.t('approval.history')}</Button>
|
|
187
211
|
<label class="svadmin-approval-center__search">
|
|
@@ -189,15 +213,28 @@
|
|
|
189
213
|
<Input value={search} oninput={scheduleSearch} placeholder={i18n.t('approval.search')} aria-label={i18n.t('approval.search')} />
|
|
190
214
|
</label>
|
|
191
215
|
</div>
|
|
192
|
-
{#if error}
|
|
216
|
+
{#if error && records.length > 0}
|
|
193
217
|
<div role="alert">{i18n.t(`approval.${error}`)}</div>
|
|
194
218
|
{/if}
|
|
195
|
-
<div class="svadmin-approval-center__body">
|
|
196
|
-
<div class="svadmin-approval-center__list">
|
|
219
|
+
<div class="svadmin-approval-center__body" data-svadmin-approval-body>
|
|
220
|
+
<div class="svadmin-approval-center__list" data-svadmin-approval-list>
|
|
197
221
|
{#if loading}
|
|
198
|
-
<
|
|
222
|
+
<DataState state="loading" />
|
|
223
|
+
{:else if records.length === 0 && error}
|
|
224
|
+
<DataState
|
|
225
|
+
state="error"
|
|
226
|
+
title={error === 'invalid' ? i18n.t('approval.invalid') : i18n.t('approval.load')}
|
|
227
|
+
description={i18n.locale === 'zh-CN' ? '请刷新后重试。' : 'Refresh and retry.'}
|
|
228
|
+
retry={() => void load()}
|
|
229
|
+
/>
|
|
199
230
|
{:else if records.length === 0}
|
|
200
|
-
<
|
|
231
|
+
<DataState
|
|
232
|
+
state={appliedSearch ? 'filtered-empty' : 'empty'}
|
|
233
|
+
{...(appliedSearch ? {} : { title: i18n.t('approval.empty') })}
|
|
234
|
+
/>
|
|
235
|
+
{#if appliedSearch}
|
|
236
|
+
<Button type="button" variant="outline" size="sm" onclick={clearSearch}>{i18n.t('common.clear')}</Button>
|
|
237
|
+
{/if}
|
|
201
238
|
{:else}
|
|
202
239
|
{#each records as record (record.id)}
|
|
203
240
|
<button type="button" aria-pressed={record.id === selectedId} class:selected={record.id === selectedId} onclick={() => selectedId = record.id}>
|
|
@@ -207,7 +244,7 @@
|
|
|
207
244
|
{/each}
|
|
208
245
|
{/if}
|
|
209
246
|
</div>
|
|
210
|
-
<div class="svadmin-approval-center__detail">
|
|
247
|
+
<div class="svadmin-approval-center__detail" data-svadmin-approval-detail>
|
|
211
248
|
{#if activeRecord}
|
|
212
249
|
<h3>{activeRecord.title}</h3>
|
|
213
250
|
<Badge variant="outline">{activeRecord.status}</Badge>
|
|
@@ -230,7 +267,7 @@
|
|
|
230
267
|
{/if}
|
|
231
268
|
</div>
|
|
232
269
|
</div>
|
|
233
|
-
<footer class="svadmin-approval-center__footer">
|
|
270
|
+
<footer class="svadmin-approval-center__footer" data-svadmin-approval-pagination>
|
|
234
271
|
<Button type="button" aria-label={i18n.t('common.prev')} title={i18n.t('common.prev')} variant="outline" disabled={page <= 1 || loading} onclick={() => { page -= 1; }}>
|
|
235
272
|
<ChevronLeft />
|
|
236
273
|
</Button>
|
|
@@ -241,25 +278,30 @@
|
|
|
241
278
|
</footer>
|
|
242
279
|
</section>
|
|
243
280
|
|
|
244
|
-
{
|
|
245
|
-
|
|
246
|
-
<
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
<label>{i18n.t('approval.
|
|
252
|
-
<
|
|
281
|
+
<Dialog.Root bind:open={actionOpen}>
|
|
282
|
+
{#if actionDialog && activeRecord}
|
|
283
|
+
<Dialog.Content class="svadmin-approval-center__dialog" aria-label={activeAction?.label ?? i18n.t('common.confirmAction')}>
|
|
284
|
+
<Dialog.Header>
|
|
285
|
+
<Dialog.Title>{activeAction?.label}</Dialog.Title>
|
|
286
|
+
<Dialog.Description>{i18n.t('approval.comment')}</Dialog.Description>
|
|
287
|
+
</Dialog.Header>
|
|
288
|
+
<label>{i18n.t('approval.comment')}
|
|
289
|
+
<textarea bind:value={comment} required={activeAction?.commentRequired} maxlength={10000}></textarea>
|
|
253
290
|
</label>
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
{
|
|
291
|
+
{#if activeAction?.targetRequired}
|
|
292
|
+
<label>{i18n.t('approval.target')}
|
|
293
|
+
<Input bind:value={targetId} required maxlength={200} />
|
|
294
|
+
</label>
|
|
295
|
+
{/if}
|
|
296
|
+
<Dialog.Footer>
|
|
297
|
+
<Button type="button" variant="outline" onclick={() => { actionOpen = false; actionDialog = undefined; }}>{i18n.t('common.cancel')}</Button>
|
|
298
|
+
<Button type="button" disabled={actionPending || !activeAction || !canSubmit(activeAction)} onclick={() => void submitAction()}>
|
|
299
|
+
{actionPending ? i18n.t('common.processing') : i18n.t('common.confirm')}
|
|
300
|
+
</Button>
|
|
301
|
+
</Dialog.Footer>
|
|
302
|
+
</Dialog.Content>
|
|
303
|
+
{/if}
|
|
304
|
+
</Dialog.Root>
|
|
263
305
|
|
|
264
306
|
<style>
|
|
265
307
|
.svadmin-approval-center { display: grid; gap: 1rem; min-width: 0; }
|
|
@@ -275,8 +317,8 @@
|
|
|
275
317
|
.svadmin-approval-center__list span { color: var(--muted-foreground); font-size: 0.875rem; }
|
|
276
318
|
.svadmin-approval-center__detail { display: grid; gap: 0.75rem; padding: 1rem; min-width: 0; }
|
|
277
319
|
.svadmin-approval-center__detail ol, .svadmin-approval-center__detail ul { margin: 0; padding-inline-start: 1.25rem; }
|
|
278
|
-
.svadmin-approval-center__dialog {
|
|
279
|
-
.svadmin-approval-center__dialog textarea { min-height: 6rem; width: 100%; }
|
|
320
|
+
:global(.svadmin-approval-center__dialog) { display: grid; gap: 1rem; width: min(28rem, calc(100vw - 2rem)); }
|
|
321
|
+
:global(.svadmin-approval-center__dialog textarea) { min-height: 6rem; width: 100%; }
|
|
280
322
|
:global(.svadmin-approval-center__spin) { animation: svadmin-approval-spin 1s linear infinite; }
|
|
281
323
|
@keyframes svadmin-approval-spin { to { transform: rotate(360deg); } }
|
|
282
324
|
@media (max-width: 640px) { .svadmin-approval-center__body { grid-template-columns: 1fr; } .svadmin-approval-center__list { border-inline-end: 0; border-block-end: 1px solid var(--border); max-height: 15rem; overflow: auto; } }
|
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
let { brand, title, description, children, footer, brandMark }: Props = $props();
|
|
15
|
+
const generatedId = $props.id();
|
|
16
|
+
const titleId = `svadmin-auth-title-${generatedId}`;
|
|
17
|
+
const descriptionId = `${titleId}-description`;
|
|
15
18
|
</script>
|
|
16
19
|
|
|
17
20
|
<div class="svadmin-u-f3c543ad5fe9 svadmin-u-793346c7362c svadmin-u-967d113a1451 svadmin-u-5d17652eacdf" data-svadmin-auth-page>
|
|
@@ -29,8 +32,16 @@
|
|
|
29
32
|
{#if brandMark}{@render brandMark()}{:else}<span data-svadmin-auth-default-mark><SvadminLogo size={36} /></span>{/if}
|
|
30
33
|
<span class="svadmin-u-4ee734926ff6 svadmin-u-e83a7042bc91 svadmin-u-d4108abe6359">{brand}</span>
|
|
31
34
|
</div>
|
|
32
|
-
<section
|
|
33
|
-
|
|
35
|
+
<section
|
|
36
|
+
class="svadmin-u-5f22e64f2282 svadmin-u-ca6bcd4b6f3f svadmin-u-18049387f0af svadmin-u-cd0ad9a56558 svadmin-u-c07e54fd1439 svadmin-u-438b2237b8d6 svadmin-u-e299d4635ea7"
|
|
37
|
+
data-svadmin-auth-card
|
|
38
|
+
aria-labelledby={titleId}
|
|
39
|
+
aria-describedby={description ? descriptionId : undefined}
|
|
40
|
+
>
|
|
41
|
+
<header class="svadmin-u-b6777c6db914 svadmin-u-6f7e013d6499">
|
|
42
|
+
<h1 id={titleId} class="svadmin-u-d5c9b0001e7e svadmin-u-e83a7042bc91 svadmin-u-e9fadafbd4a2 svadmin-u-d4108abe6359">{title}</h1>
|
|
43
|
+
{#if description}<p id={descriptionId} class="svadmin-u-fc7473ca09eb svadmin-u-18550d5945ae svadmin-u-bfa603190748">{description}</p>{/if}
|
|
44
|
+
</header>
|
|
34
45
|
{@render children()}
|
|
35
46
|
</section>
|
|
36
47
|
{#if footer}<div class="svadmin-u-ca6bf63030aa" data-svadmin-auth-footer>{@render footer()}</div>{/if}
|
|
@@ -790,8 +790,18 @@
|
|
|
790
790
|
detailOpenedInHistory = false;
|
|
791
791
|
localDetailId = undefined;
|
|
792
792
|
quickEditId = undefined;
|
|
793
|
-
|
|
794
|
-
|
|
793
|
+
const queryScopeChanged = previousTableScope.contract !== scope.contract
|
|
794
|
+
|| previousTableScope.resourceName !== scope.resourceName
|
|
795
|
+
|| previousTableScope.provider !== scope.provider
|
|
796
|
+
|| previousTableScope.meta !== scope.meta
|
|
797
|
+
|| previousTableScope.tenant !== scope.tenant
|
|
798
|
+
|| previousTableScope.auth !== scope.auth
|
|
799
|
+
|| previousTableScope.router !== scope.router
|
|
800
|
+
|| previousTableScope.permissionProvider !== scope.permissionProvider;
|
|
801
|
+
if (queryScopeChanged) {
|
|
802
|
+
listState.cancelSearch();
|
|
803
|
+
if (previousTableScope.resourceName !== scope.resourceName) resetToDefaultListState();
|
|
804
|
+
}
|
|
795
805
|
}
|
|
796
806
|
previousTableScope = scope;
|
|
797
807
|
});
|
|
@@ -1044,7 +1054,7 @@
|
|
|
1044
1054
|
|
|
1045
1055
|
{#snippet defaultEmptyState()}
|
|
1046
1056
|
{@const hasCriteria = !!(searchText.trim() || appliedSearchText.trim() || activeFilterCount > 0)}
|
|
1047
|
-
<DataState state=
|
|
1057
|
+
<DataState state={hasCriteria ? 'filtered-empty' : 'empty'} description={i18n.t(hasCriteria ? 'empty.description' : 'common.noDataHint')}>
|
|
1048
1058
|
{#snippet action()}
|
|
1049
1059
|
{#if hasCriteria}
|
|
1050
1060
|
<Button variant="outline" size="sm" onclick={clearFilters}>
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
title?: string;
|
|
18
18
|
columns?: 1 | 2 | 3 | 4;
|
|
19
19
|
headerActions?: Snippet;
|
|
20
|
+
formActions?: Snippet<[{ isLoading: boolean; onSubmit: () => void }]>;
|
|
20
21
|
onSuccess?: () => void;
|
|
21
22
|
class?: string;
|
|
22
23
|
}
|
|
@@ -27,6 +28,7 @@
|
|
|
27
28
|
title,
|
|
28
29
|
columns = 1,
|
|
29
30
|
headerActions,
|
|
31
|
+
formActions,
|
|
30
32
|
onSuccess,
|
|
31
33
|
class: className = '',
|
|
32
34
|
}: Props = $props();
|
|
@@ -37,26 +39,31 @@
|
|
|
37
39
|
const pageTitle = $derived(title ?? `${i18n.t('common.create')}${resource.label}`);
|
|
38
40
|
</script>
|
|
39
41
|
|
|
40
|
-
<div class="svadmin-u-b3542e058833 {className}">
|
|
41
|
-
<
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
<div class="svadmin-u-b3542e058833 {className}" data-svadmin-crud-page="create" data-svadmin-resource={resourceName}>
|
|
43
|
+
<div data-svadmin-crud-page-header>
|
|
44
|
+
<PageHeader
|
|
45
|
+
title={pageTitle}
|
|
46
|
+
onBack={() => navigateGuard(() => adminContext.navigate(`/${resourceName}`))}
|
|
47
|
+
>
|
|
45
48
|
{#snippet actions()}
|
|
46
49
|
<ListButton resource={resourceName} hideText onBeforeNavigate={navigateGuard} />
|
|
47
50
|
{#if headerActions}
|
|
48
51
|
{@render headerActions()}
|
|
49
52
|
{/if}
|
|
50
53
|
{/snippet}
|
|
51
|
-
|
|
54
|
+
</PageHeader>
|
|
55
|
+
</div>
|
|
52
56
|
|
|
53
|
-
<
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
57
|
+
<div data-svadmin-crud-page-form>
|
|
58
|
+
<AutoForm
|
|
59
|
+
{rendering}
|
|
60
|
+
{resourceName}
|
|
61
|
+
mode="create"
|
|
62
|
+
{columns}
|
|
63
|
+
showHeader={false}
|
|
64
|
+
{...definedOptions({ formActions })}
|
|
65
|
+
onNavigationGuardReady={(guard) => navigateGuard = guard}
|
|
66
|
+
{...definedOptions({ "onSuccess": onSuccess })}
|
|
67
|
+
/>
|
|
68
|
+
</div>
|
|
62
69
|
</div>
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
columns?: 1 | 2 | 3 | 4;
|
|
24
24
|
canDelete?: boolean;
|
|
25
25
|
headerActions?: Snippet;
|
|
26
|
+
formActions?: Snippet<[{ isLoading: boolean; onSubmit: () => void }]>;
|
|
26
27
|
onSuccess?: () => void;
|
|
27
28
|
class?: string;
|
|
28
29
|
}
|
|
@@ -35,6 +36,7 @@
|
|
|
35
36
|
columns = 1,
|
|
36
37
|
canDelete,
|
|
37
38
|
headerActions,
|
|
39
|
+
formActions,
|
|
38
40
|
onSuccess,
|
|
39
41
|
class: className = '',
|
|
40
42
|
}: Props = $props();
|
|
@@ -46,11 +48,12 @@
|
|
|
46
48
|
const showDelete = $derived(canDelete ?? resource.canDelete !== false);
|
|
47
49
|
</script>
|
|
48
50
|
|
|
49
|
-
<div class="svadmin-u-b3542e058833 {className}">
|
|
50
|
-
<
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
<div class="svadmin-u-b3542e058833 {className}" data-svadmin-crud-page="edit" data-svadmin-resource={resourceName} data-svadmin-record-id={id}>
|
|
52
|
+
<div data-svadmin-crud-page-header>
|
|
53
|
+
<PageHeader
|
|
54
|
+
title={pageTitle}
|
|
55
|
+
onBack={() => navigateGuard(() => navigation.list(resourceName))}
|
|
56
|
+
>
|
|
54
57
|
{#snippet actions()}
|
|
55
58
|
<ListButton resource={resourceName} hideText onBeforeNavigate={navigateGuard} />
|
|
56
59
|
<ShowButton resource={resourceName} recordItemId={id} hideText onBeforeNavigate={navigateGuard} />
|
|
@@ -62,16 +65,20 @@
|
|
|
62
65
|
{@render headerActions()}
|
|
63
66
|
{/if}
|
|
64
67
|
{/snippet}
|
|
65
|
-
|
|
68
|
+
</PageHeader>
|
|
69
|
+
</div>
|
|
66
70
|
|
|
67
|
-
<
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
71
|
+
<div data-svadmin-crud-page-form>
|
|
72
|
+
<AutoForm
|
|
73
|
+
{rendering}
|
|
74
|
+
{resourceName}
|
|
75
|
+
mode="edit"
|
|
76
|
+
{id}
|
|
77
|
+
{columns}
|
|
78
|
+
showHeader={false}
|
|
79
|
+
{...definedOptions({ formActions })}
|
|
80
|
+
onNavigationGuardReady={(guard) => navigateGuard = guard}
|
|
81
|
+
{...definedOptions({ "onSuccess": onSuccess })}
|
|
82
|
+
/>
|
|
83
|
+
</div>
|
|
77
84
|
</div>
|