mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-06-20 13:00:39 +00:00
feat: add restore functionality for deleted items with corresponding UI updates
This commit is contained in:
+3
-2
@@ -69,7 +69,7 @@ import {
|
|||||||
createDemoMainRoutesProps,
|
createDemoMainRoutesProps,
|
||||||
} from '@/lib/demo';
|
} from '@/lib/demo';
|
||||||
import type { AdminBackupSettings } from '@/lib/api/backup';
|
import type { AdminBackupSettings } from '@/lib/api/backup';
|
||||||
import type { AdminInvite, AdminUser, AppPhase, AuthorizedDevice, Cipher, CustomEquivalentDomain, DomainRules, Folder as VaultFolder, Profile, Send, SessionState } from '@/lib/types';
|
import type { AdminInvite, AdminUser, AppPhase, AuditLogSettings, AuthorizedDevice, Cipher, CustomEquivalentDomain, DomainRules, Folder as VaultFolder, Profile, Send, SessionState } from '@/lib/types';
|
||||||
import type { VaultCoreSnapshot } from '@/lib/vault-cache';
|
import type { VaultCoreSnapshot } from '@/lib/vault-cache';
|
||||||
|
|
||||||
function isBackupProgressDetail(value: unknown): value is BackupProgressDetail {
|
function isBackupProgressDetail(value: unknown): value is BackupProgressDetail {
|
||||||
@@ -1477,6 +1477,7 @@ export default function App() {
|
|||||||
onDeleteVaultItem: vaultSendActions.deleteVaultItem,
|
onDeleteVaultItem: vaultSendActions.deleteVaultItem,
|
||||||
onArchiveVaultItem: vaultSendActions.archiveVaultItem,
|
onArchiveVaultItem: vaultSendActions.archiveVaultItem,
|
||||||
onUnarchiveVaultItem: vaultSendActions.unarchiveVaultItem,
|
onUnarchiveVaultItem: vaultSendActions.unarchiveVaultItem,
|
||||||
|
onRestoreVaultItems: vaultSendActions.bulkRestoreVaultItems,
|
||||||
onBulkDeleteVaultItems: vaultSendActions.bulkDeleteVaultItems,
|
onBulkDeleteVaultItems: vaultSendActions.bulkDeleteVaultItems,
|
||||||
onBulkPermanentDeleteVaultItems: vaultSendActions.bulkPermanentDeleteVaultItems,
|
onBulkPermanentDeleteVaultItems: vaultSendActions.bulkPermanentDeleteVaultItems,
|
||||||
onBulkRestoreVaultItems: vaultSendActions.bulkRestoreVaultItems,
|
onBulkRestoreVaultItems: vaultSendActions.bulkRestoreVaultItems,
|
||||||
@@ -1531,7 +1532,7 @@ export default function App() {
|
|||||||
onRevokeInvite: adminActions.revokeInvite,
|
onRevokeInvite: adminActions.revokeInvite,
|
||||||
onLoadAuditLogs: (filters: AuditLogFilters) => listAuditLogs(authedFetch, filters),
|
onLoadAuditLogs: (filters: AuditLogFilters) => listAuditLogs(authedFetch, filters),
|
||||||
onLoadAuditLogSettings: () => getAuditLogSettings(authedFetch),
|
onLoadAuditLogSettings: () => getAuditLogSettings(authedFetch),
|
||||||
onSaveAuditLogSettings: (settings) => saveAuditLogSettings(authedFetch, settings),
|
onSaveAuditLogSettings: (settings: AuditLogSettings) => saveAuditLogSettings(authedFetch, settings),
|
||||||
onClearAuditLogs: () => clearAuditLogs(authedFetch),
|
onClearAuditLogs: () => clearAuditLogs(authedFetch),
|
||||||
onExportBackup: backupActions.exportBackup,
|
onExportBackup: backupActions.exportBackup,
|
||||||
onImportBackup: backupActions.importBackup,
|
onImportBackup: backupActions.importBackup,
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ export interface AppMainRoutesProps {
|
|||||||
onDeleteVaultItem: (cipher: Cipher) => Promise<void>;
|
onDeleteVaultItem: (cipher: Cipher) => Promise<void>;
|
||||||
onArchiveVaultItem: (cipher: Cipher) => Promise<void>;
|
onArchiveVaultItem: (cipher: Cipher) => Promise<void>;
|
||||||
onUnarchiveVaultItem: (cipher: Cipher) => Promise<void>;
|
onUnarchiveVaultItem: (cipher: Cipher) => Promise<void>;
|
||||||
|
onRestoreVaultItems: (ids: string[]) => Promise<void>;
|
||||||
onBulkDeleteVaultItems: (ids: string[]) => Promise<void>;
|
onBulkDeleteVaultItems: (ids: string[]) => Promise<void>;
|
||||||
onBulkPermanentDeleteVaultItems: (ids: string[]) => Promise<void>;
|
onBulkPermanentDeleteVaultItems: (ids: string[]) => Promise<void>;
|
||||||
onBulkRestoreVaultItems: (ids: string[]) => Promise<void>;
|
onBulkRestoreVaultItems: (ids: string[]) => Promise<void>;
|
||||||
@@ -214,6 +215,7 @@ export default function AppMainRoutes(props: AppMainRoutesProps) {
|
|||||||
onDelete={props.onDeleteVaultItem}
|
onDelete={props.onDeleteVaultItem}
|
||||||
onArchive={props.onArchiveVaultItem}
|
onArchive={props.onArchiveVaultItem}
|
||||||
onUnarchive={props.onUnarchiveVaultItem}
|
onUnarchive={props.onUnarchiveVaultItem}
|
||||||
|
onRestore={props.onRestoreVaultItems}
|
||||||
onBulkDelete={props.onBulkDeleteVaultItems}
|
onBulkDelete={props.onBulkDeleteVaultItems}
|
||||||
onBulkPermanentDelete={props.onBulkPermanentDeleteVaultItems}
|
onBulkPermanentDelete={props.onBulkPermanentDeleteVaultItems}
|
||||||
onBulkRestore={props.onBulkRestoreVaultItems}
|
onBulkRestore={props.onBulkRestoreVaultItems}
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ interface VaultPageProps {
|
|||||||
onDelete: (cipher: Cipher) => Promise<void>;
|
onDelete: (cipher: Cipher) => Promise<void>;
|
||||||
onArchive: (cipher: Cipher) => Promise<void>;
|
onArchive: (cipher: Cipher) => Promise<void>;
|
||||||
onUnarchive: (cipher: Cipher) => Promise<void>;
|
onUnarchive: (cipher: Cipher) => Promise<void>;
|
||||||
|
onRestore: (ids: string[]) => Promise<void>;
|
||||||
onBulkDelete: (ids: string[]) => Promise<void>;
|
onBulkDelete: (ids: string[]) => Promise<void>;
|
||||||
onBulkPermanentDelete: (ids: string[]) => Promise<void>;
|
onBulkPermanentDelete: (ids: string[]) => Promise<void>;
|
||||||
onBulkRestore: (ids: string[]) => Promise<void>;
|
onBulkRestore: (ids: string[]) => Promise<void>;
|
||||||
@@ -732,6 +733,18 @@ const folderName = useCallback((id: string | null | undefined): string => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleRestoreSelected(cipher: Cipher): Promise<void> {
|
||||||
|
setBusy(true);
|
||||||
|
try {
|
||||||
|
await props.onRestore([cipher.id]);
|
||||||
|
if (isMobileLayout && selectedCipherId === cipher.id) {
|
||||||
|
setMobilePanel('list');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function confirmBulkDelete(): Promise<void> {
|
async function confirmBulkDelete(): Promise<void> {
|
||||||
const ids = Object.entries(selectedMap)
|
const ids = Object.entries(selectedMap)
|
||||||
.filter(([, selected]) => selected)
|
.filter(([, selected]) => selected)
|
||||||
@@ -1148,6 +1161,7 @@ const folderName = useCallback((id: string | null | undefined): string => {
|
|||||||
attachmentDownloadPercent={props.attachmentDownloadPercent}
|
attachmentDownloadPercent={props.attachmentDownloadPercent}
|
||||||
onStartEdit={startEdit}
|
onStartEdit={startEdit}
|
||||||
onDelete={setPendingDelete}
|
onDelete={setPendingDelete}
|
||||||
|
onRestore={(cipher) => void handleRestoreSelected(cipher)}
|
||||||
onArchive={(cipher) => setPendingArchive(cipher)}
|
onArchive={(cipher) => setPendingArchive(cipher)}
|
||||||
onUnarchive={(cipher) => void handleUnarchiveSelected(cipher)}
|
onUnarchive={(cipher) => void handleUnarchiveSelected(cipher)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
formatAttachmentSize,
|
formatAttachmentSize,
|
||||||
formatHistoryTime,
|
formatHistoryTime,
|
||||||
formatTotp,
|
formatTotp,
|
||||||
|
isCipherDeleted,
|
||||||
maskSecret,
|
maskSecret,
|
||||||
openUri,
|
openUri,
|
||||||
parseFieldType,
|
parseFieldType,
|
||||||
@@ -36,6 +37,7 @@ interface VaultDetailViewProps {
|
|||||||
onDownloadAttachment: (cipher: Cipher, attachmentId: string) => void;
|
onDownloadAttachment: (cipher: Cipher, attachmentId: string) => void;
|
||||||
onStartEdit: () => void;
|
onStartEdit: () => void;
|
||||||
onDelete: (cipher: Cipher) => void;
|
onDelete: (cipher: Cipher) => void;
|
||||||
|
onRestore: (cipher: Cipher) => void | Promise<void>;
|
||||||
onArchive: (cipher: Cipher) => void | Promise<void>;
|
onArchive: (cipher: Cipher) => void | Promise<void>;
|
||||||
onUnarchive: (cipher: Cipher) => void | Promise<void>;
|
onUnarchive: (cipher: Cipher) => void | Promise<void>;
|
||||||
}
|
}
|
||||||
@@ -84,6 +86,7 @@ export default function VaultDetailView(props: VaultDetailViewProps) {
|
|||||||
const [showSshPrivateKey, setShowSshPrivateKey] = useState(false);
|
const [showSshPrivateKey, setShowSshPrivateKey] = useState(false);
|
||||||
const [passwordHistoryOpen, setPasswordHistoryOpen] = useState(false);
|
const [passwordHistoryOpen, setPasswordHistoryOpen] = useState(false);
|
||||||
const isArchived = !!(props.selectedCipher.archivedDate || (props.selectedCipher as { archivedAt?: string | null }).archivedAt);
|
const isArchived = !!(props.selectedCipher.archivedDate || (props.selectedCipher as { archivedAt?: string | null }).archivedAt);
|
||||||
|
const isDeleted = isCipherDeleted(props.selectedCipher);
|
||||||
const passwordHistoryEntries = useMemo(
|
const passwordHistoryEntries = useMemo(
|
||||||
() =>
|
() =>
|
||||||
(props.selectedCipher.passwordHistory || [])
|
(props.selectedCipher.passwordHistory || [])
|
||||||
@@ -446,21 +449,29 @@ export default function VaultDetailView(props: VaultDetailViewProps) {
|
|||||||
|
|
||||||
<div className="detail-actions">
|
<div className="detail-actions">
|
||||||
<div className="actions">
|
<div className="actions">
|
||||||
<button type="button" className="btn btn-secondary" onClick={props.onStartEdit}>
|
{isDeleted ? (
|
||||||
<Pencil size={14} className="btn-icon" /> {t('txt_edit')}
|
<button type="button" className="btn btn-secondary" onClick={() => void props.onRestore(props.selectedCipher)}>
|
||||||
</button>
|
<RotateCcw size={14} className="btn-icon" /> {t('txt_restore')}
|
||||||
{isArchived ? (
|
|
||||||
<button type="button" className="btn btn-secondary" onClick={() => void props.onUnarchive(props.selectedCipher)}>
|
|
||||||
<RotateCcw size={14} className="btn-icon" /> {t('txt_unarchive')}
|
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<button type="button" className="btn btn-secondary" onClick={() => void props.onArchive(props.selectedCipher)}>
|
<>
|
||||||
<Archive size={14} className="btn-icon" /> {t('txt_archive')}
|
<button type="button" className="btn btn-secondary" onClick={props.onStartEdit}>
|
||||||
</button>
|
<Pencil size={14} className="btn-icon" /> {t('txt_edit')}
|
||||||
|
</button>
|
||||||
|
{isArchived ? (
|
||||||
|
<button type="button" className="btn btn-secondary" onClick={() => void props.onUnarchive(props.selectedCipher)}>
|
||||||
|
<RotateCcw size={14} className="btn-icon" /> {t('txt_unarchive')}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button type="button" className="btn btn-secondary" onClick={() => void props.onArchive(props.selectedCipher)}>
|
||||||
|
<Archive size={14} className="btn-icon" /> {t('txt_archive')}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<button type="button" className="btn btn-danger" onClick={() => props.onDelete(props.selectedCipher)}>
|
<button type="button" className="btn btn-danger" onClick={() => props.onDelete(props.selectedCipher)}>
|
||||||
<Trash2 size={14} className="btn-icon" /> {t('txt_delete')}
|
<Trash2 size={14} className="btn-icon" /> {isDeleted ? t('txt_delete_permanently') : t('txt_delete')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import {
|
|||||||
encryptFolderImportName,
|
encryptFolderImportName,
|
||||||
getAttachmentDownloadInfo,
|
getAttachmentDownloadInfo,
|
||||||
importCiphers,
|
importCiphers,
|
||||||
|
permanentDeleteCipher,
|
||||||
type CiphersImportPayload,
|
type CiphersImportPayload,
|
||||||
type ImportedCipherMapEntry,
|
type ImportedCipherMapEntry,
|
||||||
updateCipher,
|
updateCipher,
|
||||||
@@ -490,6 +491,18 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
|
|||||||
|
|
||||||
async deleteVaultItem(cipher: Cipher) {
|
async deleteVaultItem(cipher: Cipher) {
|
||||||
const previousCipher = { ...cipher };
|
const previousCipher = { ...cipher };
|
||||||
|
if (cipher.deletedDate || (cipher as { deletedAt?: string | null }).deletedAt) {
|
||||||
|
try {
|
||||||
|
await permanentDeleteCipher(authedFetch, cipher.id);
|
||||||
|
patchCipherBatch([cipher.id], () => null);
|
||||||
|
syncVaultCoreInBackground({ includeFolders: true });
|
||||||
|
onNotify('success', t('txt_item_deleted_permanently'));
|
||||||
|
} catch (error) {
|
||||||
|
onNotify('error', error instanceof Error ? error.message : t('txt_permanent_delete_item_failed'));
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
const deletedDate = new Date().toISOString();
|
const deletedDate = new Date().toISOString();
|
||||||
patchCipherBatch([cipher.id], (current) => ({ ...current, deletedDate, archivedDate: null, revisionDate: deletedDate }));
|
patchCipherBatch([cipher.id], (current) => ({ ...current, deletedDate, archivedDate: null, revisionDate: deletedDate }));
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -803,6 +803,13 @@ export async function deleteCipher(authedFetch: AuthedFetch, cipherId: string):
|
|||||||
return (await parseJson<Cipher>(resp))!;
|
return (await parseJson<Cipher>(resp))!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function permanentDeleteCipher(authedFetch: AuthedFetch, cipherId: string): Promise<void> {
|
||||||
|
const id = String(cipherId || '').trim();
|
||||||
|
if (!id) throw new Error('Cipher id is required');
|
||||||
|
const resp = await authedFetch(`/api/ciphers/${encodeURIComponent(id)}/delete`, { method: 'DELETE' });
|
||||||
|
if (!resp.ok) throw new Error('Permanent delete item failed');
|
||||||
|
}
|
||||||
|
|
||||||
export async function archiveCipher(authedFetch: AuthedFetch, cipherId: string): Promise<Cipher> {
|
export async function archiveCipher(authedFetch: AuthedFetch, cipherId: string): Promise<Cipher> {
|
||||||
const id = String(cipherId || '').trim();
|
const id = String(cipherId || '').trim();
|
||||||
if (!id) throw new Error('Cipher id is required');
|
if (!id) throw new Error('Cipher id is required');
|
||||||
|
|||||||
@@ -932,6 +932,11 @@ export function createDemoMainRoutesProps(base: AppMainRoutesProps, notify: Noti
|
|||||||
notify('success', t('txt_item_updated'));
|
notify('success', t('txt_item_updated'));
|
||||||
},
|
},
|
||||||
onDeleteVaultItem: async (cipher) => {
|
onDeleteVaultItem: async (cipher) => {
|
||||||
|
if (cipher.deletedDate || (cipher as { deletedAt?: string | null }).deletedAt) {
|
||||||
|
state.setCiphers((prev) => prev.filter((item) => item.id !== cipher.id));
|
||||||
|
notify('success', t('txt_item_deleted_permanently'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
const deletedDate = new Date().toISOString();
|
const deletedDate = new Date().toISOString();
|
||||||
state.setCiphers((prev) => prev.map((item) => (
|
state.setCiphers((prev) => prev.map((item) => (
|
||||||
item.id === cipher.id ? { ...item, deletedDate, archivedDate: null, revisionDate: deletedDate } : item
|
item.id === cipher.id ? { ...item, deletedDate, archivedDate: null, revisionDate: deletedDate } : item
|
||||||
@@ -965,6 +970,11 @@ export function createDemoMainRoutesProps(base: AppMainRoutesProps, notify: Noti
|
|||||||
state.setCiphers((prev) => prev.filter((item) => !idSet.has(item.id)));
|
state.setCiphers((prev) => prev.filter((item) => !idSet.has(item.id)));
|
||||||
notify('success', t('txt_deleted_selected_items_permanently'));
|
notify('success', t('txt_deleted_selected_items_permanently'));
|
||||||
},
|
},
|
||||||
|
onRestoreVaultItems: async (ids) => {
|
||||||
|
const idSet = new Set(ids);
|
||||||
|
state.setCiphers((prev) => prev.map((item) => (idSet.has(item.id) ? { ...item, deletedDate: null } : item)));
|
||||||
|
notify('success', t('txt_restored_selected_items'));
|
||||||
|
},
|
||||||
onBulkRestoreVaultItems: async (ids) => {
|
onBulkRestoreVaultItems: async (ids) => {
|
||||||
const idSet = new Set(ids);
|
const idSet = new Set(ids);
|
||||||
state.setCiphers((prev) => prev.map((item) => (idSet.has(item.id) ? { ...item, deletedDate: null } : item)));
|
state.setCiphers((prev) => prev.map((item) => (idSet.has(item.id) ? { ...item, deletedDate: null } : item)));
|
||||||
|
|||||||
@@ -369,6 +369,7 @@ const en: Record<string, string> = {
|
|||||||
"txt_delete_item": "Delete Item",
|
"txt_delete_item": "Delete Item",
|
||||||
"txt_delete_passkey": "Delete Passkey",
|
"txt_delete_passkey": "Delete Passkey",
|
||||||
"txt_delete_item_failed": "Delete item failed",
|
"txt_delete_item_failed": "Delete item failed",
|
||||||
|
"txt_permanent_delete_item_failed": "Permanent delete item failed",
|
||||||
"txt_delete_permanently": "Delete Permanently",
|
"txt_delete_permanently": "Delete Permanently",
|
||||||
"txt_archive": "Archive",
|
"txt_archive": "Archive",
|
||||||
"txt_archive_item": "Archive Item",
|
"txt_archive_item": "Archive Item",
|
||||||
@@ -501,6 +502,7 @@ const en: Record<string, string> = {
|
|||||||
"txt_item": "Item",
|
"txt_item": "Item",
|
||||||
"txt_item_created": "Item created",
|
"txt_item_created": "Item created",
|
||||||
"txt_item_deleted": "Item deleted",
|
"txt_item_deleted": "Item deleted",
|
||||||
|
"txt_item_deleted_permanently": "Item permanently deleted",
|
||||||
"txt_item_history": "Item History",
|
"txt_item_history": "Item History",
|
||||||
"txt_password_history": "Password History",
|
"txt_password_history": "Password History",
|
||||||
"txt_password_updated_value": "Password updated: {value}",
|
"txt_password_updated_value": "Password updated: {value}",
|
||||||
|
|||||||
@@ -369,6 +369,7 @@ const es: Record<string, string> = {
|
|||||||
"txt_delete_item": "Eliminar elemento",
|
"txt_delete_item": "Eliminar elemento",
|
||||||
"txt_delete_passkey": "Eliminar clave de acceso",
|
"txt_delete_passkey": "Eliminar clave de acceso",
|
||||||
"txt_delete_item_failed": "Error al eliminar elemento",
|
"txt_delete_item_failed": "Error al eliminar elemento",
|
||||||
|
"txt_permanent_delete_item_failed": "Error al eliminar elemento permanentemente",
|
||||||
"txt_delete_permanently": "Eliminar permanentemente",
|
"txt_delete_permanently": "Eliminar permanentemente",
|
||||||
"txt_archive": "Archivar",
|
"txt_archive": "Archivar",
|
||||||
"txt_archive_item": "Archivar elemento",
|
"txt_archive_item": "Archivar elemento",
|
||||||
@@ -501,6 +502,7 @@ const es: Record<string, string> = {
|
|||||||
"txt_item": "Elemento",
|
"txt_item": "Elemento",
|
||||||
"txt_item_created": "Elemento creado",
|
"txt_item_created": "Elemento creado",
|
||||||
"txt_item_deleted": "Elemento eliminado",
|
"txt_item_deleted": "Elemento eliminado",
|
||||||
|
"txt_item_deleted_permanently": "Elemento eliminado permanentemente",
|
||||||
"txt_item_history": "Historial del elemento",
|
"txt_item_history": "Historial del elemento",
|
||||||
"txt_password_history": "Historial de contraseñas",
|
"txt_password_history": "Historial de contraseñas",
|
||||||
"txt_password_updated_value": "Contraseña actualizada: {value}",
|
"txt_password_updated_value": "Contraseña actualizada: {value}",
|
||||||
|
|||||||
@@ -369,6 +369,7 @@ const ru: Record<string, string> = {
|
|||||||
"txt_delete_item": "Удалить элемент",
|
"txt_delete_item": "Удалить элемент",
|
||||||
"txt_delete_passkey": "Удалить пароль",
|
"txt_delete_passkey": "Удалить пароль",
|
||||||
"txt_delete_item_failed": "Удалить элемент не удалось",
|
"txt_delete_item_failed": "Удалить элемент не удалось",
|
||||||
|
"txt_permanent_delete_item_failed": "Не удалось окончательно удалить элемент",
|
||||||
"txt_delete_permanently": "Удалить навсегда",
|
"txt_delete_permanently": "Удалить навсегда",
|
||||||
"txt_archive": "Архив",
|
"txt_archive": "Архив",
|
||||||
"txt_archive_item": "Архивный элемент",
|
"txt_archive_item": "Архивный элемент",
|
||||||
@@ -501,6 +502,7 @@ const ru: Record<string, string> = {
|
|||||||
"txt_item": "Товар",
|
"txt_item": "Товар",
|
||||||
"txt_item_created": "Объект создан",
|
"txt_item_created": "Объект создан",
|
||||||
"txt_item_deleted": "Объект удален.",
|
"txt_item_deleted": "Объект удален.",
|
||||||
|
"txt_item_deleted_permanently": "Объект окончательно удален.",
|
||||||
"txt_item_history": "История предмета",
|
"txt_item_history": "История предмета",
|
||||||
"txt_password_history": "История паролей",
|
"txt_password_history": "История паролей",
|
||||||
"txt_password_updated_value": "Пароль обновлен: {value}",
|
"txt_password_updated_value": "Пароль обновлен: {value}",
|
||||||
|
|||||||
@@ -369,6 +369,7 @@ const zhCN: Record<string, string> = {
|
|||||||
"txt_delete_item": "删除项目",
|
"txt_delete_item": "删除项目",
|
||||||
"txt_delete_passkey": "删除通行密钥",
|
"txt_delete_passkey": "删除通行密钥",
|
||||||
"txt_delete_item_failed": "删除项目失败",
|
"txt_delete_item_failed": "删除项目失败",
|
||||||
|
"txt_permanent_delete_item_failed": "永久删除项目失败",
|
||||||
"txt_delete_permanently": "永久删除",
|
"txt_delete_permanently": "永久删除",
|
||||||
"txt_archive": "归档",
|
"txt_archive": "归档",
|
||||||
"txt_archive_item": "归档项目",
|
"txt_archive_item": "归档项目",
|
||||||
@@ -501,6 +502,7 @@ const zhCN: Record<string, string> = {
|
|||||||
"txt_item": "项目",
|
"txt_item": "项目",
|
||||||
"txt_item_created": "项目已创建",
|
"txt_item_created": "项目已创建",
|
||||||
"txt_item_deleted": "项目已删除",
|
"txt_item_deleted": "项目已删除",
|
||||||
|
"txt_item_deleted_permanently": "项目已永久删除",
|
||||||
"txt_item_history": "项目历史",
|
"txt_item_history": "项目历史",
|
||||||
"txt_password_history": "密码历史记录",
|
"txt_password_history": "密码历史记录",
|
||||||
"txt_password_updated_value": "密码更新于: {value}",
|
"txt_password_updated_value": "密码更新于: {value}",
|
||||||
|
|||||||
@@ -369,6 +369,7 @@ const zhTW: Record<string, string> = {
|
|||||||
"txt_delete_item": "刪除項目",
|
"txt_delete_item": "刪除項目",
|
||||||
"txt_delete_passkey": "刪除通行密鑰",
|
"txt_delete_passkey": "刪除通行密鑰",
|
||||||
"txt_delete_item_failed": "刪除項目失敗",
|
"txt_delete_item_failed": "刪除項目失敗",
|
||||||
|
"txt_permanent_delete_item_failed": "永久刪除項目失敗",
|
||||||
"txt_delete_permanently": "永久刪除",
|
"txt_delete_permanently": "永久刪除",
|
||||||
"txt_archive": "歸檔",
|
"txt_archive": "歸檔",
|
||||||
"txt_archive_item": "歸檔項目",
|
"txt_archive_item": "歸檔項目",
|
||||||
@@ -501,6 +502,7 @@ const zhTW: Record<string, string> = {
|
|||||||
"txt_item": "項目",
|
"txt_item": "項目",
|
||||||
"txt_item_created": "項目已創建",
|
"txt_item_created": "項目已創建",
|
||||||
"txt_item_deleted": "項目已刪除",
|
"txt_item_deleted": "項目已刪除",
|
||||||
|
"txt_item_deleted_permanently": "項目已永久刪除",
|
||||||
"txt_item_history": "項目歷史",
|
"txt_item_history": "項目歷史",
|
||||||
"txt_password_history": "密碼歷史記錄",
|
"txt_password_history": "密碼歷史記錄",
|
||||||
"txt_password_updated_value": "密碼更新新於: {value}",
|
"txt_password_updated_value": "密碼更新新於: {value}",
|
||||||
|
|||||||
Reference in New Issue
Block a user