feat(ext): list/detail load + render org items in org context (normalized, read-only)
This commit is contained in:
@@ -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<void> {
|
||||
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 = `
|
||||
<div class="search-bar">
|
||||
<input type="text" id="search-input" placeholder="/ search..." value="${escapeHtml(state.searchQuery)}">
|
||||
</div>
|
||||
<div class="toolbar" style="display:flex; gap:4px; padding:6px 12px; border-bottom:1px solid #21262d;">
|
||||
<button class="btn" id="new-btn" style="font-size:11px;">+ new</button>
|
||||
${isOrg ? '' : '<button class="btn" id="new-btn" style="font-size:11px;">+ new</button>'}
|
||||
<button class="btn" id="sync-btn" style="font-size:11px;">sync</button>
|
||||
<span style="flex:1;"></span>
|
||||
<button class="btn" id="vault-btn" style="font-size:11px;" title="Open vault (Shift+F)">${GLYPH_VAULT_TAB}</button>
|
||||
@@ -98,7 +133,7 @@ export function renderItemList(app: HTMLElement): void {
|
||||
</div>
|
||||
<div class="keyhints">
|
||||
<span><kbd>/</kbd> search</span>
|
||||
<span><kbd>+</kbd> new</span>
|
||||
${isOrg ? '' : '<span><kbd>+</kbd> new</span>'}
|
||||
<span><kbd>↑↓</kbd> nav</span>
|
||||
<span><kbd>Enter</kbd> open</span>
|
||||
</div>
|
||||
@@ -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<void> {
|
||||
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');
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user