feat(ext/popup): wire custom-field editor into all 6 type forms
Each typed-item form now mounts the collapsible sections editor before the form-actions. Save functions accept sectionsDraft and persist it via Item.sections so custom fields round-trip correctly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,58 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
vi.mock('../../../popup', async () => {
|
||||||
|
const navigate = vi.fn();
|
||||||
|
const setState = vi.fn();
|
||||||
|
const sendMessage = vi.fn();
|
||||||
|
const getState = vi.fn(() => ({
|
||||||
|
view: 'add', entries: [], selectedId: null, selectedItem: null, selectedIndex: 0,
|
||||||
|
searchQuery: '', activeGroup: null, error: null, loading: false,
|
||||||
|
capturedTabId: null, capturedUrl: '', newType: 'login',
|
||||||
|
vaultSettings: null, generatorDefaults: null,
|
||||||
|
}));
|
||||||
|
const escapeHtml = (s: string) => s
|
||||||
|
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"').replace(/'/g, ''');
|
||||||
|
return { navigate, setState, sendMessage, getState, escapeHtml };
|
||||||
|
});
|
||||||
|
|
||||||
|
import { renderForm } from '../login';
|
||||||
|
import { sendMessage } from '../../../popup';
|
||||||
|
|
||||||
|
describe('Login form packs sectionsDraft into Item.sections', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
document.body.innerHTML = '<div id="app"></div>';
|
||||||
|
vi.mocked(sendMessage).mockReset();
|
||||||
|
vi.mocked(sendMessage).mockResolvedValue({ ok: true, data: { id: 'fakeid0000000000', items: [] } });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('persists added sections and fields', async () => {
|
||||||
|
const app = document.getElementById('app')!;
|
||||||
|
renderForm(app, 'add', null);
|
||||||
|
|
||||||
|
(document.getElementById('f-title') as HTMLInputElement).value = 'Example';
|
||||||
|
|
||||||
|
(document.querySelector('.disclosure__toggle') as HTMLButtonElement).click();
|
||||||
|
(document.querySelector('.add-section') as HTMLButtonElement).click();
|
||||||
|
(document.querySelector('[data-add-field="text"]') as HTMLButtonElement).click();
|
||||||
|
|
||||||
|
const labelInput = document.querySelector('[data-field-label="0-0"]') as HTMLInputElement;
|
||||||
|
labelInput.value = 'recovery email';
|
||||||
|
labelInput.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
|
const valueInput = document.querySelector('[data-field-value-input="0-0"]') as HTMLInputElement;
|
||||||
|
valueInput.value = 'backup@example.com';
|
||||||
|
valueInput.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
|
|
||||||
|
document.getElementById('save-btn')!.click();
|
||||||
|
await new Promise((r) => setTimeout(r, 5));
|
||||||
|
|
||||||
|
const calls = vi.mocked(sendMessage).mock.calls;
|
||||||
|
const addCall = calls.find(([msg]) => (msg as { type: string }).type === 'add_item');
|
||||||
|
const msg = addCall![0] as { type: 'add_item'; item: any };
|
||||||
|
expect(msg.item.sections).toHaveLength(1);
|
||||||
|
expect(msg.item.sections[0].fields).toHaveLength(1);
|
||||||
|
expect(msg.item.sections[0].fields[0].label).toBe('recovery email');
|
||||||
|
expect(msg.item.sections[0].fields[0].value).toEqual({ kind: 'text', value: 'backup@example.com' });
|
||||||
|
expect(msg.item.sections[0].fields[0].id).toMatch(/^[0-9a-f]{16}$/);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -2,15 +2,17 @@
|
|||||||
/// Detail view has a styled card-silhouette signature block.
|
/// Detail view has a styled card-silhouette signature block.
|
||||||
|
|
||||||
import { getState, setState, sendMessage, navigate, escapeHtml } from '../../popup';
|
import { getState, setState, sendMessage, navigate, escapeHtml } from '../../popup';
|
||||||
import type { Item, ItemId, ManifestEntry, CardKind } from '../../../shared/types';
|
import type { Item, ItemId, ManifestEntry, CardKind, Section } from '../../../shared/types';
|
||||||
import {
|
import {
|
||||||
renderConcealedRow, renderSignatureBlock, wireFieldHandlers, renderSections,
|
renderConcealedRow, renderSignatureBlock, wireFieldHandlers, renderSections,
|
||||||
|
renderSectionsEditor, wireSectionsEditor,
|
||||||
} from '../fields';
|
} from '../fields';
|
||||||
|
|
||||||
const CARD_KINDS: CardKind[] = ['credit', 'debit', 'gift', 'loyalty', 'other'];
|
const CARD_KINDS: CardKind[] = ['credit', 'debit', 'gift', 'loyalty', 'other'];
|
||||||
|
|
||||||
let activeKeyHandler: ((e: KeyboardEvent) => void) | null = null;
|
let activeKeyHandler: ((e: KeyboardEvent) => void) | null = null;
|
||||||
let activeFormEscHandler: ((e: KeyboardEvent) => void) | null = null;
|
let activeFormEscHandler: ((e: KeyboardEvent) => void) | null = null;
|
||||||
|
let sectionsExpanded = false;
|
||||||
|
|
||||||
export function teardown(): void {
|
export function teardown(): void {
|
||||||
if (activeKeyHandler) {
|
if (activeKeyHandler) {
|
||||||
@@ -21,6 +23,7 @@ export function teardown(): void {
|
|||||||
document.removeEventListener('keydown', activeFormEscHandler);
|
document.removeEventListener('keydown', activeFormEscHandler);
|
||||||
activeFormEscHandler = null;
|
activeFormEscHandler = null;
|
||||||
}
|
}
|
||||||
|
sectionsExpanded = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function brandFromNumber(num: string): string {
|
function brandFromNumber(num: string): string {
|
||||||
@@ -140,6 +143,10 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
|
|||||||
const c = (existing?.core.type === 'card') ? existing.core : null;
|
const c = (existing?.core.type === 'card') ? existing.core : null;
|
||||||
const currentYear = new Date().getFullYear();
|
const currentYear = new Date().getFullYear();
|
||||||
|
|
||||||
|
const sectionsDraft: Section[] = existing
|
||||||
|
? JSON.parse(JSON.stringify(existing.sections)) as Section[]
|
||||||
|
: [];
|
||||||
|
|
||||||
const monthOptions = Array.from({ length: 12 }, (_, i) => {
|
const monthOptions = Array.from({ length: 12 }, (_, i) => {
|
||||||
const m = String(i + 1).padStart(2, '0');
|
const m = String(i + 1).padStart(2, '0');
|
||||||
const sel = c?.expiry?.month === i + 1 ? 'selected' : '';
|
const sel = c?.expiry?.month === i + 1 ? 'selected' : '';
|
||||||
@@ -176,6 +183,7 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
|
|||||||
<input id="f-pin" type="password" inputmode="numeric" maxlength="8" value="${escapeHtml(c?.pin ?? '')}"></div>
|
<input id="f-pin" type="password" inputmode="numeric" maxlength="8" value="${escapeHtml(c?.pin ?? '')}"></div>
|
||||||
<div class="form-group"><label class="label" for="f-kind">kind</label>
|
<div class="form-group"><label class="label" for="f-kind">kind</label>
|
||||||
<select id="f-kind">${kindOptions}</select></div>
|
<select id="f-kind">${kindOptions}</select></div>
|
||||||
|
${renderSectionsEditor(sectionsDraft, sectionsExpanded)}
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button class="btn" id="cancel-btn">cancel</button>
|
<button class="btn" id="cancel-btn">cancel</button>
|
||||||
<button class="btn btn-primary" id="save-btn">save</button>
|
<button class="btn btn-primary" id="save-btn">save</button>
|
||||||
@@ -183,12 +191,21 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const rerender = (): void => {
|
||||||
|
const disclosure = app.querySelector('.disclosure');
|
||||||
|
if (!disclosure) return;
|
||||||
|
sectionsExpanded = disclosure.getAttribute('data-expanded') === 'true';
|
||||||
|
disclosure.outerHTML = renderSectionsEditor(sectionsDraft, sectionsExpanded);
|
||||||
|
wireSectionsEditor(app, sectionsDraft, rerender);
|
||||||
|
};
|
||||||
|
wireSectionsEditor(app, sectionsDraft, rerender);
|
||||||
|
|
||||||
document.getElementById('cancel-btn')?.addEventListener('click', () => {
|
document.getElementById('cancel-btn')?.addEventListener('click', () => {
|
||||||
setState({ error: null });
|
setState({ error: null });
|
||||||
navigate(mode === 'edit' ? 'detail' : 'list');
|
navigate(mode === 'edit' ? 'detail' : 'list');
|
||||||
});
|
});
|
||||||
document.getElementById('save-btn')?.addEventListener('click', async () => {
|
document.getElementById('save-btn')?.addEventListener('click', async () => {
|
||||||
await saveCard(mode, existing);
|
await saveCard(mode, existing, sectionsDraft);
|
||||||
});
|
});
|
||||||
|
|
||||||
const escHandler = (e: KeyboardEvent) => {
|
const escHandler = (e: KeyboardEvent) => {
|
||||||
@@ -203,7 +220,7 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
|
|||||||
(document.getElementById('f-title') as HTMLInputElement | null)?.focus();
|
(document.getElementById('f-title') as HTMLInputElement | null)?.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveCard(mode: 'add' | 'edit', existing: Item | null): Promise<void> {
|
async function saveCard(mode: 'add' | 'edit', existing: Item | null, sectionsDraft: Section[]): Promise<void> {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const title = (document.getElementById('f-title') as HTMLInputElement).value.trim();
|
const title = (document.getElementById('f-title') as HTMLInputElement).value.trim();
|
||||||
if (!title) { setState({ error: 'Title is required' }); return; }
|
if (!title) { setState({ error: 'Title is required' }); return; }
|
||||||
@@ -242,7 +259,7 @@ async function saveCard(mode: 'add' | 'edit', existing: Item | null): Promise<vo
|
|||||||
created: existing?.created ?? now,
|
created: existing?.created ?? now,
|
||||||
modified: now, trashed_at: undefined,
|
modified: now, trashed_at: undefined,
|
||||||
core,
|
core,
|
||||||
sections: existing?.sections ?? [],
|
sections: sectionsDraft,
|
||||||
attachments: existing?.attachments ?? [],
|
attachments: existing?.attachments ?? [],
|
||||||
field_history: existing?.field_history ?? {},
|
field_history: existing?.field_history ?? {},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,13 +2,15 @@
|
|||||||
/// Detail view shows a "profile card" signature block + plain rows.
|
/// Detail view shows a "profile card" signature block + plain rows.
|
||||||
|
|
||||||
import { getState, setState, sendMessage, navigate, escapeHtml } from '../../popup';
|
import { getState, setState, sendMessage, navigate, escapeHtml } from '../../popup';
|
||||||
import type { Item, ItemId, ManifestEntry } from '../../../shared/types';
|
import type { Item, ItemId, ManifestEntry, Section } from '../../../shared/types';
|
||||||
import {
|
import {
|
||||||
renderRow, renderSignatureBlock, wireFieldHandlers, renderSections,
|
renderRow, renderSignatureBlock, wireFieldHandlers, renderSections,
|
||||||
|
renderSectionsEditor, wireSectionsEditor,
|
||||||
} from '../fields';
|
} from '../fields';
|
||||||
|
|
||||||
let activeKeyHandler: ((e: KeyboardEvent) => void) | null = null;
|
let activeKeyHandler: ((e: KeyboardEvent) => void) | null = null;
|
||||||
let activeFormEscHandler: ((e: KeyboardEvent) => void) | null = null;
|
let activeFormEscHandler: ((e: KeyboardEvent) => void) | null = null;
|
||||||
|
let sectionsExpanded = false;
|
||||||
|
|
||||||
export function teardown(): void {
|
export function teardown(): void {
|
||||||
if (activeKeyHandler) {
|
if (activeKeyHandler) {
|
||||||
@@ -19,6 +21,7 @@ export function teardown(): void {
|
|||||||
document.removeEventListener('keydown', activeFormEscHandler);
|
document.removeEventListener('keydown', activeFormEscHandler);
|
||||||
activeFormEscHandler = null;
|
activeFormEscHandler = null;
|
||||||
}
|
}
|
||||||
|
sectionsExpanded = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function initials(name: string | undefined): string {
|
function initials(name: string | undefined): string {
|
||||||
@@ -115,6 +118,10 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
|
|||||||
const title = existing?.title ?? '';
|
const title = existing?.title ?? '';
|
||||||
const c = (existing?.core.type === 'identity') ? existing.core : null;
|
const c = (existing?.core.type === 'identity') ? existing.core : null;
|
||||||
|
|
||||||
|
const sectionsDraft: Section[] = existing
|
||||||
|
? JSON.parse(JSON.stringify(existing.sections)) as Section[]
|
||||||
|
: [];
|
||||||
|
|
||||||
app.innerHTML = `
|
app.innerHTML = `
|
||||||
<div class="pad">
|
<div class="pad">
|
||||||
<div class="detail-title" style="margin-bottom:16px;">${mode === 'add' ? 'new identity' : 'edit identity'}</div>
|
<div class="detail-title" style="margin-bottom:16px;">${mode === 'add' ? 'new identity' : 'edit identity'}</div>
|
||||||
@@ -131,6 +138,7 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
|
|||||||
<textarea id="f-address" rows="3" placeholder="street, city, postcode...">${escapeHtml(c?.address ?? '')}</textarea></div>
|
<textarea id="f-address" rows="3" placeholder="street, city, postcode...">${escapeHtml(c?.address ?? '')}</textarea></div>
|
||||||
<div class="form-group"><label class="label" for="f-dob">date of birth</label>
|
<div class="form-group"><label class="label" for="f-dob">date of birth</label>
|
||||||
<input id="f-dob" type="date" value="${escapeHtml(c?.date_of_birth ?? '')}"></div>
|
<input id="f-dob" type="date" value="${escapeHtml(c?.date_of_birth ?? '')}"></div>
|
||||||
|
${renderSectionsEditor(sectionsDraft, sectionsExpanded)}
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button class="btn" id="cancel-btn">cancel</button>
|
<button class="btn" id="cancel-btn">cancel</button>
|
||||||
<button class="btn btn-primary" id="save-btn">save</button>
|
<button class="btn btn-primary" id="save-btn">save</button>
|
||||||
@@ -138,12 +146,21 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const rerender = (): void => {
|
||||||
|
const disclosure = app.querySelector('.disclosure');
|
||||||
|
if (!disclosure) return;
|
||||||
|
sectionsExpanded = disclosure.getAttribute('data-expanded') === 'true';
|
||||||
|
disclosure.outerHTML = renderSectionsEditor(sectionsDraft, sectionsExpanded);
|
||||||
|
wireSectionsEditor(app, sectionsDraft, rerender);
|
||||||
|
};
|
||||||
|
wireSectionsEditor(app, sectionsDraft, rerender);
|
||||||
|
|
||||||
document.getElementById('cancel-btn')?.addEventListener('click', () => {
|
document.getElementById('cancel-btn')?.addEventListener('click', () => {
|
||||||
setState({ error: null });
|
setState({ error: null });
|
||||||
navigate(mode === 'edit' ? 'detail' : 'list');
|
navigate(mode === 'edit' ? 'detail' : 'list');
|
||||||
});
|
});
|
||||||
document.getElementById('save-btn')?.addEventListener('click', async () => {
|
document.getElementById('save-btn')?.addEventListener('click', async () => {
|
||||||
await saveIdentity(mode, existing);
|
await saveIdentity(mode, existing, sectionsDraft);
|
||||||
});
|
});
|
||||||
|
|
||||||
const escHandler = (e: KeyboardEvent) => {
|
const escHandler = (e: KeyboardEvent) => {
|
||||||
@@ -158,7 +175,7 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
|
|||||||
(document.getElementById('f-title') as HTMLInputElement | null)?.focus();
|
(document.getElementById('f-title') as HTMLInputElement | null)?.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveIdentity(mode: 'add' | 'edit', existing: Item | null): Promise<void> {
|
async function saveIdentity(mode: 'add' | 'edit', existing: Item | null, sectionsDraft: Section[]): Promise<void> {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const title = (document.getElementById('f-title') as HTMLInputElement).value.trim();
|
const title = (document.getElementById('f-title') as HTMLInputElement).value.trim();
|
||||||
if (!title) { setState({ error: 'Title is required' }); return; }
|
if (!title) { setState({ error: 'Title is required' }); return; }
|
||||||
@@ -184,7 +201,7 @@ async function saveIdentity(mode: 'add' | 'edit', existing: Item | null): Promis
|
|||||||
created: existing?.created ?? now,
|
created: existing?.created ?? now,
|
||||||
modified: now, trashed_at: undefined,
|
modified: now, trashed_at: undefined,
|
||||||
core,
|
core,
|
||||||
sections: existing?.sections ?? [],
|
sections: sectionsDraft,
|
||||||
attachments: existing?.attachments ?? [],
|
attachments: existing?.attachments ?? [],
|
||||||
field_history: existing?.field_history ?? {},
|
field_history: existing?.field_history ?? {},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,13 +3,15 @@
|
|||||||
/// since <textarea type="password"> isn't a thing.
|
/// since <textarea type="password"> isn't a thing.
|
||||||
|
|
||||||
import { getState, setState, sendMessage, navigate, escapeHtml } from '../../popup';
|
import { getState, setState, sendMessage, navigate, escapeHtml } from '../../popup';
|
||||||
import type { Item, ItemId, ManifestEntry } from '../../../shared/types';
|
import type { Item, ItemId, ManifestEntry, Section } from '../../../shared/types';
|
||||||
import {
|
import {
|
||||||
renderRow, renderConcealedRow, renderSignatureBlock, wireFieldHandlers, renderSections,
|
renderRow, renderConcealedRow, renderSignatureBlock, wireFieldHandlers, renderSections,
|
||||||
|
renderSectionsEditor, wireSectionsEditor,
|
||||||
} from '../fields';
|
} from '../fields';
|
||||||
|
|
||||||
let activeKeyHandler: ((e: KeyboardEvent) => void) | null = null;
|
let activeKeyHandler: ((e: KeyboardEvent) => void) | null = null;
|
||||||
let activeFormEscHandler: ((e: KeyboardEvent) => void) | null = null;
|
let activeFormEscHandler: ((e: KeyboardEvent) => void) | null = null;
|
||||||
|
let sectionsExpanded = false;
|
||||||
|
|
||||||
export function teardown(): void {
|
export function teardown(): void {
|
||||||
if (activeKeyHandler) {
|
if (activeKeyHandler) {
|
||||||
@@ -20,6 +22,7 @@ export function teardown(): void {
|
|||||||
document.removeEventListener('keydown', activeFormEscHandler);
|
document.removeEventListener('keydown', activeFormEscHandler);
|
||||||
activeFormEscHandler = null;
|
activeFormEscHandler = null;
|
||||||
}
|
}
|
||||||
|
sectionsExpanded = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function renderDetail(app: HTMLElement, item: Item): Promise<void> {
|
export async function renderDetail(app: HTMLElement, item: Item): Promise<void> {
|
||||||
@@ -104,6 +107,10 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
|
|||||||
const title = existing?.title ?? '';
|
const title = existing?.title ?? '';
|
||||||
const c = (existing?.core.type === 'key') ? existing.core : null;
|
const c = (existing?.core.type === 'key') ? existing.core : null;
|
||||||
|
|
||||||
|
const sectionsDraft: Section[] = existing
|
||||||
|
? JSON.parse(JSON.stringify(existing.sections)) as Section[]
|
||||||
|
: [];
|
||||||
|
|
||||||
app.innerHTML = `
|
app.innerHTML = `
|
||||||
<div class="pad">
|
<div class="pad">
|
||||||
<div class="detail-title" style="margin-bottom:16px;">${mode === 'add' ? 'new key' : 'edit key'}</div>
|
<div class="detail-title" style="margin-bottom:16px;">${mode === 'add' ? 'new key' : 'edit key'}</div>
|
||||||
@@ -121,6 +128,7 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
|
|||||||
<input id="f-algorithm" type="text" value="${escapeHtml(c?.algorithm ?? '')}" placeholder="ed25519"></div>
|
<input id="f-algorithm" type="text" value="${escapeHtml(c?.algorithm ?? '')}" placeholder="ed25519"></div>
|
||||||
<div class="form-group"><label class="label" for="f-public-key">public key</label>
|
<div class="form-group"><label class="label" for="f-public-key">public key</label>
|
||||||
<textarea id="f-public-key" rows="4" style="font-family:monospace;" placeholder="ssh-ed25519 AAAA...">${escapeHtml(c?.public_key ?? '')}</textarea></div>
|
<textarea id="f-public-key" rows="4" style="font-family:monospace;" placeholder="ssh-ed25519 AAAA...">${escapeHtml(c?.public_key ?? '')}</textarea></div>
|
||||||
|
${renderSectionsEditor(sectionsDraft, sectionsExpanded)}
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button class="btn" id="cancel-btn">cancel</button>
|
<button class="btn" id="cancel-btn">cancel</button>
|
||||||
<button class="btn btn-primary" id="save-btn">save</button>
|
<button class="btn btn-primary" id="save-btn">save</button>
|
||||||
@@ -128,6 +136,15 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const rerender = (): void => {
|
||||||
|
const disclosure = app.querySelector('.disclosure');
|
||||||
|
if (!disclosure) return;
|
||||||
|
sectionsExpanded = disclosure.getAttribute('data-expanded') === 'true';
|
||||||
|
disclosure.outerHTML = renderSectionsEditor(sectionsDraft, sectionsExpanded);
|
||||||
|
wireSectionsEditor(app, sectionsDraft, rerender);
|
||||||
|
};
|
||||||
|
wireSectionsEditor(app, sectionsDraft, rerender);
|
||||||
|
|
||||||
// Show/hide toggle for the key_material textarea.
|
// Show/hide toggle for the key_material textarea.
|
||||||
let revealed = false;
|
let revealed = false;
|
||||||
document.getElementById('key-show-btn')?.addEventListener('click', () => {
|
document.getElementById('key-show-btn')?.addEventListener('click', () => {
|
||||||
@@ -142,7 +159,7 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
|
|||||||
navigate(mode === 'edit' ? 'detail' : 'list');
|
navigate(mode === 'edit' ? 'detail' : 'list');
|
||||||
});
|
});
|
||||||
document.getElementById('save-btn')?.addEventListener('click', async () => {
|
document.getElementById('save-btn')?.addEventListener('click', async () => {
|
||||||
await saveKey(mode, existing);
|
await saveKey(mode, existing, sectionsDraft);
|
||||||
});
|
});
|
||||||
|
|
||||||
const escHandler = (e: KeyboardEvent) => {
|
const escHandler = (e: KeyboardEvent) => {
|
||||||
@@ -157,7 +174,7 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
|
|||||||
(document.getElementById('f-title') as HTMLInputElement | null)?.focus();
|
(document.getElementById('f-title') as HTMLInputElement | null)?.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveKey(mode: 'add' | 'edit', existing: Item | null): Promise<void> {
|
async function saveKey(mode: 'add' | 'edit', existing: Item | null, sectionsDraft: Section[]): Promise<void> {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const title = (document.getElementById('f-title') as HTMLInputElement).value.trim();
|
const title = (document.getElementById('f-title') as HTMLInputElement).value.trim();
|
||||||
if (!title) { setState({ error: 'Title is required' }); return; }
|
if (!title) { setState({ error: 'Title is required' }); return; }
|
||||||
@@ -185,7 +202,7 @@ async function saveKey(mode: 'add' | 'edit', existing: Item | null): Promise<voi
|
|||||||
created: existing?.created ?? now,
|
created: existing?.created ?? now,
|
||||||
modified: now, trashed_at: undefined,
|
modified: now, trashed_at: undefined,
|
||||||
core,
|
core,
|
||||||
sections: existing?.sections ?? [],
|
sections: sectionsDraft,
|
||||||
attachments: existing?.attachments ?? [],
|
attachments: existing?.attachments ?? [],
|
||||||
field_history: existing?.field_history ?? {},
|
field_history: existing?.field_history ?? {},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
/// field helpers introduced in Slice 2.
|
/// field helpers introduced in Slice 2.
|
||||||
|
|
||||||
import { getState, setState, sendMessage, navigate, escapeHtml } from '../../popup';
|
import { getState, setState, sendMessage, navigate, escapeHtml } from '../../popup';
|
||||||
import type { Item, ItemId, LoginCore, ManifestEntry, TotpConfig } from '../../../shared/types';
|
import type { Item, ItemId, LoginCore, ManifestEntry, Section, TotpConfig } from '../../../shared/types';
|
||||||
import { DEFAULT_PASSWORD_REQUEST } from '../../../shared/types';
|
import { DEFAULT_PASSWORD_REQUEST } from '../../../shared/types';
|
||||||
import { base32Decode, base32Encode } from '../../../shared/base32';
|
import { base32Decode, base32Encode } from '../../../shared/base32';
|
||||||
import {
|
import {
|
||||||
@@ -11,6 +11,8 @@ import {
|
|||||||
renderSignatureBlock,
|
renderSignatureBlock,
|
||||||
wireFieldHandlers,
|
wireFieldHandlers,
|
||||||
renderSections,
|
renderSections,
|
||||||
|
renderSectionsEditor,
|
||||||
|
wireSectionsEditor,
|
||||||
} from '../fields';
|
} from '../fields';
|
||||||
|
|
||||||
/// Called by the dispatcher before each render. Stops any in-flight
|
/// Called by the dispatcher before each render. Stops any in-flight
|
||||||
@@ -25,6 +27,7 @@ export function teardown(): void {
|
|||||||
document.removeEventListener('keydown', activeFormEscHandler);
|
document.removeEventListener('keydown', activeFormEscHandler);
|
||||||
activeFormEscHandler = null;
|
activeFormEscHandler = null;
|
||||||
}
|
}
|
||||||
|
sectionsExpanded = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------
|
// ----------------------------------------------------------------------
|
||||||
@@ -181,6 +184,7 @@ export async function renderDetail(app: HTMLElement, item: Item): Promise<void>
|
|||||||
let totpTickerId: ReturnType<typeof setInterval> | null = null;
|
let totpTickerId: ReturnType<typeof setInterval> | null = null;
|
||||||
let activeKeyHandler: ((e: KeyboardEvent) => void) | null = null;
|
let activeKeyHandler: ((e: KeyboardEvent) => void) | null = null;
|
||||||
let activeFormEscHandler: ((e: KeyboardEvent) => void) | null = null;
|
let activeFormEscHandler: ((e: KeyboardEvent) => void) | null = null;
|
||||||
|
let sectionsExpanded = false;
|
||||||
function stopTotpTicker(): void {
|
function stopTotpTicker(): void {
|
||||||
if (totpTickerId !== null) { clearInterval(totpTickerId); totpTickerId = null; }
|
if (totpTickerId !== null) { clearInterval(totpTickerId); totpTickerId = null; }
|
||||||
}
|
}
|
||||||
@@ -217,6 +221,10 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
|
|||||||
const group = existing?.group ?? '';
|
const group = existing?.group ?? '';
|
||||||
const notes = existing?.notes ?? '';
|
const notes = existing?.notes ?? '';
|
||||||
|
|
||||||
|
const sectionsDraft: Section[] = existing
|
||||||
|
? JSON.parse(JSON.stringify(existing.sections)) as Section[]
|
||||||
|
: [];
|
||||||
|
|
||||||
app.innerHTML = `
|
app.innerHTML = `
|
||||||
<div class="pad">
|
<div class="pad">
|
||||||
<div class="detail-title" style="margin-bottom:16px;">${mode === 'add' ? 'new login' : 'edit login'}</div>
|
<div class="detail-title" style="margin-bottom:16px;">${mode === 'add' ? 'new login' : 'edit login'}</div>
|
||||||
@@ -238,6 +246,7 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
|
|||||||
<input id="f-group" type="text" value="${escapeHtml(group)}" placeholder="work"></div>
|
<input id="f-group" type="text" value="${escapeHtml(group)}" placeholder="work"></div>
|
||||||
<div class="form-group"><label class="label" for="f-notes">notes</label>
|
<div class="form-group"><label class="label" for="f-notes">notes</label>
|
||||||
<textarea id="f-notes" placeholder="recovery codes, security questions...">${escapeHtml(notes)}</textarea></div>
|
<textarea id="f-notes" placeholder="recovery codes, security questions...">${escapeHtml(notes)}</textarea></div>
|
||||||
|
${renderSectionsEditor(sectionsDraft, sectionsExpanded)}
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button class="btn" id="cancel-btn">cancel</button>
|
<button class="btn" id="cancel-btn">cancel</button>
|
||||||
<button class="btn btn-primary" id="save-btn">${state.loading ? '<span class="spinner"></span>' : 'save'}</button>
|
<button class="btn btn-primary" id="save-btn">${state.loading ? '<span class="spinner"></span>' : 'save'}</button>
|
||||||
@@ -245,6 +254,15 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const rerender = (): void => {
|
||||||
|
const disclosure = app.querySelector('.disclosure');
|
||||||
|
if (!disclosure) return;
|
||||||
|
sectionsExpanded = disclosure.getAttribute('data-expanded') === 'true';
|
||||||
|
disclosure.outerHTML = renderSectionsEditor(sectionsDraft, sectionsExpanded);
|
||||||
|
wireSectionsEditor(app, sectionsDraft, rerender);
|
||||||
|
};
|
||||||
|
wireSectionsEditor(app, sectionsDraft, rerender);
|
||||||
|
|
||||||
document.getElementById('gen-btn')?.addEventListener('click', async () => {
|
document.getElementById('gen-btn')?.addEventListener('click', async () => {
|
||||||
const resp = await sendMessage({ type: 'generate_password', request: DEFAULT_PASSWORD_REQUEST });
|
const resp = await sendMessage({ type: 'generate_password', request: DEFAULT_PASSWORD_REQUEST });
|
||||||
if (resp.ok) {
|
if (resp.ok) {
|
||||||
@@ -261,7 +279,7 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
|
|||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('save-btn')?.addEventListener('click', async () => {
|
document.getElementById('save-btn')?.addEventListener('click', async () => {
|
||||||
await saveLogin(mode, existing);
|
await saveLogin(mode, existing, sectionsDraft);
|
||||||
});
|
});
|
||||||
|
|
||||||
const escHandler = (e: KeyboardEvent) => {
|
const escHandler = (e: KeyboardEvent) => {
|
||||||
@@ -289,7 +307,7 @@ function normalizeUrl(raw: string): { ok: true; value: string } | { ok: false; e
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveLogin(mode: 'add' | 'edit', existing: Item | null): Promise<void> {
|
async function saveLogin(mode: 'add' | 'edit', existing: Item | null, sectionsDraft: Section[]): Promise<void> {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const title = (document.getElementById('f-title') as HTMLInputElement).value.trim();
|
const title = (document.getElementById('f-title') as HTMLInputElement).value.trim();
|
||||||
const rawUrl = (document.getElementById('f-url') as HTMLInputElement).value;
|
const rawUrl = (document.getElementById('f-url') as HTMLInputElement).value;
|
||||||
@@ -339,7 +357,7 @@ async function saveLogin(mode: 'add' | 'edit', existing: Item | null): Promise<v
|
|||||||
modified: now,
|
modified: now,
|
||||||
trashed_at: undefined,
|
trashed_at: undefined,
|
||||||
core,
|
core,
|
||||||
sections: existing?.sections ?? [],
|
sections: sectionsDraft,
|
||||||
attachments: existing?.attachments ?? [],
|
attachments: existing?.attachments ?? [],
|
||||||
field_history: existing?.field_history ?? {},
|
field_history: existing?.field_history ?? {},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,13 +2,15 @@
|
|||||||
/// detail view; the form is just a big <textarea>.
|
/// detail view; the form is just a big <textarea>.
|
||||||
|
|
||||||
import { getState, setState, sendMessage, navigate, escapeHtml } from '../../popup';
|
import { getState, setState, sendMessage, navigate, escapeHtml } from '../../popup';
|
||||||
import type { Item, ItemId, ManifestEntry } from '../../../shared/types';
|
import type { Item, ItemId, ManifestEntry, Section } from '../../../shared/types';
|
||||||
import {
|
import {
|
||||||
renderConcealedRow, renderSignatureBlock, wireFieldHandlers, renderSections,
|
renderConcealedRow, renderSignatureBlock, wireFieldHandlers, renderSections,
|
||||||
|
renderSectionsEditor, wireSectionsEditor,
|
||||||
} from '../fields';
|
} from '../fields';
|
||||||
|
|
||||||
let activeKeyHandler: ((e: KeyboardEvent) => void) | null = null;
|
let activeKeyHandler: ((e: KeyboardEvent) => void) | null = null;
|
||||||
let activeFormEscHandler: ((e: KeyboardEvent) => void) | null = null;
|
let activeFormEscHandler: ((e: KeyboardEvent) => void) | null = null;
|
||||||
|
let sectionsExpanded = false;
|
||||||
|
|
||||||
export function teardown(): void {
|
export function teardown(): void {
|
||||||
if (activeKeyHandler) {
|
if (activeKeyHandler) {
|
||||||
@@ -19,6 +21,7 @@ export function teardown(): void {
|
|||||||
document.removeEventListener('keydown', activeFormEscHandler);
|
document.removeEventListener('keydown', activeFormEscHandler);
|
||||||
activeFormEscHandler = null;
|
activeFormEscHandler = null;
|
||||||
}
|
}
|
||||||
|
sectionsExpanded = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function renderDetail(app: HTMLElement, item: Item): Promise<void> {
|
export async function renderDetail(app: HTMLElement, item: Item): Promise<void> {
|
||||||
@@ -93,6 +96,10 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
|
|||||||
const title = existing?.title ?? '';
|
const title = existing?.title ?? '';
|
||||||
const body = (existing?.core.type === 'secure_note') ? existing.core.body ?? '' : '';
|
const body = (existing?.core.type === 'secure_note') ? existing.core.body ?? '' : '';
|
||||||
|
|
||||||
|
const sectionsDraft: Section[] = existing
|
||||||
|
? JSON.parse(JSON.stringify(existing.sections)) as Section[]
|
||||||
|
: [];
|
||||||
|
|
||||||
app.innerHTML = `
|
app.innerHTML = `
|
||||||
<div class="pad">
|
<div class="pad">
|
||||||
<div class="detail-title" style="margin-bottom:16px;">${mode === 'add' ? 'new secure note' : 'edit secure note'}</div>
|
<div class="detail-title" style="margin-bottom:16px;">${mode === 'add' ? 'new secure note' : 'edit secure note'}</div>
|
||||||
@@ -101,6 +108,7 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
|
|||||||
<input id="f-title" type="text" value="${escapeHtml(title)}" placeholder="My recovery codes"></div>
|
<input id="f-title" type="text" value="${escapeHtml(title)}" placeholder="My recovery codes"></div>
|
||||||
<div class="form-group"><label class="label" for="f-body">body</label>
|
<div class="form-group"><label class="label" for="f-body">body</label>
|
||||||
<textarea id="f-body" rows="10" placeholder="paste secrets here">${escapeHtml(body)}</textarea></div>
|
<textarea id="f-body" rows="10" placeholder="paste secrets here">${escapeHtml(body)}</textarea></div>
|
||||||
|
${renderSectionsEditor(sectionsDraft, sectionsExpanded)}
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button class="btn" id="cancel-btn">cancel</button>
|
<button class="btn" id="cancel-btn">cancel</button>
|
||||||
<button class="btn btn-primary" id="save-btn">save</button>
|
<button class="btn btn-primary" id="save-btn">save</button>
|
||||||
@@ -108,12 +116,21 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const rerender = (): void => {
|
||||||
|
const disclosure = app.querySelector('.disclosure');
|
||||||
|
if (!disclosure) return;
|
||||||
|
sectionsExpanded = disclosure.getAttribute('data-expanded') === 'true';
|
||||||
|
disclosure.outerHTML = renderSectionsEditor(sectionsDraft, sectionsExpanded);
|
||||||
|
wireSectionsEditor(app, sectionsDraft, rerender);
|
||||||
|
};
|
||||||
|
wireSectionsEditor(app, sectionsDraft, rerender);
|
||||||
|
|
||||||
document.getElementById('cancel-btn')?.addEventListener('click', () => {
|
document.getElementById('cancel-btn')?.addEventListener('click', () => {
|
||||||
setState({ error: null });
|
setState({ error: null });
|
||||||
navigate(mode === 'edit' ? 'detail' : 'list');
|
navigate(mode === 'edit' ? 'detail' : 'list');
|
||||||
});
|
});
|
||||||
document.getElementById('save-btn')?.addEventListener('click', async () => {
|
document.getElementById('save-btn')?.addEventListener('click', async () => {
|
||||||
await saveSecureNote(mode, existing);
|
await saveSecureNote(mode, existing, sectionsDraft);
|
||||||
});
|
});
|
||||||
|
|
||||||
const escHandler = (e: KeyboardEvent) => {
|
const escHandler = (e: KeyboardEvent) => {
|
||||||
@@ -128,7 +145,7 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
|
|||||||
(document.getElementById('f-title') as HTMLInputElement | null)?.focus();
|
(document.getElementById('f-title') as HTMLInputElement | null)?.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveSecureNote(mode: 'add' | 'edit', existing: Item | null): Promise<void> {
|
async function saveSecureNote(mode: 'add' | 'edit', existing: Item | null, sectionsDraft: Section[]): Promise<void> {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const title = (document.getElementById('f-title') as HTMLInputElement).value.trim();
|
const title = (document.getElementById('f-title') as HTMLInputElement).value.trim();
|
||||||
const body = (document.getElementById('f-body') as HTMLTextAreaElement).value;
|
const body = (document.getElementById('f-body') as HTMLTextAreaElement).value;
|
||||||
@@ -146,7 +163,7 @@ async function saveSecureNote(mode: 'add' | 'edit', existing: Item | null): Prom
|
|||||||
modified: now,
|
modified: now,
|
||||||
trashed_at: undefined,
|
trashed_at: undefined,
|
||||||
core: { type: 'secure_note', body },
|
core: { type: 'secure_note', body },
|
||||||
sections: existing?.sections ?? [],
|
sections: sectionsDraft,
|
||||||
attachments: existing?.attachments ?? [],
|
attachments: existing?.attachments ?? [],
|
||||||
field_history: existing?.field_history ?? {},
|
field_history: existing?.field_history ?? {},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,10 +3,11 @@
|
|||||||
/// (TOTP vs Steam Guard) and a single secret input.
|
/// (TOTP vs Steam Guard) and a single secret input.
|
||||||
|
|
||||||
import { getState, setState, sendMessage, navigate, escapeHtml } from '../../popup';
|
import { getState, setState, sendMessage, navigate, escapeHtml } from '../../popup';
|
||||||
import type { Item, ItemId, ManifestEntry, TotpKind } from '../../../shared/types';
|
import type { Item, ItemId, ManifestEntry, Section, TotpKind } from '../../../shared/types';
|
||||||
import { base32Decode, base32Encode } from '../../../shared/base32';
|
import { base32Decode, base32Encode } from '../../../shared/base32';
|
||||||
import {
|
import {
|
||||||
renderRow, renderConcealedRow, renderSignatureBlock, wireFieldHandlers, renderSections,
|
renderRow, renderConcealedRow, renderSignatureBlock, wireFieldHandlers, renderSections,
|
||||||
|
renderSectionsEditor, wireSectionsEditor,
|
||||||
} from '../fields';
|
} from '../fields';
|
||||||
|
|
||||||
// ----------------------------------------------------------------------
|
// ----------------------------------------------------------------------
|
||||||
@@ -16,6 +17,7 @@ import {
|
|||||||
let totpTickerId: ReturnType<typeof setInterval> | null = null;
|
let totpTickerId: ReturnType<typeof setInterval> | null = null;
|
||||||
let activeKeyHandler: ((e: KeyboardEvent) => void) | null = null;
|
let activeKeyHandler: ((e: KeyboardEvent) => void) | null = null;
|
||||||
let activeFormEscHandler: ((e: KeyboardEvent) => void) | null = null;
|
let activeFormEscHandler: ((e: KeyboardEvent) => void) | null = null;
|
||||||
|
let sectionsExpanded = false;
|
||||||
|
|
||||||
function stopTotpTicker(): void {
|
function stopTotpTicker(): void {
|
||||||
if (totpTickerId !== null) { clearInterval(totpTickerId); totpTickerId = null; }
|
if (totpTickerId !== null) { clearInterval(totpTickerId); totpTickerId = null; }
|
||||||
@@ -33,6 +35,7 @@ export function teardown(): void {
|
|||||||
document.removeEventListener('keydown', activeFormEscHandler);
|
document.removeEventListener('keydown', activeFormEscHandler);
|
||||||
activeFormEscHandler = null;
|
activeFormEscHandler = null;
|
||||||
}
|
}
|
||||||
|
sectionsExpanded = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------
|
// ----------------------------------------------------------------------
|
||||||
@@ -194,6 +197,10 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
|
|||||||
formKind = c?.config.kind === 'steam' ? 'steam' : 'totp';
|
formKind = c?.config.kind === 'steam' ? 'steam' : 'totp';
|
||||||
const secretB32 = c?.config.secret ? base32Encode(new Uint8Array(c.config.secret)) : '';
|
const secretB32 = c?.config.secret ? base32Encode(new Uint8Array(c.config.secret)) : '';
|
||||||
|
|
||||||
|
const sectionsDraft: Section[] = existing
|
||||||
|
? JSON.parse(JSON.stringify(existing.sections)) as Section[]
|
||||||
|
: [];
|
||||||
|
|
||||||
const renderInner = (): string => `
|
const renderInner = (): string => `
|
||||||
<div class="pad">
|
<div class="pad">
|
||||||
<div class="detail-title" style="margin-bottom:16px;">${mode === 'add' ? 'new totp' : 'edit totp'}</div>
|
<div class="detail-title" style="margin-bottom:16px;">${mode === 'add' ? 'new totp' : 'edit totp'}</div>
|
||||||
@@ -213,6 +220,7 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
|
|||||||
<input id="f-issuer" type="text" value="${escapeHtml(c?.issuer ?? '')}" placeholder="GitHub"></div>
|
<input id="f-issuer" type="text" value="${escapeHtml(c?.issuer ?? '')}" placeholder="GitHub"></div>
|
||||||
<div class="form-group"><label class="label" for="f-label">label</label>
|
<div class="form-group"><label class="label" for="f-label">label</label>
|
||||||
<input id="f-label" type="text" value="${escapeHtml(c?.label ?? '')}" placeholder="alice@github.com"></div>
|
<input id="f-label" type="text" value="${escapeHtml(c?.label ?? '')}" placeholder="alice@github.com"></div>
|
||||||
|
${renderSectionsEditor(sectionsDraft, sectionsExpanded)}
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button class="btn" id="cancel-btn">cancel</button>
|
<button class="btn" id="cancel-btn">cancel</button>
|
||||||
<button class="btn btn-primary" id="save-btn">save</button>
|
<button class="btn btn-primary" id="save-btn">save</button>
|
||||||
@@ -235,7 +243,20 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
|
|||||||
(document.getElementById('f-issuer') as HTMLInputElement).value = issuerVal;
|
(document.getElementById('f-issuer') as HTMLInputElement).value = issuerVal;
|
||||||
(document.getElementById('f-label') as HTMLInputElement).value = labelVal;
|
(document.getElementById('f-label') as HTMLInputElement).value = labelVal;
|
||||||
wireKindToggle();
|
wireKindToggle();
|
||||||
wireFormButtons(mode, existing);
|
wireFormButtons(mode, existing, sectionsDraft);
|
||||||
|
wireSectionsEditor(app, sectionsDraft, sectionsRerender);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Rerender only the sections editor in place (used by structural section
|
||||||
|
// mutations — add/remove). Reuses the form-wide reRender for simplicity
|
||||||
|
// since kind toggle already re-mounts the full inner DOM; here we just
|
||||||
|
// need to preserve sectionsExpanded and swap the disclosure block.
|
||||||
|
const sectionsRerender = (): void => {
|
||||||
|
const disclosure = app.querySelector('.disclosure');
|
||||||
|
if (!disclosure) return;
|
||||||
|
sectionsExpanded = disclosure.getAttribute('data-expanded') === 'true';
|
||||||
|
disclosure.outerHTML = renderSectionsEditor(sectionsDraft, sectionsExpanded);
|
||||||
|
wireSectionsEditor(app, sectionsDraft, sectionsRerender);
|
||||||
};
|
};
|
||||||
|
|
||||||
const wireKindToggle = (): void => {
|
const wireKindToggle = (): void => {
|
||||||
@@ -250,7 +271,8 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
|
|||||||
};
|
};
|
||||||
|
|
||||||
wireKindToggle();
|
wireKindToggle();
|
||||||
wireFormButtons(mode, existing);
|
wireFormButtons(mode, existing, sectionsDraft);
|
||||||
|
wireSectionsEditor(app, sectionsDraft, sectionsRerender);
|
||||||
|
|
||||||
const escHandler = (e: KeyboardEvent) => {
|
const escHandler = (e: KeyboardEvent) => {
|
||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
@@ -264,17 +286,17 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
|
|||||||
(document.getElementById('f-title') as HTMLInputElement | null)?.focus();
|
(document.getElementById('f-title') as HTMLInputElement | null)?.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
function wireFormButtons(mode: 'add' | 'edit', existing: Item | null): void {
|
function wireFormButtons(mode: 'add' | 'edit', existing: Item | null, sectionsDraft: Section[]): void {
|
||||||
document.getElementById('cancel-btn')?.addEventListener('click', () => {
|
document.getElementById('cancel-btn')?.addEventListener('click', () => {
|
||||||
setState({ error: null });
|
setState({ error: null });
|
||||||
navigate(mode === 'edit' ? 'detail' : 'list');
|
navigate(mode === 'edit' ? 'detail' : 'list');
|
||||||
});
|
});
|
||||||
document.getElementById('save-btn')?.addEventListener('click', async () => {
|
document.getElementById('save-btn')?.addEventListener('click', async () => {
|
||||||
await saveTotp(mode, existing);
|
await saveTotp(mode, existing, sectionsDraft);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveTotp(mode: 'add' | 'edit', existing: Item | null): Promise<void> {
|
async function saveTotp(mode: 'add' | 'edit', existing: Item | null, sectionsDraft: Section[]): Promise<void> {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const title = (document.getElementById('f-title') as HTMLInputElement).value.trim();
|
const title = (document.getElementById('f-title') as HTMLInputElement).value.trim();
|
||||||
if (!title) { setState({ error: 'Title is required' }); return; }
|
if (!title) { setState({ error: 'Title is required' }); return; }
|
||||||
@@ -317,7 +339,7 @@ async function saveTotp(mode: 'add' | 'edit', existing: Item | null): Promise<vo
|
|||||||
created: existing?.created ?? now,
|
created: existing?.created ?? now,
|
||||||
modified: now, trashed_at: undefined,
|
modified: now, trashed_at: undefined,
|
||||||
core,
|
core,
|
||||||
sections: existing?.sections ?? [],
|
sections: sectionsDraft,
|
||||||
attachments: existing?.attachments ?? [],
|
attachments: existing?.attachments ?? [],
|
||||||
field_history: existing?.field_history ?? {},
|
field_history: existing?.field_history ?? {},
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user