feat: enhance backup progress handling and improve user status toggling

This commit is contained in:
shuaiplus
2026-04-07 20:58:23 +08:00
parent c9e7417825
commit 53231a4878
13 changed files with 59 additions and 39 deletions
+18 -6
View File
@@ -40,6 +40,12 @@ export default function AdminPage(props: AdminPageProps) {
return status || '-';
};
const normalizeToggleableStatus = (status: string): 'active' | 'banned' | null => {
const normalized = String(status || '').toLowerCase();
if (normalized === 'active' || normalized === 'banned') return normalized;
return null;
};
return (
<div className="stack">
<section className="card">
@@ -55,8 +61,10 @@ export default function AdminPage(props: AdminPageProps) {
</tr>
</thead>
<tbody>
{props.users.map((user) => (
<tr key={user.id}>
{props.users.map((user) => {
const toggleableStatus = normalizeToggleableStatus(user.status);
return (
<tr key={user.id}>
<td data-label={t('txt_email')}>{user.email}</td>
<td data-label={t('txt_name')}>{user.name || t('txt_dash')}</td>
<td data-label={t('txt_role')}>{roleText(user.role)}</td>
@@ -66,8 +74,11 @@ export default function AdminPage(props: AdminPageProps) {
<button
type="button"
className="btn btn-secondary"
disabled={user.id === props.currentUserId}
onClick={() => void props.onToggleUserStatus(user.id, user.status)}
disabled={user.id === props.currentUserId || !toggleableStatus}
onClick={() => {
if (!toggleableStatus) return;
void props.onToggleUserStatus(user.id, toggleableStatus);
}}
>
{user.status === 'active' ? <UserX size={14} className="btn-icon" /> : <UserCheck size={14} className="btn-icon" />}
{user.status === 'active' ? t('txt_ban') : t('txt_unban')}
@@ -80,8 +91,9 @@ export default function AdminPage(props: AdminPageProps) {
)}
</div>
</td>
</tr>
))}
</tr>
);
})}
</tbody>
</table>
</section>
+1 -1
View File
@@ -625,7 +625,7 @@ export default function BackupCenterPage(props: BackupCenterPageProps) {
setSettings(result.settings);
setSelectedDestinationId(selectedDestination.id);
await loadRemoteBrowser(selectedDestination.id, currentRemoteBrowserPath, { force: true });
props.onNotify('success', t('txt_backup_remote_run_success_verified', { name: result.fileName }));
props.onNotify('success', t('txt_backup_remote_run_success_verified', { name: result.result.fileName }));
} catch (error) {
const message = error instanceof Error ? error.message : t('txt_backup_remote_run_failed');
setLocalError(message);
+4 -3
View File
@@ -1,6 +1,7 @@
import { useEffect, useState } from 'preact/hooks';
import { Download, Eye, Lock } from 'lucide-preact';
import { accessPublicSend, accessPublicSendFile, decryptPublicSend, decryptPublicSendFileBytes } from '@/lib/api/send';
import { toBufferSource } from '@/lib/crypto';
import { downloadBytesAsFile, readResponseBytesWithProgress } from '@/lib/download';
import StandalonePageFrame from '@/components/StandalonePageFrame';
import { t } from '@/lib/i18n';
@@ -61,13 +62,13 @@ export default function PublicSendPage(props: PublicSendPageProps) {
if (props.keyPart) {
try {
const decryptedBytes = await decryptPublicSendFileBytes(encryptedBytes, props.keyPart);
blob = new Blob([decryptedBytes as unknown as BlobPart], { type: 'application/octet-stream' });
blob = new Blob([toBufferSource(decryptedBytes)], { type: 'application/octet-stream' });
} catch {
// Legacy compatibility: early web-created file sends uploaded plaintext bytes.
blob = new Blob([encryptedBytes], { type: 'application/octet-stream' });
blob = new Blob([toBufferSource(encryptedBytes)], { type: 'application/octet-stream' });
}
} else {
blob = new Blob([encryptedBytes], { type: 'application/octet-stream' });
blob = new Blob([toBufferSource(encryptedBytes)], { type: 'application/octet-stream' });
}
downloadBytesAsFile(
new Uint8Array(await blob.arrayBuffer()),
+3 -1
View File
@@ -1,3 +1,4 @@
import type { JSX } from 'preact';
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
import { Clipboard, Globe, GripVertical } from 'lucide-preact';
import {
@@ -96,6 +97,7 @@ function SortableTotpRow(props: SortableTotpRowProps) {
const { attributes, listeners, setActivatorNodeRef, setNodeRef, transform, transition, isDragging } = useSortable({
id: props.cipher.id,
});
const dragButtonAttributes = attributes as JSX.HTMLAttributes<HTMLButtonElement>;
const style = {
transform: CSS.Transform.toString(transform),
@@ -113,7 +115,7 @@ function SortableTotpRow(props: SortableTotpRowProps) {
className="btn btn-secondary small totp-drag-btn"
title={t('txt_drag_to_reorder')}
aria-label={t('txt_drag_to_reorder')}
{...attributes}
{...dragButtonAttributes}
{...listeners}
>
<GripVertical size={14} className="btn-icon" />
@@ -134,6 +134,7 @@ export function BackupDestinationDetail(props: BackupDestinationDetailProps) {
...COMMON_TIME_ZONES,
...props.availableTimeZones,
]));
const selectedIntervalHours = props.selectedDestination?.schedule.intervalHours ?? 24;
if (props.selectedRecommendedProvider) {
return (
@@ -216,7 +217,7 @@ export function BackupDestinationDetail(props: BackupDestinationDetailProps) {
type="text"
inputMode="numeric"
pattern="[0-9]*"
value={String(props.selectedDestination.schedule.intervalHours || 24)}
value={String(selectedIntervalHours)}
disabled={props.loadingSettings || props.disableWhileBusy}
onInput={(event) => {
const raw = (event.currentTarget as HTMLInputElement).value.replace(/[^\d]/g, '');
@@ -234,7 +235,7 @@ export function BackupDestinationDetail(props: BackupDestinationDetailProps) {
</div>
<div className="backup-interval-presets" aria-label={t('txt_backup_interval_hours_presets')}>
{INTERVAL_HOUR_PRESETS.map((preset) => {
const active = preset === props.selectedDestination.schedule.intervalHours;
const active = preset === selectedIntervalHours;
return (
<button
key={preset}
+3 -2
View File
@@ -1,4 +1,4 @@
import type { RefObject } from 'preact';
import type { JSX, RefObject } from 'preact';
import { CheckCheck, Download, GripVertical, Paperclip, Plus, RefreshCw, Star, StarOff, Trash2, Upload, X } from 'lucide-preact';
import { useEffect, useRef, useState } from 'preact/hooks';
import {
@@ -71,6 +71,7 @@ function SortableWebsiteRow(props: SortableWebsiteRowProps) {
const { attributes, listeners, setActivatorNodeRef, setNodeRef, transform, transition, isDragging } = useSortable({
id: props.id,
});
const dragButtonAttributes = attributes as JSX.HTMLAttributes<HTMLButtonElement>;
const style = {
transform: CSS.Transform.toString(transform),
@@ -89,7 +90,7 @@ function SortableWebsiteRow(props: SortableWebsiteRowProps) {
className="btn btn-secondary small website-drag-btn"
title={t('txt_drag_to_reorder')}
aria-label={t('txt_drag_to_reorder')}
{...attributes}
{...dragButtonAttributes}
{...listeners}
>
<GripVertical size={14} className="btn-icon" />