mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-06-20 21:00:41 +00:00
feat(vault): add folder rename action in sidebar
This commit is contained in:
@@ -1116,6 +1116,7 @@ export default function App() {
|
|||||||
onBulkMoveVaultItems: vaultSendActions.bulkMoveVaultItems,
|
onBulkMoveVaultItems: vaultSendActions.bulkMoveVaultItems,
|
||||||
onVerifyMasterPassword: vaultSendActions.verifyMasterPassword,
|
onVerifyMasterPassword: vaultSendActions.verifyMasterPassword,
|
||||||
onCreateFolder: vaultSendActions.createFolder,
|
onCreateFolder: vaultSendActions.createFolder,
|
||||||
|
onRenameFolder: vaultSendActions.renameFolder,
|
||||||
onDeleteFolder: vaultSendActions.deleteFolder,
|
onDeleteFolder: vaultSendActions.deleteFolder,
|
||||||
onBulkDeleteFolders: vaultSendActions.bulkDeleteFolders,
|
onBulkDeleteFolders: vaultSendActions.bulkDeleteFolders,
|
||||||
onDownloadVaultAttachment: vaultSendActions.downloadVaultAttachment,
|
onDownloadVaultAttachment: vaultSendActions.downloadVaultAttachment,
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ export interface AppMainRoutesProps {
|
|||||||
onBulkMoveVaultItems: (ids: string[], folderId: string | null) => Promise<void>;
|
onBulkMoveVaultItems: (ids: string[], folderId: string | null) => Promise<void>;
|
||||||
onVerifyMasterPassword: (email: string, password: string) => Promise<void>;
|
onVerifyMasterPassword: (email: string, password: string) => Promise<void>;
|
||||||
onCreateFolder: (name: string) => Promise<void>;
|
onCreateFolder: (name: string) => Promise<void>;
|
||||||
|
onRenameFolder: (folderId: string, name: string) => Promise<void>;
|
||||||
onDeleteFolder: (folderId: string) => Promise<void>;
|
onDeleteFolder: (folderId: string) => Promise<void>;
|
||||||
onBulkDeleteFolders: (folderIds: string[]) => Promise<void>;
|
onBulkDeleteFolders: (folderIds: string[]) => Promise<void>;
|
||||||
onDownloadVaultAttachment: (cipher: Cipher, attachmentId: string) => Promise<void>;
|
onDownloadVaultAttachment: (cipher: Cipher, attachmentId: string) => Promise<void>;
|
||||||
@@ -192,6 +193,7 @@ export default function AppMainRoutes(props: AppMainRoutesProps) {
|
|||||||
onVerifyMasterPassword={props.onVerifyMasterPassword}
|
onVerifyMasterPassword={props.onVerifyMasterPassword}
|
||||||
onNotify={props.onNotify}
|
onNotify={props.onNotify}
|
||||||
onCreateFolder={props.onCreateFolder}
|
onCreateFolder={props.onCreateFolder}
|
||||||
|
onRenameFolder={props.onRenameFolder}
|
||||||
onDeleteFolder={props.onDeleteFolder}
|
onDeleteFolder={props.onDeleteFolder}
|
||||||
onBulkDeleteFolders={props.onBulkDeleteFolders}
|
onBulkDeleteFolders={props.onBulkDeleteFolders}
|
||||||
onDownloadAttachment={props.onDownloadVaultAttachment}
|
onDownloadAttachment={props.onDownloadVaultAttachment}
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ interface VaultPageProps {
|
|||||||
onVerifyMasterPassword: (email: string, password: string) => Promise<void>;
|
onVerifyMasterPassword: (email: string, password: string) => Promise<void>;
|
||||||
onNotify: (type: 'success' | 'error' | 'warning', text: string) => void;
|
onNotify: (type: 'success' | 'error' | 'warning', text: string) => void;
|
||||||
onCreateFolder: (name: string) => Promise<void>;
|
onCreateFolder: (name: string) => Promise<void>;
|
||||||
|
onRenameFolder: (folderId: string, name: string) => Promise<void>;
|
||||||
onDeleteFolder: (folderId: string) => Promise<void>;
|
onDeleteFolder: (folderId: string) => Promise<void>;
|
||||||
onBulkDeleteFolders: (folderIds: string[]) => Promise<void>;
|
onBulkDeleteFolders: (folderIds: string[]) => Promise<void>;
|
||||||
onDownloadAttachment: (cipher: Cipher, attachmentId: string) => Promise<void>;
|
onDownloadAttachment: (cipher: Cipher, attachmentId: string) => Promise<void>;
|
||||||
@@ -91,6 +92,8 @@ export default function VaultPage(props: VaultPageProps) {
|
|||||||
const [moveFolderId, setMoveFolderId] = useState('__none__');
|
const [moveFolderId, setMoveFolderId] = useState('__none__');
|
||||||
const [createFolderOpen, setCreateFolderOpen] = useState(false);
|
const [createFolderOpen, setCreateFolderOpen] = useState(false);
|
||||||
const [newFolderName, setNewFolderName] = useState('');
|
const [newFolderName, setNewFolderName] = useState('');
|
||||||
|
const [pendingRenameFolder, setPendingRenameFolder] = useState<Folder | null>(null);
|
||||||
|
const [renameFolderName, setRenameFolderName] = useState('');
|
||||||
const [pendingDeleteFolder, setPendingDeleteFolder] = useState<Folder | null>(null);
|
const [pendingDeleteFolder, setPendingDeleteFolder] = useState<Folder | null>(null);
|
||||||
const [deleteAllFoldersOpen, setDeleteAllFoldersOpen] = useState(false);
|
const [deleteAllFoldersOpen, setDeleteAllFoldersOpen] = useState(false);
|
||||||
const [totpLive, setTotpLive] = useState<{ code: string; remain: number } | null>(null);
|
const [totpLive, setTotpLive] = useState<{ code: string; remain: number } | null>(null);
|
||||||
@@ -699,6 +702,23 @@ function folderName(id: string | null | undefined): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function confirmRenameFolder(): Promise<void> {
|
||||||
|
if (!pendingRenameFolder) return;
|
||||||
|
const nextName = renameFolderName.trim();
|
||||||
|
if (!nextName) {
|
||||||
|
props.onNotify('error', t('txt_folder_name_is_required'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setBusy(true);
|
||||||
|
try {
|
||||||
|
await props.onRenameFolder(pendingRenameFolder.id, nextName);
|
||||||
|
setPendingRenameFolder(null);
|
||||||
|
setRenameFolderName('');
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function confirmBulkRestore(): Promise<void> {
|
async function confirmBulkRestore(): Promise<void> {
|
||||||
const ids = Object.entries(selectedMap)
|
const ids = Object.entries(selectedMap)
|
||||||
.filter(([, selected]) => selected)
|
.filter(([, selected]) => selected)
|
||||||
@@ -806,6 +826,10 @@ function folderName(id: string | null | undefined): string {
|
|||||||
onChangeFilter={setSidebarFilter}
|
onChangeFilter={setSidebarFilter}
|
||||||
onOpenDeleteAllFolders={() => setDeleteAllFoldersOpen(true)}
|
onOpenDeleteAllFolders={() => setDeleteAllFoldersOpen(true)}
|
||||||
onOpenCreateFolder={() => setCreateFolderOpen(true)}
|
onOpenCreateFolder={() => setCreateFolderOpen(true)}
|
||||||
|
onOpenRenameFolder={(folder) => {
|
||||||
|
setPendingRenameFolder(folder);
|
||||||
|
setRenameFolderName(folder.decName || folder.name || '');
|
||||||
|
}}
|
||||||
onOpenDeleteFolder={setPendingDeleteFolder}
|
onOpenDeleteFolder={setPendingDeleteFolder}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -986,6 +1010,8 @@ function folderName(id: string | null | undefined): string {
|
|||||||
folders={props.folders}
|
folders={props.folders}
|
||||||
createFolderOpen={createFolderOpen}
|
createFolderOpen={createFolderOpen}
|
||||||
newFolderName={newFolderName}
|
newFolderName={newFolderName}
|
||||||
|
renameFolderOpen={!!pendingRenameFolder}
|
||||||
|
renameFolderName={renameFolderName}
|
||||||
pendingDeleteFolder={pendingDeleteFolder}
|
pendingDeleteFolder={pendingDeleteFolder}
|
||||||
deleteAllFoldersOpen={deleteAllFoldersOpen}
|
deleteAllFoldersOpen={deleteAllFoldersOpen}
|
||||||
repromptOpen={repromptOpen}
|
repromptOpen={repromptOpen}
|
||||||
@@ -1036,6 +1062,12 @@ function folderName(id: string | null | undefined): string {
|
|||||||
setNewFolderName('');
|
setNewFolderName('');
|
||||||
}}
|
}}
|
||||||
onNewFolderNameChange={setNewFolderName}
|
onNewFolderNameChange={setNewFolderName}
|
||||||
|
onConfirmRenameFolder={() => void confirmRenameFolder()}
|
||||||
|
onCancelRenameFolder={() => {
|
||||||
|
setPendingRenameFolder(null);
|
||||||
|
setRenameFolderName('');
|
||||||
|
}}
|
||||||
|
onRenameFolderNameChange={setRenameFolderName}
|
||||||
onConfirmDeleteFolder={() => void confirmDeleteFolder()}
|
onConfirmDeleteFolder={() => void confirmDeleteFolder()}
|
||||||
onCancelDeleteFolder={() => setPendingDeleteFolder(null)}
|
onCancelDeleteFolder={() => setPendingDeleteFolder(null)}
|
||||||
onConfirmDeleteAllFolders={() => void confirmDeleteAllFolders()}
|
onConfirmDeleteAllFolders={() => void confirmDeleteAllFolders()}
|
||||||
@@ -1051,7 +1083,3 @@ function folderName(id: string | null | undefined): string {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ interface VaultDialogsProps {
|
|||||||
folders: Folder[];
|
folders: Folder[];
|
||||||
createFolderOpen: boolean;
|
createFolderOpen: boolean;
|
||||||
newFolderName: string;
|
newFolderName: string;
|
||||||
|
renameFolderOpen: boolean;
|
||||||
|
renameFolderName: string;
|
||||||
pendingDeleteFolder: Folder | null;
|
pendingDeleteFolder: Folder | null;
|
||||||
deleteAllFoldersOpen: boolean;
|
deleteAllFoldersOpen: boolean;
|
||||||
repromptOpen: boolean;
|
repromptOpen: boolean;
|
||||||
@@ -42,6 +44,9 @@ interface VaultDialogsProps {
|
|||||||
onConfirmCreateFolder: () => void;
|
onConfirmCreateFolder: () => void;
|
||||||
onCancelCreateFolder: () => void;
|
onCancelCreateFolder: () => void;
|
||||||
onNewFolderNameChange: (value: string) => void;
|
onNewFolderNameChange: (value: string) => void;
|
||||||
|
onConfirmRenameFolder: () => void;
|
||||||
|
onCancelRenameFolder: () => void;
|
||||||
|
onRenameFolderNameChange: (value: string) => void;
|
||||||
onConfirmDeleteFolder: () => void;
|
onConfirmDeleteFolder: () => void;
|
||||||
onCancelDeleteFolder: () => void;
|
onCancelDeleteFolder: () => void;
|
||||||
onConfirmDeleteAllFolders: () => void;
|
onConfirmDeleteAllFolders: () => void;
|
||||||
@@ -150,6 +155,13 @@ export default function VaultDialogs(props: VaultDialogsProps) {
|
|||||||
</label>
|
</label>
|
||||||
</ConfirmDialog>
|
</ConfirmDialog>
|
||||||
|
|
||||||
|
<ConfirmDialog open={props.renameFolderOpen} title={t('txt_edit')} message={t('txt_enter_a_folder_name')} confirmText={t('txt_save')} cancelText={t('txt_cancel')} onConfirm={props.onConfirmRenameFolder} onCancel={props.onCancelRenameFolder}>
|
||||||
|
<label className="field">
|
||||||
|
<span>{t('txt_folder_name')}</span>
|
||||||
|
<input className="input" value={props.renameFolderName} onInput={(e) => props.onRenameFolderNameChange((e.currentTarget as HTMLInputElement).value)} />
|
||||||
|
</label>
|
||||||
|
</ConfirmDialog>
|
||||||
|
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
open={!!props.pendingDeleteFolder}
|
open={!!props.pendingDeleteFolder}
|
||||||
title={t('txt_delete_folder')}
|
title={t('txt_delete_folder')}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
Globe,
|
Globe,
|
||||||
KeyRound,
|
KeyRound,
|
||||||
LayoutGrid,
|
LayoutGrid,
|
||||||
|
Pencil,
|
||||||
ShieldUser,
|
ShieldUser,
|
||||||
Star,
|
Star,
|
||||||
StickyNote,
|
StickyNote,
|
||||||
@@ -28,6 +29,7 @@ interface VaultSidebarProps {
|
|||||||
onChangeFilter: (filter: SidebarFilter) => void;
|
onChangeFilter: (filter: SidebarFilter) => void;
|
||||||
onOpenDeleteAllFolders: () => void;
|
onOpenDeleteAllFolders: () => void;
|
||||||
onOpenCreateFolder: () => void;
|
onOpenCreateFolder: () => void;
|
||||||
|
onOpenRenameFolder: (folder: Folder) => void;
|
||||||
onOpenDeleteFolder: (folder: Folder) => void;
|
onOpenDeleteFolder: (folder: Folder) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,6 +115,20 @@ export default function VaultSidebar(props: VaultSidebarProps) {
|
|||||||
{folder.decName || folder.name || folder.id}
|
{folder.decName || folder.name || folder.id}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="folder-delete-btn folder-edit-btn"
|
||||||
|
title={t('txt_edit')}
|
||||||
|
aria-label={t('txt_edit')}
|
||||||
|
disabled={props.busy}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
props.onOpenRenameFolder(folder);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Pencil size={12} />
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="folder-delete-btn"
|
className="folder-delete-btn"
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import {
|
|||||||
type CiphersImportPayload,
|
type CiphersImportPayload,
|
||||||
type ImportedCipherMapEntry,
|
type ImportedCipherMapEntry,
|
||||||
updateCipher,
|
updateCipher,
|
||||||
|
updateFolder,
|
||||||
unarchiveCipher,
|
unarchiveCipher,
|
||||||
uploadCipherAttachment,
|
uploadCipherAttachment,
|
||||||
} from '@/lib/api/vault';
|
} from '@/lib/api/vault';
|
||||||
@@ -340,6 +341,28 @@ export default function useVaultSendActions(options: UseVaultSendActionsOptions)
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async renameFolder(folderId: string, name: string) {
|
||||||
|
const id = String(folderId || '').trim();
|
||||||
|
const nextName = String(name || '').trim();
|
||||||
|
if (!id) {
|
||||||
|
onNotify('error', t('txt_folder_not_found'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!nextName) {
|
||||||
|
onNotify('error', t('txt_folder_name_is_required'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (!session) throw new Error(t('txt_vault_key_unavailable'));
|
||||||
|
await updateFolder(authedFetch, session, id, nextName);
|
||||||
|
await refetchFolders();
|
||||||
|
onNotify('success', t('txt_folder_updated'));
|
||||||
|
} catch (error) {
|
||||||
|
onNotify('error', error instanceof Error ? error.message : t('txt_update_folder_failed'));
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
async bulkRestoreVaultItems(ids: string[]) {
|
async bulkRestoreVaultItems(ids: string[]) {
|
||||||
try {
|
try {
|
||||||
await bulkRestoreCiphers(authedFetch, ids);
|
await bulkRestoreCiphers(authedFetch, ids);
|
||||||
|
|||||||
@@ -1493,7 +1493,9 @@ messages.en.txt_delete_all_folders = 'Delete All Folders';
|
|||||||
messages.en.txt_delete_all_folders_message = 'Delete all folders? Items inside will move to No Folder.';
|
messages.en.txt_delete_all_folders_message = 'Delete all folders? Items inside will move to No Folder.';
|
||||||
messages.en.txt_folder_not_found = 'Folder not found';
|
messages.en.txt_folder_not_found = 'Folder not found';
|
||||||
messages.en.txt_folder_deleted = 'Folder deleted';
|
messages.en.txt_folder_deleted = 'Folder deleted';
|
||||||
|
messages.en.txt_folder_updated = 'Folder updated';
|
||||||
messages.en.txt_folders_deleted = 'Folders deleted';
|
messages.en.txt_folders_deleted = 'Folders deleted';
|
||||||
|
messages.en.txt_update_folder_failed = 'Update folder failed';
|
||||||
messages.en.txt_delete_folder_failed = 'Delete folder failed';
|
messages.en.txt_delete_folder_failed = 'Delete folder failed';
|
||||||
messages.en.txt_delete_all_folders_failed = 'Delete all folders failed';
|
messages.en.txt_delete_all_folders_failed = 'Delete all folders failed';
|
||||||
messages.en.txt_other = 'Other';
|
messages.en.txt_other = 'Other';
|
||||||
@@ -1575,7 +1577,9 @@ zhCNOverrides.txt_delete_all_folders = '删除全部文件夹';
|
|||||||
zhCNOverrides.txt_delete_all_folders_message = '确认删除全部文件夹吗?其中的项目将移至无文件夹。';
|
zhCNOverrides.txt_delete_all_folders_message = '确认删除全部文件夹吗?其中的项目将移至无文件夹。';
|
||||||
zhCNOverrides.txt_folder_not_found = '文件夹不存在';
|
zhCNOverrides.txt_folder_not_found = '文件夹不存在';
|
||||||
zhCNOverrides.txt_folder_deleted = '文件夹已删除';
|
zhCNOverrides.txt_folder_deleted = '文件夹已删除';
|
||||||
|
zhCNOverrides.txt_folder_updated = '文件夹已重命名';
|
||||||
zhCNOverrides.txt_folders_deleted = '文件夹已删除';
|
zhCNOverrides.txt_folders_deleted = '文件夹已删除';
|
||||||
|
zhCNOverrides.txt_update_folder_failed = '重命名文件夹失败';
|
||||||
zhCNOverrides.txt_delete_folder_failed = '删除文件夹失败';
|
zhCNOverrides.txt_delete_folder_failed = '删除文件夹失败';
|
||||||
zhCNOverrides.txt_delete_all_folders_failed = '删除全部文件夹失败';
|
zhCNOverrides.txt_delete_all_folders_failed = '删除全部文件夹失败';
|
||||||
zhCNOverrides.txt_other = '其他';
|
zhCNOverrides.txt_other = '其他';
|
||||||
@@ -1629,4 +1633,3 @@ export function setLocale(next: Locale): void {
|
|||||||
// ignore storage errors
|
// ignore storage errors
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1164,6 +1164,11 @@ input[type='file'].input::file-selector-button:hover {
|
|||||||
transform: scale(1.06);
|
transform: scale(1.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.folder-edit-btn:hover {
|
||||||
|
color: #1d4ed8;
|
||||||
|
background: #dbeafe;
|
||||||
|
}
|
||||||
|
|
||||||
.list-col {
|
.list-col {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
Reference in New Issue
Block a user