Upgrade OneNote sync with section/page targeting and edit mode
- Add GET /api/onenote/section-pages endpoint for listing pages in a section - Extend /api/onenote/append with syncMode add/edit support - Support editing an existing note block by noteId on a selected page - Keep add mode for appending to selected page or creating new page in section - Add Note + Sync UI controls for sync mode, section/page target, existing noteId, and optional new page title - Wire frontend flow to load sync targets and pages, then sync with selected mode
This commit is contained in:
+161
-35
@@ -163,6 +163,35 @@
|
||||
<label class="text-sm font-medium text-slate-300">Body</label>
|
||||
<textarea id="noteBody" rows="8" class="field mt-1" placeholder="Write note content..."></textarea>
|
||||
</div>
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<div>
|
||||
<label class="text-sm font-medium text-slate-300">Sync Mode</label>
|
||||
<select id="syncModeSelect" class="field mt-1">
|
||||
<option value="add">add new note block</option>
|
||||
<option value="edit">edit existing note block</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-sm font-medium text-slate-300">Existing Note ID (edit mode)</label>
|
||||
<input id="syncEditNoteId" class="field mt-1" placeholder="Paste noteId to replace on selected page" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<button id="loadSyncTargetsBtn" class="btn btn-secondary">Load sync targets</button>
|
||||
<span id="syncTargetsStatus" class="text-xs text-slate-400"></span>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-sm font-medium text-slate-300">OneNote Section (sync target)</label>
|
||||
<select id="syncSectionSelect" class="field mt-1"><option value="">— click Load sync targets —</option></select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-sm font-medium text-slate-300">OneNote Page (optional)</label>
|
||||
<select id="syncPageSelect" class="field mt-1"><option value="">— create new page in selected section —</option></select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-sm font-medium text-slate-300">New Page Title (used when page is not selected)</label>
|
||||
<input id="syncPageTitle" class="field mt-1" placeholder="Optional (defaults to note title)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 flex flex-wrap gap-3">
|
||||
<button id="submitNoteBtn" class="btn btn-accent-indigo">Submit to Prism Notes</button>
|
||||
@@ -219,6 +248,12 @@
|
||||
const oneNoteAuthStatus = document.getElementById('oneNoteAuthStatus');
|
||||
const resolveStatus = document.getElementById('resolveStatus');
|
||||
const sectionSelect = document.getElementById('sectionSelect');
|
||||
const syncTargetsStatus = document.getElementById('syncTargetsStatus');
|
||||
const syncModeSelect = document.getElementById('syncModeSelect');
|
||||
const syncEditNoteId = document.getElementById('syncEditNoteId');
|
||||
const syncSectionSelect = document.getElementById('syncSectionSelect');
|
||||
const syncPageSelect = document.getElementById('syncPageSelect');
|
||||
const syncPageTitle = document.getElementById('syncPageTitle');
|
||||
let resolvedSiteId = null;
|
||||
|
||||
const noteTitle = document.getElementById('noteTitle');
|
||||
@@ -230,6 +265,7 @@
|
||||
const proofBody = document.getElementById('proofBody');
|
||||
|
||||
proofTitle.value = `Prism Notes Proof ${new Date().toISOString()}`;
|
||||
syncPageTitle.value = `Prism Notes Sync ${new Date().toISOString().slice(0, 10)}`;
|
||||
proofBody.value = [
|
||||
'Proof that OneNote can be updated programmatically from outside OneNote.',
|
||||
`Created: ${new Date().toISOString()}`,
|
||||
@@ -461,14 +497,15 @@
|
||||
writeOutput({ message: 'PocketBase session cleared.' });
|
||||
}
|
||||
|
||||
function buildNotePayload() {
|
||||
function buildNotePayload(options = {}) {
|
||||
const bodyPlain = noteBody.value.trim();
|
||||
if (!bodyPlain) {
|
||||
throw new Error('Note body is required');
|
||||
}
|
||||
const title = noteTitle.value.trim() || 'Untitled';
|
||||
const requestedNoteId = String(options.noteId || '').trim();
|
||||
return {
|
||||
noteId: globalThis.crypto?.randomUUID?.() || `note-${Date.now()}`,
|
||||
noteId: requestedNoteId || globalThis.crypto?.randomUUID?.() || `note-${Date.now()}`,
|
||||
title,
|
||||
bodyPlain,
|
||||
noteType: noteType.value || 'personal',
|
||||
@@ -477,6 +514,81 @@
|
||||
};
|
||||
}
|
||||
|
||||
async function loadSyncTargets() {
|
||||
syncTargetsStatus.textContent = 'Loading sections…';
|
||||
if (!hasUsableToken()) {
|
||||
syncTargetsStatus.textContent = '⚠ Connect OneNote first';
|
||||
throw new Error('OneNote connection required');
|
||||
}
|
||||
|
||||
const resp = await fetch('/api/onenote/site-sections', {
|
||||
headers: { Authorization: `Bearer ${delegatedAccessToken}` },
|
||||
});
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
if (!resp.ok || !data?.success) {
|
||||
const msg = data?.message || JSON.stringify(data?.error || data);
|
||||
syncTargetsStatus.textContent = `⚠ ${msg}`;
|
||||
throw new Error(msg);
|
||||
}
|
||||
|
||||
resolvedSiteId = data.siteId;
|
||||
const sections = Array.isArray(data.sections) ? data.sections : [];
|
||||
|
||||
syncSectionSelect.innerHTML = '';
|
||||
sectionSelect.innerHTML = '';
|
||||
for (const section of sections) {
|
||||
const label = `${section.displayName || section.id} — ${section.id}`;
|
||||
|
||||
const syncOpt = document.createElement('option');
|
||||
syncOpt.value = section.id;
|
||||
syncOpt.textContent = label;
|
||||
syncSectionSelect.appendChild(syncOpt);
|
||||
|
||||
const proofOpt = document.createElement('option');
|
||||
proofOpt.value = section.id;
|
||||
proofOpt.textContent = label;
|
||||
sectionSelect.appendChild(proofOpt);
|
||||
}
|
||||
|
||||
syncTargetsStatus.textContent = `✓ ${sections.length} section(s) loaded`;
|
||||
resolveStatus.textContent = `✓ ${sections.length} section(s) loaded`;
|
||||
|
||||
await loadSyncPagesForSelectedSection();
|
||||
writeOutput({ message: `Loaded ${sections.length} section(s)`, siteId: resolvedSiteId, sections });
|
||||
}
|
||||
|
||||
async function loadSyncPagesForSelectedSection() {
|
||||
const sectionId = syncSectionSelect.value.trim();
|
||||
syncPageSelect.innerHTML = '<option value="">— create new page in selected section —</option>';
|
||||
|
||||
if (!sectionId) {
|
||||
syncTargetsStatus.textContent = 'Select a section first';
|
||||
return;
|
||||
}
|
||||
|
||||
syncTargetsStatus.textContent = 'Loading pages…';
|
||||
const resp = await fetch(`/api/onenote/section-pages?sectionId=${encodeURIComponent(sectionId)}`, {
|
||||
headers: { Authorization: `Bearer ${delegatedAccessToken}` },
|
||||
});
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
if (!resp.ok || !data?.success) {
|
||||
const msg = data?.message || JSON.stringify(data?.error || data);
|
||||
syncTargetsStatus.textContent = `⚠ ${msg}`;
|
||||
throw new Error(msg);
|
||||
}
|
||||
|
||||
const pages = Array.isArray(data.pages) ? data.pages : [];
|
||||
for (const page of pages) {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = page.id;
|
||||
opt.textContent = `${page.title || '(untitled)'} — ${page.id}`;
|
||||
syncPageSelect.appendChild(opt);
|
||||
}
|
||||
|
||||
syncTargetsStatus.textContent = `✓ ${pages.length} page(s) loaded`;
|
||||
writeOutput({ message: `Loaded ${pages.length} page(s)`, sectionId, pages });
|
||||
}
|
||||
|
||||
async function submitNoteToPrismNotes() {
|
||||
const client = await ensurePocketBaseClient();
|
||||
if (!client.authStore.isValid || !client.authStore.token) {
|
||||
@@ -507,7 +619,23 @@
|
||||
throw new Error('OneNote connection required');
|
||||
}
|
||||
|
||||
const payload = buildNotePayload();
|
||||
const syncMode = syncModeSelect.value === 'edit' ? 'edit' : 'add';
|
||||
const sectionId = syncSectionSelect.value.trim();
|
||||
const pageId = syncPageSelect.value.trim();
|
||||
const pageTitle = syncPageTitle.value.trim();
|
||||
const targetNoteId = syncEditNoteId.value.trim();
|
||||
|
||||
if (!sectionId) {
|
||||
throw new Error('Load sync targets and select a section first');
|
||||
}
|
||||
if (syncMode === 'edit' && !pageId) {
|
||||
throw new Error('Edit mode requires selecting an existing OneNote page');
|
||||
}
|
||||
if (syncMode === 'edit' && !targetNoteId) {
|
||||
throw new Error('Edit mode requires Existing Note ID');
|
||||
}
|
||||
|
||||
const payload = buildNotePayload({ noteId: syncMode === 'edit' ? targetNoteId : '' });
|
||||
const resp = await fetch('/api/onenote/append', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -515,46 +643,35 @@
|
||||
Authorization: `Bearer ${delegatedAccessToken}`,
|
||||
'x-pb-token': client.authStore.token,
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
body: JSON.stringify({
|
||||
...payload,
|
||||
syncMode,
|
||||
sectionId,
|
||||
pageId,
|
||||
pageTitle,
|
||||
targetNoteId,
|
||||
}),
|
||||
});
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
if (!resp.ok || !data?.success) {
|
||||
throw new Error(data?.message || 'OneNote sync failed');
|
||||
}
|
||||
writeOutput({ message: 'Synced to configured OneNote target', response: data, payload });
|
||||
|
||||
if (syncMode === 'add' && data?.pageId && !pageId) {
|
||||
const createdOpt = document.createElement('option');
|
||||
createdOpt.value = data.pageId;
|
||||
createdOpt.textContent = `${pageTitle || payload.title} — ${data.pageId}`;
|
||||
syncPageSelect.appendChild(createdOpt);
|
||||
syncPageSelect.value = data.pageId;
|
||||
}
|
||||
syncEditNoteId.value = payload.noteId;
|
||||
|
||||
writeOutput({ message: `OneNote sync complete (${data?.mode || syncMode})`, response: data, payload });
|
||||
}
|
||||
|
||||
async function resolveSiteSections() {
|
||||
resolveStatus.textContent = 'Resolving…';
|
||||
if (!hasUsableToken()) {
|
||||
resolveStatus.textContent = '⚠ Connect OneNote first (section 2)';
|
||||
throw new Error('OneNote connection required — click Connect OneNote in section 2 first');
|
||||
}
|
||||
try {
|
||||
const resp = await fetch('/api/onenote/site-sections', {
|
||||
headers: { Authorization: `Bearer ${delegatedAccessToken}` },
|
||||
});
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
if (!resp.ok || !data?.success) {
|
||||
const msg = data?.message || JSON.stringify(data?.error || data);
|
||||
resolveStatus.textContent = `⚠ ${msg}`;
|
||||
throw new Error(msg);
|
||||
}
|
||||
resolvedSiteId = data.siteId;
|
||||
const sections = Array.isArray(data.sections) ? data.sections : [];
|
||||
sectionSelect.innerHTML = '';
|
||||
for (const s of sections) {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = s.id;
|
||||
opt.textContent = `${s.displayName || s.id} — ${s.id}`;
|
||||
sectionSelect.appendChild(opt);
|
||||
}
|
||||
resolveStatus.textContent = `✓ ${sections.length} section(s) loaded`;
|
||||
writeOutput({ message: `Resolved ${sections.length} section(s)`, siteId: resolvedSiteId, sections });
|
||||
} catch (e) {
|
||||
if (!resolveStatus.textContent.startsWith('⚠')) resolveStatus.textContent = `⚠ ${e.message}`;
|
||||
throw e;
|
||||
}
|
||||
await loadSyncTargets();
|
||||
}
|
||||
|
||||
async function createProofPage() {
|
||||
@@ -599,11 +716,20 @@
|
||||
bind('logoutBtn', logoutPocketBase);
|
||||
bind('connectOneNoteBtn', connectOneNoteInteractive);
|
||||
bind('refreshOneNoteTokenBtn', refreshOneNoteToken);
|
||||
bind('submitNoteBtn', submitNoteToPrismNotes);
|
||||
bind('loadSyncTargetsBtn', loadSyncTargets);
|
||||
bind('submitNoteBtn', submitNoteToPrismNotes);
|
||||
bind('syncOneNoteBtn', syncCurrentNoteToOneNote);
|
||||
bind('resolveSectionsBtn', resolveSiteSections);
|
||||
bind('createProofPageBtn', createProofPage);
|
||||
|
||||
syncSectionSelect.addEventListener('change', async () => {
|
||||
try {
|
||||
await loadSyncPagesForSelectedSection();
|
||||
} catch (error) {
|
||||
writeOutput({ error: error?.message || String(error) });
|
||||
}
|
||||
});
|
||||
|
||||
ensurePocketBaseClient()
|
||||
.then(() => updatePbStatus())
|
||||
.catch((error) => writeOutput({ error: error?.message || String(error) }));
|
||||
|
||||
Reference in New Issue
Block a user