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
This commit is contained in:
1 parent
9c09e8c5a4
commit
68132ff4b3
2 files changed
+77
-31
No files matched your search
@@ -12,7 +12,7 @@ pnpm install
|
|||||||
(cd ../potluck && pnpm dev) # → http://localhost:4321/_emdash/admin
|
(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
|
## How it reads the week
|
||||||
|
|
||||||
|
|||||||
+76
-30
@@ -61,6 +61,56 @@ async function tagIssue(ctx: RouteContext, id: string, tags: string[]): Promise<
|
|||||||
return failed;
|
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 */
|
/** the admin page: a form for the diigo username and the week */
|
||||||
async function page(ctx: RouteContext, note: Block[] = [], toast?: BlockResponse["toast"]): Promise<BlockResponse> {
|
async function page(ctx: RouteContext, note: Block[] = [], toast?: BlockResponse["toast"]): Promise<BlockResponse> {
|
||||||
const user = (await ctx.kv.get<string>("user")) ?? DEFAULT_USER;
|
const user = (await ctx.kv.get<string>("user")) ?? DEFAULT_USER;
|
||||||
@@ -84,6 +134,20 @@ async function page(ctx: RouteContext, note: Block[] = [], toast?: BlockResponse
|
|||||||
return { blocks, toast };
|
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<BlockResponse> {
|
||||||
|
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 {
|
export function createPlugin(): PluginDefinition {
|
||||||
return definePlugin({
|
return definePlugin({
|
||||||
id: "diigo-week",
|
id: "diigo-week",
|
||||||
@@ -91,42 +155,24 @@ export function createPlugin(): PluginDefinition {
|
|||||||
capabilities: ["content:write", "taxonomies:write"],
|
capabilities: ["content:write", "taxonomies:write"],
|
||||||
admin: {
|
admin: {
|
||||||
pages: [{ path: "/draft", label: "Draft from diigo", icon: "bookmark" }],
|
pages: [{ path: "/draft", label: "Draft from diigo", icon: "bookmark" }],
|
||||||
|
widgets: [{ id: "last-week", title: "This week's issue", size: "half" }],
|
||||||
},
|
},
|
||||||
routes: {
|
routes: {
|
||||||
admin: {
|
admin: {
|
||||||
permission: "plugins:manage",
|
permission: "plugins:manage",
|
||||||
handler: async (ctx) => {
|
handler: async (ctx) => {
|
||||||
const input = ctx.input as { type?: string; action_id?: string; values?: Record<string, unknown> } | undefined;
|
const input = ctx.input as { type?: string; page?: string; action_id?: string; values?: Record<string, unknown> } | undefined;
|
||||||
if (input?.type !== "form_submit" || input.action_id !== "draft") return page(ctx);
|
const user = (await ctx.kv.get<string>("user")) ?? DEFAULT_USER;
|
||||||
const user = String(input.values?.user ?? "").trim();
|
if (input?.type === "block_action" && input.action_id === "draft-last-week") {
|
||||||
const week = String(input.values?.week ?? "").trim();
|
const { note, toast } = await draft(ctx, user, lastWeek(new Date()));
|
||||||
try {
|
return widget(ctx, note, toast);
|
||||||
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" });
|
|
||||||
}
|
}
|
||||||
|
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);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in new issue
Block a user