From 68132ff4b39bd6c4c4c6ed9216cfc1d3a8a79a3e Mon Sep 17 00:00:00 2001 From: Dan Connolly Date: Fri, 9 Oct 2026 02:06:27 -0500 Subject: [PATCH] diigo-week: a dashboard card drafts last week in one click The card shows last week's issue with a link to it, or a button to draft it. Drafting a week that already has an issue (draft or published) is refused, with a link to that issue, rather than making a duplicate. Generated-by: Claude Opus 5.5 --- diigo-week/README.md | 2 +- diigo-week/src/index.ts | 106 ++++++++++++++++++++++++++++------------ 2 files changed, 77 insertions(+), 31 deletions(-) diff --git a/diigo-week/README.md b/diigo-week/README.md index fa3844a..86db994 100644 --- a/diigo-week/README.md +++ b/diigo-week/README.md @@ -12,7 +12,7 @@ pnpm install (cd ../potluck && pnpm dev) # → http://localhost:4321/_emdash/admin ``` -In potluck's admin: Plugins → **Draft from diigo** → **Draft issue**. The draft shows up under Issues. +On potluck's admin dashboard, the **This week's issue** card has a **Draft 2026-Wnn from diigo** button for last week; it links to the draft once made. For any other week: Plugins → **Draft from diigo**. A week that already has an issue isn't drafted again. ## How it reads the week diff --git a/diigo-week/src/index.ts b/diigo-week/src/index.ts index f51442e..12c71f8 100644 --- a/diigo-week/src/index.ts +++ b/diigo-week/src/index.ts @@ -61,6 +61,56 @@ async function tagIssue(ctx: RouteContext, id: string, tags: string[]): Promise< return failed; } +/** the issue already drafted or published for `week`, if any (issues + * are few: a scan of the newest is enough) */ +async function issueFor(ctx: RouteContext, week: string): Promise<{ id: string; status: string; title: string } | undefined> { + const found = (await ctx.content!.list("issues", { limit: 100 })).items.find((it) => it.data.week === week); + return found && { id: found.id, status: found.status, title: String(found.data.title ?? week) }; +} + +/** a button-row link to an issue in the editor */ +const openIssue = (id: string, label: string): Block => ({ + type: "actions", + elements: [{ type: "link", label, target: { kind: "content", collection: "issues", id }, appearance: "primary" }], +}); + +/** draft `week` from `user`'s bookmarks, unless the week has an issue + * already: blocks that say what happened (with a link), and a toast */ +async function draft(ctx: RouteContext, user: string, week: string): Promise<{ note: Block[]; toast: BlockResponse["toast"] }> { + try { + if (!/^[A-Za-z0-9_-]{1,64}$/.test(user)) throw new Error("diigo username: letters, digits, _ and - only"); + const existing = await issueFor(ctx, week); + if (existing) { + return { + note: [ + { type: "banner", variant: "default", title: `${week} already has an issue`, description: `"${existing.title}" (${existing.status}). Delete it to draft the week again.` }, + openIssue(existing.id, "Open it"), + ], + toast: { message: `${week} already has an issue`, type: "info" }, + }; + } + const bookmarks = await readWeek(await getter(await diigoCache()), user, week); + const issue = draftIssue(week, bookmarks); + const content = ctx.content; + if (typeof content?.create !== "function") throw new Error("this plugin lacks content:write"); + const created = await content.create("issues", issue); + const untagged = await tagIssue(ctx, created.id, issueTags(bookmarks)); + if (untagged.length) ctx.log.warn("tags not assigned", { id: created.id, untagged }); + ctx.log.info("drafted issue", { week, bookmarks: bookmarks.length, id: created.id }); + return { + note: [ + { type: "banner", variant: "default", title: `Drafted "${issue.title}"`, description: `${bookmarks.length} bookmarks, as a draft at /issues/${created.slug ?? "(no slug)"}. Cut, comment, and publish.${untagged.length ? ` Tags not assigned: ${untagged.join(", ")}.` : ""}` }, + openIssue(created.id, "Open the draft"), + ], + toast: { message: `Drafted ${issue.title}`, type: "success" }, + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + ctx.log.error("draft failed", { week, message }); + return { note: [{ type: "banner", variant: "alert", title: "Couldn't draft the issue", description: message }], toast: { message, type: "error" } }; + } +} + /** the admin page: a form for the diigo username and the week */ async function page(ctx: RouteContext, note: Block[] = [], toast?: BlockResponse["toast"]): Promise { const user = (await ctx.kv.get("user")) ?? DEFAULT_USER; @@ -84,6 +134,20 @@ async function page(ctx: RouteContext, note: Block[] = [], toast?: BlockResponse return { blocks, toast }; } +/** the dashboard widget: last week's issue if there is one, else a button to draft it */ +async function widget(ctx: RouteContext, note: Block[] = [], toast?: BlockResponse["toast"]): Promise { + const week = lastWeek(new Date()); + if (note.length) return { blocks: note, toast }; + const existing = await issueFor(ctx, week); + const blocks: Block[] = existing + ? [{ type: "section", text: `${week}: "${existing.title}" (${existing.status})` }, openIssue(existing.id, "Open it")] + : [ + { type: "section", text: `${week} has no issue yet.` }, + { type: "actions", elements: [{ type: "button", action_id: "draft-last-week", label: `Draft ${week} from diigo`, style: "primary" }] }, + ]; + return { blocks, toast }; +} + export function createPlugin(): PluginDefinition { return definePlugin({ id: "diigo-week", @@ -91,42 +155,24 @@ export function createPlugin(): PluginDefinition { capabilities: ["content:write", "taxonomies:write"], admin: { pages: [{ path: "/draft", label: "Draft from diigo", icon: "bookmark" }], + widgets: [{ id: "last-week", title: "This week's issue", size: "half" }], }, routes: { admin: { permission: "plugins:manage", handler: async (ctx) => { - const input = ctx.input as { type?: string; action_id?: string; values?: Record } | undefined; - if (input?.type !== "form_submit" || input.action_id !== "draft") return page(ctx); - const user = String(input.values?.user ?? "").trim(); - const week = String(input.values?.week ?? "").trim(); - try { - if (!/^[A-Za-z0-9_-]{1,64}$/.test(user)) throw new Error("diigo username: letters, digits, _ and - only"); - await ctx.kv.set("user", user); - const bookmarks = await readWeek(await getter(await diigoCache()), user, week); - const issue = draftIssue(week, bookmarks); - const content = ctx.content; - if (typeof content?.create !== "function") throw new Error("this plugin lacks content:write"); - const created = await content.create("issues", issue); - const untagged = await tagIssue(ctx, created.id, issueTags(bookmarks)); - if (untagged.length) ctx.log.warn("tags not assigned", { id: created.id, untagged }); - ctx.log.info("drafted issue", { week, bookmarks: bookmarks.length, id: created.id }); - return page( - ctx, - [ - { type: "banner", variant: "default", title: `Drafted "${issue.title}"`, description: `${bookmarks.length} bookmarks, as a draft at /issues/${created.slug ?? "(no slug)"}. Cut, comment, and publish.${untagged.length ? ` Tags not assigned: ${untagged.join(", ")}.` : ""}` }, - { - type: "actions", - elements: [{ type: "link", label: "Open the draft", target: { kind: "content", collection: "issues", id: created.id }, appearance: "primary" }], - }, - ], - { message: `Drafted ${issue.title}`, type: "success" }, - ); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - ctx.log.error("draft failed", { week, message }); - return page(ctx, [{ type: "banner", variant: "alert", title: "Couldn't draft the issue", description: message }], { message, type: "error" }); + const input = ctx.input as { type?: string; page?: string; action_id?: string; values?: Record } | undefined; + const user = (await ctx.kv.get("user")) ?? DEFAULT_USER; + if (input?.type === "block_action" && input.action_id === "draft-last-week") { + const { note, toast } = await draft(ctx, user, lastWeek(new Date())); + return widget(ctx, note, toast); } + if (input?.page?.startsWith("widget:")) return widget(ctx); + if (input?.type !== "form_submit" || input.action_id !== "draft") return page(ctx); + const formUser = String(input.values?.user ?? "").trim(); + if (/^[A-Za-z0-9_-]{1,64}$/.test(formUser)) await ctx.kv.set("user", formUser); + const { note, toast } = await draft(ctx, formUser, String(input.values?.week ?? "").trim()); + return page(ctx, note, toast); }, }, },