feat(ext): org context switcher (popup header + vault sidebar) + offline read-only banner
This commit is contained in:
@@ -5,21 +5,68 @@
|
||||
// (and its offline flag) into shared state, and reloads the list. An offline
|
||||
// org renders a read-only banner; Dev-C's write UI reads `orgOffline` to
|
||||
// disable writes.
|
||||
//
|
||||
// SCAFFOLD STATE (HOLD until Dev-A merges): renders the select shell only. The
|
||||
// `org_list_configs` fetch, the `org_switch` dispatch + list reload, and the
|
||||
// offline banner are held until Dev-A's org messages land in the Request union.
|
||||
|
||||
import type { OrgConfigSummary } from '../../shared/types';
|
||||
import { sendMessage, setState, navigate, getState } from '../../shared/state';
|
||||
|
||||
// Module-level teardown (one instance per browser context; popup and vault
|
||||
// each have their own module scope).
|
||||
let _cleanupListener: (() => void) | null = null;
|
||||
|
||||
export async function renderOrgSwitcher(host: HTMLElement): Promise<void> {
|
||||
// HOLD(dev-a): fetch org_list_configs -> append an <option> per org; on change
|
||||
// send org_switch, setState({ orgContext, orgOffline }), navigate('list', {}),
|
||||
// and render the "org offline — writes disabled" banner when data.offline.
|
||||
// Tear down any previous instance before re-mounting.
|
||||
teardown();
|
||||
|
||||
// 1. Fetch org configs to populate the switcher options.
|
||||
const resp = await sendMessage({ type: 'org_list_configs' });
|
||||
const orgConfigs: OrgConfigSummary[] = resp.ok
|
||||
? (resp.data as OrgConfigSummary[])
|
||||
: [];
|
||||
|
||||
// 2. Current context (default: 'personal').
|
||||
const currentContext = getState().orgContext ?? 'personal';
|
||||
|
||||
// 3. Build and inject the <select>.
|
||||
let optionsHtml =
|
||||
`<option value="personal"${currentContext === 'personal' ? ' selected' : ''}>Personal</option>`;
|
||||
for (const org of orgConfigs) {
|
||||
const sel = currentContext === org.orgId ? ' selected' : '';
|
||||
optionsHtml += `<option value="${org.orgId}"${sel}>${org.displayName}</option>`;
|
||||
}
|
||||
host.innerHTML =
|
||||
'<select class="org-switcher" aria-label="Vault context">' +
|
||||
'<option value="personal">Personal</option>' +
|
||||
'</select>';
|
||||
`<select class="org-switcher" aria-label="Vault context">${optionsHtml}</select>`;
|
||||
|
||||
// 4. Wire the change event.
|
||||
const select = host.querySelector('select') as HTMLSelectElement;
|
||||
|
||||
const handler = async (_e: Event) => {
|
||||
const context = select.value;
|
||||
const switchResp = await sendMessage({ type: 'org_switch', context });
|
||||
if (!switchResp.ok) return;
|
||||
const data = switchResp.data as { context: string; offline: boolean };
|
||||
setState({ orgContext: data.context, orgOffline: data.offline });
|
||||
|
||||
// Offline banner — toggle inside host alongside the select.
|
||||
const existing = host.querySelector('.org-offline-banner');
|
||||
if (data.offline) {
|
||||
if (!existing) {
|
||||
const banner = document.createElement('div');
|
||||
banner.className = 'org-offline-banner';
|
||||
banner.textContent = 'org offline — writes disabled';
|
||||
host.appendChild(banner);
|
||||
}
|
||||
} else {
|
||||
existing?.remove();
|
||||
}
|
||||
|
||||
navigate('list', {});
|
||||
};
|
||||
|
||||
select.addEventListener('change', handler);
|
||||
_cleanupListener = () => select.removeEventListener('change', handler);
|
||||
}
|
||||
|
||||
export function teardown(): void {
|
||||
// HOLD(dev-a): detach the change listener once renderOrgSwitcher wires it.
|
||||
_cleanupListener?.();
|
||||
_cleanupListener = null;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<title>Relicario</title>
|
||||
</head>
|
||||
<body class="surface-backdrop">
|
||||
<div id="popup-header"></div>
|
||||
<div id="app"></div>
|
||||
<script src="popup.js"></script>
|
||||
</body>
|
||||
|
||||
@@ -14,6 +14,7 @@ import { renderItemDetail } from './components/item-detail';
|
||||
import { renderItemForm } from './components/item-form';
|
||||
import { renderSettings, teardownSettings } from './components/settings';
|
||||
import { renderVaultSettings } from './components/settings-vault';
|
||||
import { renderOrgSwitcher } from './components/org-switcher';
|
||||
import { renderTrash } from './components/trash';
|
||||
import { renderDevices } from './components/devices';
|
||||
import { renderFieldHistory } from './components/field-history';
|
||||
@@ -245,6 +246,10 @@ async function init(): Promise<void> {
|
||||
currentState.generatorDefaults = vs.generator_defaults;
|
||||
}
|
||||
|
||||
// Mount the org context switcher in the persistent header slot (lives
|
||||
// outside #app so it survives view swaps).
|
||||
void renderOrgSwitcher(document.getElementById('popup-header')!);
|
||||
|
||||
// Check URL params for deep linking (when opened in tab)
|
||||
const urlParams = parseUrlParams();
|
||||
if (urlParams) {
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
type VaultController, escapeHtml, typeIcon,
|
||||
} from './vault-context';
|
||||
import { renderSidebarShell } from './vault-sidebar';
|
||||
import { renderOrgSwitcher } from '../popup/components/org-switcher';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Type picker (right side panel)
|
||||
@@ -118,6 +119,7 @@ export function renderShell(ctx: VaultController, app: HTMLElement): void {
|
||||
`;
|
||||
ctx.wireSidebar();
|
||||
wireTypePanel(ctx);
|
||||
void renderOrgSwitcher(document.getElementById('org-switcher-slot')!);
|
||||
}
|
||||
|
||||
applyShellViewClass(ctx);
|
||||
|
||||
@@ -27,6 +27,7 @@ export function renderSidebarShell(): string {
|
||||
<div class="vault-sidebar__header">
|
||||
<img class="brand-logo" src="icons/relicario-logo.svg" alt="">
|
||||
<span class="brand">Relicario</span>
|
||||
<div id="org-switcher-slot"></div>
|
||||
</div>
|
||||
<div class="vault-sidebar__search">
|
||||
<input type="text" id="vault-search" placeholder="/ search…" />
|
||||
|
||||
Reference in New Issue
Block a user