Add timer tags and optional start description
Buttons can carry a Timer Tags select value and prompt for an optional description before starting. Tag options and the prompt toggle are configured per-button in the property inspector; startTimer writes the tag to the Timer Tags select property and the description to the Description rich_text property when set.
This commit is contained in:
@@ -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") {
|
||||
|
||||
Reference in New Issue
Block a user