diff --git a/com.pdma.notion-timer.sdPlugin/bin/plugin.js b/com.pdma.notion-timer.sdPlugin/bin/plugin.js index 6b0d506..06df4f3 100644 --- a/com.pdma.notion-timer.sdPlugin/bin/plugin.js +++ b/com.pdma.notion-timer.sdPlugin/bin/plugin.js @@ -6375,6 +6375,15 @@ async function fetchUsers(token) { const data = await resp.json(); return data.results.filter((u) => u.type === "person" && u.person?.email).map((u) => ({ id: u.id, name: u.name })).sort((a, b) => a.name.localeCompare(b.name)); } +async function fetchTagOptions(token, databaseId, propertyName) { + const resp = await fetchWithTimeout(`${NOTION_BASE}/databases/${databaseId}`, { + headers: headers(token) + }); + if (!resp.ok) throw new Error(`Failed to fetch database schema: ${resp.status}`); + const data = await resp.json(); + const options = data.properties?.[propertyName]?.select?.options ?? []; + return options.map((o) => o.name); +} async function fetchProjects(token, dbId) { const results = []; let cursor; @@ -6401,21 +6410,24 @@ async function fetchProjects(token, dbId) { } while (cursor); return results; } -async function startTimer(token, timingDbId, projectId, projectName, userId) { +async function startTimer(token, timingDbId, projectId, projectName, userId, tag, description) { const now = (/* @__PURE__ */ new Date()).toISOString(); + const properties = { + Name: { title: [{ text: { content: "Timer Task" } }] }, + "Start Time": { date: { start: now } }, + Projects: { relation: [{ id: projectId }] }, + Status: { status: { name: "Running" } }, + Responsible: { people: [{ object: "user", id: userId }] } + }; + if (tag) properties["Timer Tags"] = { select: { name: tag } }; + if (description) properties.Description = { rich_text: [{ text: { content: description } }] }; const resp = await fetchWithTimeout(`${NOTION_BASE}/pages`, { method: "POST", headers: headers(token), body: JSON.stringify({ parent: { database_id: timingDbId }, icon: { type: "emoji", emoji: "\u23F1\uFE0F" }, - properties: { - Name: { title: [{ text: { content: "Timer Task" } }] }, - "Start Time": { date: { start: now } }, - Projects: { relation: [{ id: projectId }] }, - Status: { status: { name: "Running" } }, - Responsible: { people: [{ object: "user", id: userId }] } - } + properties }) }); if (!resp.ok) throw new Error(`Failed to start timer: ${resp.status}`); @@ -6555,14 +6567,18 @@ async function sendProjectsToPI(tokenOverride) { await plugin_default.ui.sendToPropertyInspector({ event: "projects", data: [], error: "Enter your Notion API token above.", version: CURRENT_VERSION }); return; } - const [projects, usersResult] = await Promise.all([ + const [projects, usersResult, tagsResult] = await Promise.all([ fetchProjects(token, global.projectsDbId), fetchUsers(token).catch((err) => { plugin_default.logger.error("Failed to fetch users:", err); return []; + }), + fetchTagOptions(token, global.timingDbId, "Timer Tags").catch((err) => { + plugin_default.logger.error("Failed to fetch tag options:", err); + return []; }) ]); - await plugin_default.ui.sendToPropertyInspector({ event: "projects", data: projects, users: usersResult, version: CURRENT_VERSION }); + await plugin_default.ui.sendToPropertyInspector({ event: "projects", data: projects, users: usersResult, tags: tagsResult, version: CURRENT_VERSION }); } catch (err) { plugin_default.logger.error("Failed to fetch projects:", err); await plugin_default.ui.sendToPropertyInspector({ event: "projects", data: [], error: String(err), version: CURRENT_VERSION }); @@ -6571,8 +6587,9 @@ async function sendProjectsToPI(tokenOverride) { function isConfigured(g) { return !!(g.notionToken && g.userId); } -function buttonTitle(projectName) { - return projectName.replace(/^[\p{Extended_Pictographic}\uFE0F\s]+/u, "").trim(); +function buttonTitle(projectName, tag) { + const base = projectName.replace(/^[\p{Extended_Pictographic}\uFE0F\s]+/u, "").trim(); + return tag ? `${base} \xB7 ${tag}` : base; } var ELAPSED_CHECK_MS = 60 * 60 * 1e3; var DIALOG_TIMEOUT_SECONDS = 300; @@ -6592,6 +6609,22 @@ Should it continue?" buttons {"Stop", "Continue"} default button "Continue" givi }); }); } +async function promptDescription(title) { + const { execFile: execFile3 } = await import("node:child_process"); + const safeTitle = title.replace(/[\\"]/g, ""); + const script = `display dialog "Description for ${safeTitle} (optional):" default answer "" buttons {"Cancel", "Start"} default button "Start" giving up after ${DIALOG_TIMEOUT_SECONDS}`; + return new Promise((resolve) => { + execFile3("osascript", ["-e", script], (err, stdout) => { + const out = String(stdout); + if (err || /gave up:true/.test(out) || !/button returned:Start/.test(out)) { + resolve({ ok: false, text: "" }); + return; + } + const match = /text returned:(.*?)(?:, gave up:|$)/.exec(out); + resolve({ ok: true, text: match ? match[1].trim() : "" }); + }); + }); +} var TimerToggle = class extends SingletonAction { projectCache = /* @__PURE__ */ new Map(); keyDownBusy = false; @@ -6631,7 +6664,7 @@ var TimerToggle = class extends SingletonAction { } async onWillAppear(ev) { this.projectCache.set(ev.action.id, ev.payload.settings); - const title = buttonTitle(ev.payload.settings.projectName || ""); + const title = buttonTitle(ev.payload.settings.projectName || "", ev.payload.settings.tag); await loadRunningState(); const isRunning = memRunningActionId === ev.action.id; if (isRunning) { @@ -6645,7 +6678,7 @@ var TimerToggle = class extends SingletonAction { } async onKeyDown(ev) { if (this.keyDownBusy) { - const title = buttonTitle(ev.payload.settings.projectName || ""); + const title = buttonTitle(ev.payload.settings.projectName || "", ev.payload.settings.tag); const shouldBeRunning = memRunningActionId === ev.action.id; await new Promise((resolve) => setTimeout(resolve, 150)); await Promise.all([ @@ -6656,9 +6689,15 @@ var TimerToggle = class extends SingletonAction { } this.keyDownBusy = true; try { - const { projectId, projectName } = ev.payload.settings; - const title = buttonTitle(projectName || ""); + const { projectId, projectName, tag, askDescription } = ev.payload.settings; + const title = buttonTitle(projectName || "", tag); const isRunning = memRunningActionId === ev.action.id; + let description = ""; + if (!isRunning && projectId && askDescription) { + const result = await promptDescription(title); + if (!result.ok) return; + description = result.text; + } if (projectId) { if (isRunning) { await Promise.all([ev.action.setState(0), ev.action.setTitle(title)]); @@ -6667,7 +6706,7 @@ var TimerToggle = class extends SingletonAction { if (other.id === ev.action.id) continue; if (memRunningActionId === other.id) { const s = this.projectCache.get(other.id); - await Promise.all([other.setState(0), other.setTitle(buttonTitle(s?.projectName || ""))]); + await Promise.all([other.setState(0), other.setTitle(buttonTitle(s?.projectName || "", s?.tag))]); } } await Promise.all([ev.action.setState(1), ev.action.setTitle(`\u23F1 ${title}`)]); @@ -6702,7 +6741,7 @@ var TimerToggle = class extends SingletonAction { setRunningEntry(null, null); this.clearElapsedCheck(); } - const entryId = await startTimer(global.notionToken, global.timingDbId, projectId, projectName, global.userId); + const entryId = await startTimer(global.notionToken, global.timingDbId, projectId, projectName, global.userId, tag, description); setRunningEntry(entryId, ev.action.id); this.scheduleElapsedCheck(entryId, ev.action.id, title); await Promise.all([ev.action.setState(1), ev.action.setTitle(`\u23F1 ${title}`)]); @@ -6749,7 +6788,7 @@ plugin_default.actions.registerAction(new LaunchNextcloud()); plugin_default.ui.onSendToPlugin(async (ev) => { if (ev.payload.event === "saveSettings" && ev.payload.settings) { await ev.action.setSettings(ev.payload.settings); - const title = buttonTitle(ev.payload.settings.projectName || ""); + const title = buttonTitle(ev.payload.settings.projectName || "", ev.payload.settings.tag); if (title) await ev.action.setTitle(title); } if (ev.payload.event === "refreshProjects") { diff --git a/com.pdma.notion-timer.sdPlugin/bin/plugin.js.sig b/com.pdma.notion-timer.sdPlugin/bin/plugin.js.sig index c2f133a..f5c1f74 100644 Binary files a/com.pdma.notion-timer.sdPlugin/bin/plugin.js.sig and b/com.pdma.notion-timer.sdPlugin/bin/plugin.js.sig differ diff --git a/com.pdma.notion-timer.sdPlugin/ui/property-inspector.html b/com.pdma.notion-timer.sdPlugin/ui/property-inspector.html index 3dc4c73..589e3d1 100644 --- a/com.pdma.notion-timer.sdPlugin/ui/property-inspector.html +++ b/com.pdma.notion-timer.sdPlugin/ui/property-inspector.html @@ -58,6 +58,11 @@ padding-left: 98px; line-height: 1.4; } + .info-icon { + color: #888; + font-size: 13px; + cursor: help; + } .divider { border: none; border-top: 1px solid #333; @@ -142,6 +147,22 @@ +
Recorded as the entry's Tag in Notion and shown on the button.
@@ -163,6 +184,7 @@ var credConfigured = false; var globalUserId = ""; var cachedUsers = []; + var cachedTags = []; function setStatus(msg, cls) { var el = document.getElementById("statusText"); @@ -224,11 +246,39 @@ }); } + function populateTags(tags) { + cachedTags = tags; + var sel = document.getElementById("tagSelect"); + sel.innerHTML = ''; + var values = tags.slice(); + // A tag saved on this button before it existed as a Notion select option + // (or since removed there) must still show up, selected, instead of + // silently reverting to "No tag". + if (currentSettings.tag && values.indexOf(currentSettings.tag) === -1) { + values.push(currentSettings.tag); + } + values.forEach(function(t) { + var opt = document.createElement("option"); + opt.value = t; + opt.textContent = t; + if (currentSettings.tag === t) opt.selected = true; + sel.appendChild(opt); + }); + } + + function setTagRowVisible(visible) { + document.getElementById("tagRow").style.display = visible ? "" : "none"; + document.getElementById("tagHint").style.display = visible ? "" : "none"; + } + function save() { var sel = document.getElementById("projectSelect"); var opt = sel.options[sel.selectedIndex]; currentSettings.projectId = sel.value; currentSettings.projectName = sel.value ? opt.textContent.trim() : ""; + currentSettings.tagEnabled = document.getElementById("tagEnabled").checked; + currentSettings.tag = currentSettings.tagEnabled ? document.getElementById("tagSelect").value : ""; + currentSettings.askDescription = document.getElementById("askDescription").checked; $PI.setSettings(currentSettings); $PI.sendToPlugin({ event: "saveSettings", settings: currentSettings }); setStatus(currentSettings.projectName ? "Saved: " + currentSettings.projectName : "", ""); @@ -255,10 +305,19 @@ $PI.onConnected(function(jsn) { currentSettings = jsn.actionInfo.payload.settings || {}; + document.getElementById("tagEnabled").checked = !!currentSettings.tagEnabled; + setTagRowVisible(!!currentSettings.tagEnabled); + document.getElementById("askDescription").checked = !!currentSettings.askDescription; $PI.getSettings(); $PI.getGlobalSettings(); document.getElementById("projectSelect").addEventListener("change", save); + document.getElementById("tagEnabled").addEventListener("change", function() { + setTagRowVisible(this.checked); + save(); + }); + document.getElementById("tagSelect").addEventListener("change", save); + document.getElementById("askDescription").addEventListener("change", save); document.getElementById("notionToken").addEventListener("input", scheduleCredSave); document.getElementById("userId").addEventListener("change", saveCredentials); document.getElementById("updateBtn").addEventListener("click", function() { @@ -296,6 +355,10 @@ if (currentSettings.projectId && sel.options.length > 1) { sel.value = currentSettings.projectId; } + document.getElementById("tagEnabled").checked = !!currentSettings.tagEnabled; + setTagRowVisible(!!currentSettings.tagEnabled); + populateTags(cachedTags); + document.getElementById("askDescription").checked = !!currentSettings.askDescription; if (currentSettings.activeEntryId) { setStatus("⏱ Timer running", "running"); } @@ -313,6 +376,9 @@ if (payload.users) { populateUsers(payload.users, globalUserId); } + if (payload.tags) { + populateTags(payload.tags); + } if (payload.error) { setStatus(payload.error, "error"); } else { diff --git a/src/notion.ts b/src/notion.ts index c9c7bc5..db5c250 100644 --- a/src/notion.ts +++ b/src/notion.ts @@ -63,6 +63,20 @@ export async function fetchUsers(token: string): Promise