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
|
// (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
|
// org renders a read-only banner; Dev-C's write UI reads `orgOffline` to
|
||||||
// disable writes.
|
// disable writes.
|
||||||
//
|
|
||||||
// SCAFFOLD STATE (HOLD until Dev-A merges): renders the select shell only. The
|
import type { OrgConfigSummary } from '../../shared/types';
|
||||||
// `org_list_configs` fetch, the `org_switch` dispatch + list reload, and the
|
import { sendMessage, setState, navigate, getState } from '../../shared/state';
|
||||||
// offline banner are held until Dev-A's org messages land in the Request union.
|
|
||||||
|
// 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> {
|
export async function renderOrgSwitcher(host: HTMLElement): Promise<void> {
|
||||||
// HOLD(dev-a): fetch org_list_configs -> append an <option> per org; on change
|
// Tear down any previous instance before re-mounting.
|
||||||
// send org_switch, setState({ orgContext, orgOffline }), navigate('list', {}),
|
teardown();
|
||||||
// and render the "org offline — writes disabled" banner when data.offline.
|
|
||||||
|
// 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 =
|
host.innerHTML =
|
||||||
'<select class="org-switcher" aria-label="Vault context">' +
|
`<select class="org-switcher" aria-label="Vault context">${optionsHtml}</select>`;
|
||||||
'<option value="personal">Personal</option>' +
|
|
||||||
'</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 {
|
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>
|
<title>Relicario</title>
|
||||||
</head>
|
</head>
|
||||||
<body class="surface-backdrop">
|
<body class="surface-backdrop">
|
||||||
|
<div id="popup-header"></div>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<script src="popup.js"></script>
|
<script src="popup.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { renderItemDetail } from './components/item-detail';
|
|||||||
import { renderItemForm } from './components/item-form';
|
import { renderItemForm } from './components/item-form';
|
||||||
import { renderSettings, teardownSettings } from './components/settings';
|
import { renderSettings, teardownSettings } from './components/settings';
|
||||||
import { renderVaultSettings } from './components/settings-vault';
|
import { renderVaultSettings } from './components/settings-vault';
|
||||||
|
import { renderOrgSwitcher } from './components/org-switcher';
|
||||||
import { renderTrash } from './components/trash';
|
import { renderTrash } from './components/trash';
|
||||||
import { renderDevices } from './components/devices';
|
import { renderDevices } from './components/devices';
|
||||||
import { renderFieldHistory } from './components/field-history';
|
import { renderFieldHistory } from './components/field-history';
|
||||||
@@ -245,6 +246,10 @@ async function init(): Promise<void> {
|
|||||||
currentState.generatorDefaults = vs.generator_defaults;
|
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)
|
// Check URL params for deep linking (when opened in tab)
|
||||||
const urlParams = parseUrlParams();
|
const urlParams = parseUrlParams();
|
||||||
if (urlParams) {
|
if (urlParams) {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
type VaultController, escapeHtml, typeIcon,
|
type VaultController, escapeHtml, typeIcon,
|
||||||
} from './vault-context';
|
} from './vault-context';
|
||||||
import { renderSidebarShell } from './vault-sidebar';
|
import { renderSidebarShell } from './vault-sidebar';
|
||||||
|
import { renderOrgSwitcher } from '../popup/components/org-switcher';
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Type picker (right side panel)
|
// Type picker (right side panel)
|
||||||
@@ -118,6 +119,7 @@ export function renderShell(ctx: VaultController, app: HTMLElement): void {
|
|||||||
`;
|
`;
|
||||||
ctx.wireSidebar();
|
ctx.wireSidebar();
|
||||||
wireTypePanel(ctx);
|
wireTypePanel(ctx);
|
||||||
|
void renderOrgSwitcher(document.getElementById('org-switcher-slot')!);
|
||||||
}
|
}
|
||||||
|
|
||||||
applyShellViewClass(ctx);
|
applyShellViewClass(ctx);
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export function renderSidebarShell(): string {
|
|||||||
<div class="vault-sidebar__header">
|
<div class="vault-sidebar__header">
|
||||||
<img class="brand-logo" src="icons/relicario-logo.svg" alt="">
|
<img class="brand-logo" src="icons/relicario-logo.svg" alt="">
|
||||||
<span class="brand">Relicario</span>
|
<span class="brand">Relicario</span>
|
||||||
|
<div id="org-switcher-slot"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="vault-sidebar__search">
|
<div class="vault-sidebar__search">
|
||||||
<input type="text" id="vault-search" placeholder="/ search…" />
|
<input type="text" id="vault-search" placeholder="/ search…" />
|
||||||
|
|||||||
Reference in New Issue
Block a user