mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-06-20 21:00:41 +00:00
feat: enhance backup progress handling and improve user status toggling
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user