refactor(ext/vault): extract vault-drawer.ts + ensureDrawerClosedForRoute (Plan C Phase 4)
Moves the drawer (open/close/render + getDrawerCoreFields + selectItemForDrawer) out of vault.ts into vault-drawer.ts, taking the VaultController ctx. Adds ensureDrawerClosedForRoute(state, route) — called in renderPane before the view switch — so drawer state cannot leak across navigation to non-list/detail routes (P2 safety net). New drawer-state.test.ts covers it (TDD). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -31,6 +31,10 @@ import {
|
||||
} from './vault-shell';
|
||||
import { wireSidebar, renderSidebarCategories } from './vault-sidebar';
|
||||
import { renderListPane } from './vault-list';
|
||||
import {
|
||||
openDrawer, closeDrawer, renderDrawer, selectItemForDrawer,
|
||||
ensureDrawerClosedForRoute,
|
||||
} from './vault-drawer';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
@@ -146,12 +150,12 @@ const ctx: VaultController = {
|
||||
renderPane: () => renderPane(),
|
||||
renderListPane: () => renderListPane(ctx),
|
||||
renderSidebarCategories: () => renderSidebarCategories(ctx),
|
||||
renderDrawer: (item) => renderDrawer(item),
|
||||
renderDrawer: (item) => renderDrawer(ctx, item),
|
||||
applyShellViewClass: () => applyShellViewClass(ctx),
|
||||
setHash,
|
||||
openDrawer: () => openDrawer(),
|
||||
closeDrawer: () => closeDrawer(),
|
||||
selectItemForDrawer: (id) => selectItemForDrawer(id),
|
||||
closeDrawer: () => closeDrawer(ctx),
|
||||
selectItemForDrawer: (id) => selectItemForDrawer(ctx, id),
|
||||
openTypePanel: () => openTypePanel(ctx),
|
||||
closeTypePanel: () => closeTypePanel(ctx),
|
||||
wireSidebar: () => wireSidebar(ctx),
|
||||
@@ -183,132 +187,6 @@ registerHost({
|
||||
openVaultTab: () => {},
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Drawer (implemented in Task 10)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function openDrawer(): void {
|
||||
document.getElementById('vault-drawer')?.classList.add('vault-drawer--open');
|
||||
}
|
||||
|
||||
function closeDrawer(): void {
|
||||
state.drawerOpen = false;
|
||||
state.selectedId = null;
|
||||
state.selectedItem = null;
|
||||
document.getElementById('vault-drawer')?.classList.remove('vault-drawer--open');
|
||||
}
|
||||
|
||||
function getDrawerCoreFields(item: Item): Array<[string, string, boolean]> {
|
||||
const core = item.core as unknown as Record<string, unknown>;
|
||||
if (!core) return [];
|
||||
const fields: Array<[string, string, boolean]> = [];
|
||||
|
||||
switch (item.type) {
|
||||
case 'login':
|
||||
if ('username' in core) fields.push(['username', String(core.username ?? ''), false]);
|
||||
if ('password' in core) fields.push(['password', '••••••••', false]);
|
||||
if ('url' in core) fields.push(['url', String(core.url ?? ''), true]);
|
||||
break;
|
||||
case 'card': {
|
||||
if ('number' in core) fields.push(['number', String(core.number ?? ''), false]);
|
||||
if ('holder' in core) fields.push(['holder', String(core.holder ?? ''), false]);
|
||||
if ('expiry' in core && core.expiry) {
|
||||
const exp = core.expiry as { month: number; year: number };
|
||||
fields.push(['expiry', `${String(exp.month).padStart(2, '0')}/${exp.year}`, false]);
|
||||
}
|
||||
if ('cvv' in core) fields.push(['cvv', '•••', false]);
|
||||
if ('pin' in core) fields.push(['pin', '••••', false]);
|
||||
break;
|
||||
}
|
||||
case 'identity':
|
||||
if ('full_name' in core) fields.push(['full name', String(core.full_name ?? ''), true]);
|
||||
if ('email' in core) fields.push(['email', String(core.email ?? ''), true]);
|
||||
if ('phone' in core) fields.push(['phone', String(core.phone ?? ''), false]);
|
||||
if ('address' in core) fields.push(['address', String(core.address ?? ''), true]);
|
||||
if ('date_of_birth' in core) fields.push(['date of birth', String(core.date_of_birth ?? ''), false]);
|
||||
break;
|
||||
case 'key':
|
||||
if ('label' in core) fields.push(['label', String(core.label ?? ''), true]);
|
||||
if ('algorithm' in core) fields.push(['algorithm', String(core.algorithm ?? ''), false]);
|
||||
if ('public_key' in core) fields.push(['public key', String(core.public_key ?? ''), true]);
|
||||
break;
|
||||
case 'secure_note':
|
||||
if ('body' in core) fields.push(['body', String(core.body ?? ''), true]);
|
||||
break;
|
||||
case 'totp':
|
||||
if ('issuer' in core) fields.push(['issuer', String(core.issuer ?? ''), false]);
|
||||
if ('label' in core) fields.push(['label', String(core.label ?? ''), false]);
|
||||
break;
|
||||
case 'document':
|
||||
if ('filename' in core) fields.push(['filename', String(core.filename ?? ''), true]);
|
||||
if ('mime_type' in core) fields.push(['type', String(core.mime_type ?? ''), false]);
|
||||
break;
|
||||
}
|
||||
|
||||
if (item.notes) fields.push(['notes', item.notes, true]);
|
||||
return fields;
|
||||
}
|
||||
|
||||
function renderDrawer(item: Item): void {
|
||||
const drawer = document.getElementById('vault-drawer');
|
||||
if (!drawer) return;
|
||||
|
||||
const coreFields = getDrawerCoreFields(item);
|
||||
|
||||
drawer.innerHTML = `
|
||||
<div class="vault-drawer__header">
|
||||
<span class="vault-drawer__type-pill">${item.type.replace('_', ' ').toUpperCase()}</span>
|
||||
<div class="vault-drawer__actions">
|
||||
<button class="btn" id="drawer-edit-btn" style="font-size:11px;">edit</button>
|
||||
<button class="vault-drawer__close" id="drawer-close-btn" title="Close (Esc)">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="vault-drawer__body">
|
||||
<div class="vault-drawer__title">${escapeHtml(item.title)}</div>
|
||||
${item.type === 'login' && (item.core as { url?: string }).url
|
||||
? `<div class="vault-drawer__subtitle">${escapeHtml((item.core as { url?: string }).url ?? '')}</div>`
|
||||
: ''}
|
||||
<div class="vault-drawer__field-grid">
|
||||
${coreFields.map(([label, value, full]) => `
|
||||
<div class="vault-drawer__field${full ? ' vault-drawer__field--full' : ''}">
|
||||
<div class="vault-drawer__field-label">${escapeHtml(label)}</div>
|
||||
<div class="vault-drawer__field-value">${escapeHtml(value)}</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.getElementById('drawer-close-btn')?.addEventListener('click', () => {
|
||||
closeDrawer();
|
||||
renderListPane(ctx);
|
||||
});
|
||||
|
||||
document.getElementById('drawer-edit-btn')?.addEventListener('click', () => {
|
||||
if (state.selectedId) {
|
||||
setHash('edit', state.selectedId);
|
||||
renderPane();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Item selection (implemented in Task 10)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function selectItemForDrawer(id: string): Promise<void> {
|
||||
const resp = await sendMessage({ type: 'get_item', id });
|
||||
if (!resp.ok) return;
|
||||
const data = resp.data as { item: Item };
|
||||
state.selectedId = id;
|
||||
state.selectedItem = data.item;
|
||||
state.drawerOpen = true;
|
||||
renderSidebarCategories(ctx);
|
||||
renderListPane(ctx);
|
||||
renderDrawer(data.item);
|
||||
openDrawer();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Platform-aware save hint
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -395,6 +273,7 @@ function renderPane(): void {
|
||||
teardownPaneComponents();
|
||||
|
||||
const route = parseHash();
|
||||
ensureDrawerClosedForRoute(state, route);
|
||||
// Keep state.view in sync with hash for components that read it
|
||||
state.view = route.view;
|
||||
applyShellViewClass(ctx);
|
||||
|
||||
Reference in New Issue
Block a user