diff --git a/extension/src/popup/components/item-list.ts b/extension/src/popup/components/item-list.ts index ae15f9d..6776a19 100644 --- a/extension/src/popup/components/item-list.ts +++ b/extension/src/popup/components/item-list.ts @@ -3,6 +3,7 @@ /// to the detail view. import { getState, setState, sendMessage, navigate, escapeHtml, openVaultTab } from '../../shared/state'; +import { currentContext, messageForList, messageForGet } from '../../shared/org-context'; import { showToast } from '../../shared/toast'; import { GLYPH_VAULT_TAB, @@ -10,7 +11,7 @@ import { GLYPH_TYPE_LOGIN, GLYPH_TYPE_SECURE_NOTE, GLYPH_TYPE_TOTP, GLYPH_TYPE_CARD, GLYPH_TYPE_IDENTITY, GLYPH_TYPE_KEY, GLYPH_TYPE_DOCUMENT, } from '../../shared/glyphs'; -import type { ItemId, ItemType, ManifestEntry, Item } from '../../shared/types'; +import type { ItemId, ItemType, ManifestEntry, OrgManifestEntry, Item } from '../../shared/types'; /// Extract the display hostname from an icon_hint or fallback to the first tag. function metaLine(e: ManifestEntry): string { @@ -32,6 +33,39 @@ function typeIcon(t: ItemType): string { } } +/// Normalize OrgManifestEntry[] (leaner org shape; no attachment_summaries / +/// favorite / icon_hint / group) into the Array<[ItemId, ManifestEntry]> that +/// buildRowsHtml and getFilteredEntries expect. Used by BOTH the sync handler +/// and the context-aware initial load. +function normalizeOrgEntries(data: OrgManifestEntry[]): Array<[ItemId, ManifestEntry]> { + return data.map(e => [e.id, { + id: e.id, + type: e.type, + title: e.title, + tags: e.tags, + modified: e.modified, + trashed_at: e.trashed_at, + favorite: false, + attachment_summaries: [], + // icon_hint, group: undefined (optional in ManifestEntry; omitted for org items) + }]); +} + +/// Context-aware item list loader. Sends list_items or org_list_items depending +/// on the current context, then normalizes the response into entries and calls +/// setState. Called once per context by renderItemList (loop-safe guard). +async function loadEntriesForContext(): Promise { + const resp = await sendMessage(messageForList()); + if (!resp.ok) { + setState({ error: resp.error ?? 'Failed to load items' }); + return; + } + const entries: Array<[ItemId, ManifestEntry]> = currentContext() === 'personal' + ? (resp.data as { items: Array<[ItemId, ManifestEntry]> }).items + : normalizeOrgEntries(resp.data as OrgManifestEntry[]); + setState({ entries }); +} + function buildRowsHtml(): string { const state = getState(); const filtered = getFilteredEntries(); @@ -81,12 +115,13 @@ function wireRowClicks(): void { export function renderItemList(app: HTMLElement): void { const state = getState(); + const isOrg = currentContext() !== 'personal'; app.innerHTML = `
- + ${isOrg ? '' : ''} @@ -98,7 +133,7 @@ export function renderItemList(app: HTMLElement): void {
/ search - + new + ${isOrg ? '' : '+ new'} ↑↓ nav Enter open
@@ -127,10 +162,12 @@ export function renderItemList(app: HTMLElement): void { setState({ loading: true, error: null }); const resp = await sendMessage({ type: 'sync' }); if (resp.ok) { - const listResp = await sendMessage({ type: 'list_items' }); + const listResp = await sendMessage(messageForList()); if (listResp.ok) { - const data = listResp.data as { items: Array<[ItemId, ManifestEntry]> }; - setState({ entries: data.items, loading: false }); + const entries: Array<[ItemId, ManifestEntry]> = currentContext() === 'personal' + ? (listResp.data as { items: Array<[ItemId, ManifestEntry]> }).items + : normalizeOrgEntries(listResp.data as OrgManifestEntry[]); + setState({ entries, loading: false }); showToast('Synced', 'success'); return; } @@ -155,11 +192,21 @@ export function renderItemList(app: HTMLElement): void { wireRowClicks(); document.addEventListener('keydown', handleListKeydown); + + // Context-aware initial load (org only; personal uses manual sync). + // Loop-safe guard: setState({loadedContext}) triggers a re-render that calls + // renderItemList again, but the guard now matches so no second load fires. + // When loadEntriesForContext() later resolves and calls setState({entries}), + // loadedContext still matches so still no loop. + if (isOrg && getState().loadedContext !== currentContext()) { + setState({ loadedContext: currentContext() }); + void loadEntriesForContext(); + } } async function openItem(id: ItemId): Promise { setState({ loading: true }); - const resp = await sendMessage({ type: 'get_item', id }); + const resp = await sendMessage(messageForGet(id)); if (resp.ok) { const data = resp.data as { item: Item }; navigate('detail', { @@ -226,7 +273,7 @@ function handleListKeydown(e: KeyboardEvent): void { return; } - if (e.key === '+' && !isSearch) { + if (e.key === '+' && !isSearch && currentContext() === 'personal') { e.preventDefault(); document.removeEventListener('keydown', handleListKeydown); navigate('add'); diff --git a/extension/src/shared/popup-state.ts b/extension/src/shared/popup-state.ts index 777d2bc..6cb41b4 100644 --- a/extension/src/shared/popup-state.ts +++ b/extension/src/shared/popup-state.ts @@ -57,6 +57,11 @@ export interface PopupState { orgOffline?: boolean; orgConfigs?: OrgConfigSummary[]; orgCollections?: Collection[]; + // Tracks which context's items were last loaded into `entries`. Used by + // renderItemList to fire a context-aware load exactly ONCE per context + // switch (loop-safe guard: setState({loadedContext}) re-renders, but the + // guard then matches so no second load fires). + loadedContext?: string; // Vault-tab-only fields. The popup surface leaves these at their defaults // (unlocked=false implicit via separate lock-screen view, drawer/panel false). // Kept on the shared shape so VaultState satisfies StateHost.getState()