Implement activity log (task 22)
- activity_log table in _core/schema.ts with household+created_at index; migration 0008_activity_log.sql applied - logActivity() in _core/activity.ts reads current session and inserts a row - ActivityLogEntry type + renderActivity?(entry): string added to EntityTypeRegistration so modules declare human-readable labels without any if/else branches in the widget - core.activity widget replaced with a real async server component that queries the last 20 rows and renders via the registry - logActivity wired into every create/update/delete in calendar, lists, and notes server actions; getAuthorizedItem also returns text so toggle/delete can include item text in the payload - pnpm typecheck, lint, build, and all 4 E2E specs pass Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
1133fa8aac
commit
28475e483d
@@ -21,6 +21,7 @@ Living progress tracker. Update at the end of each task. Codex and Claude Code b
|
||||
- **12 — Notes module**. Added generic core `reminders` table plus household-scoped `notes` schema in migration `0006_new_hannibal_king.sql`, notes CRUD server actions, reminder synchronization for `notes.note`, `/notes` index, `/notes/new`, `/notes/[id]` editor with safe markdown preview, manifest entity/search/reminder/share registration, `notes.filtered` widget registration, quick-add placeholder, and Playwright happy-path spec in `tests/e2e/notes.spec.ts`. `pnpm typecheck`, `pnpm lint`, `pnpm build`, and notes E2E pass.
|
||||
- **20 — Dashboard composition (single-dashboard MVP)**. Added `default_dashboard_layout` jsonb column to `users` + migration `0007_uneven_living_lightning.sql`. Created `src/modules/_core/manifest.tsx` (`core.activity` placeholder widget) and registered it. Updated all three module manifests (calendar, lists, notes) with real async server component widget renders (data-fetching, empty states). Created `src/lib/dashboard.ts` (layout parsing + `computeDefaultLayout` greedy packer). Built `src/app/page.tsx` — 12-col CSS Grid, static `smColSpan` lookup for Tailwind class safety, per-widget `<Suspense>` for parallel loading, graceful skip for unknown widget IDs. `pnpm typecheck`, `pnpm lint`, `pnpm build`, and all 4 E2E specs pass.
|
||||
- **21 — Quick-add registry**. Added `url: string` to `QuickAddAction` type (action is now optional). Added `getQuickAdds()` / `SerializedQuickAddItem` to registry (strips non-serializable `action` fn before crossing server→client boundary). Updated all three module manifests with navigation URLs. Built `QuickAddProvider` (context + cmd+k global shortcut), `QuickAddFab` (opens sheet, replaces plain button in dashboard), `QuickAddSheet` (bottom drawer / desktop popover grouped by module), and `CommandPalette` (cmdk-powered modal with arrow + enter + esc keyboard nav). Provider in root layout receives actions from `getQuickAdds()` at render time — adding a module's `quickAdds` automatically appears in both surfaces. Also added `.claude/**` to ESLint ignores to prevent stale worktree build artifacts from failing lint. `pnpm typecheck`, `pnpm lint`, `pnpm build`, and all 4 E2E specs pass.
|
||||
- **22 — Activity log**. Added `activity_log` table to `_core/schema.ts` with index on `(household_id, created_at desc)`. Migration `0008_activity_log.sql` applied. `logActivity()` server function in `_core/activity.ts` reads current session and inserts a row. Added `ActivityLogEntry` type and optional `renderActivity?(entry): string` to `EntityTypeRegistration` in `_core/module.ts`. All three module manifests implement `renderActivity` for each entity type (human-readable, no hardcoded branches in the widget). Replaced `core.activity` widget stub with a real async server component that queries the last 20 rows via `getEntityType(entry.entityType)?.renderActivity(entry)`. Wired `logActivity()` into every create/update/delete in calendar, lists, and notes server actions. Also added `text` to `getAuthorizedItem` select so toggle/delete log the item text. `pnpm typecheck`, `pnpm lint`, `pnpm build`, and all 4 E2E specs pass.
|
||||
|
||||
## Next up
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
CREATE TABLE "activity_log" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"household_id" uuid NOT NULL,
|
||||
"entity_type" text NOT NULL,
|
||||
"entity_id" uuid NOT NULL,
|
||||
"actor_id" uuid NOT NULL,
|
||||
"action" text NOT NULL,
|
||||
"payload" jsonb,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "activity_log" ADD CONSTRAINT "activity_log_household_id_households_id_fk" FOREIGN KEY ("household_id") REFERENCES "public"."households"("id") ON DELETE cascade ON UPDATE no action;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "activity_log" ADD CONSTRAINT "activity_log_actor_id_users_id_fk" FOREIGN KEY ("actor_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX "activity_log_household_created_idx" ON "activity_log" USING btree ("household_id","created_at");
|
||||
@@ -0,0 +1,20 @@
|
||||
import { db } from "@/lib/db";
|
||||
import { getCurrentSession } from "@/lib/session";
|
||||
import { activityLog } from "./schema";
|
||||
|
||||
export async function logActivity(input: {
|
||||
entityType: string;
|
||||
entityId: string;
|
||||
action: string;
|
||||
payload?: Record<string, unknown>;
|
||||
}): Promise<void> {
|
||||
const { user, household } = await getCurrentSession();
|
||||
await db.insert(activityLog).values({
|
||||
householdId: household.id,
|
||||
entityType: input.entityType,
|
||||
entityId: input.entityId,
|
||||
actorId: user.id,
|
||||
action: input.action,
|
||||
payload: input.payload ?? null,
|
||||
});
|
||||
}
|
||||
@@ -8,6 +8,8 @@ export type {
|
||||
ReminderCapabilities,
|
||||
SearchAdapter,
|
||||
SearchResult,
|
||||
ActivityLogEntry,
|
||||
} from "./module";
|
||||
export { registerModule, getRegistry, getEntityType, getWidget, getQuickAdds } from "./registry";
|
||||
export type { QuickAddItem, SerializedQuickAddItem } from "./registry";
|
||||
export { logActivity } from "./activity";
|
||||
|
||||
@@ -1,5 +1,49 @@
|
||||
import type { ModuleManifest } from "./module";
|
||||
import { desc, eq } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { db } from "@/lib/db";
|
||||
import { getCurrentSession } from "@/lib/session";
|
||||
import type { ActivityLogEntry, ModuleManifest } from "./module";
|
||||
import { getEntityType } from "./registry";
|
||||
import { activityLog } from "./schema";
|
||||
|
||||
const activityConfigSchema = z.object({
|
||||
limit: z.number().int().min(1).max(50).optional(),
|
||||
});
|
||||
|
||||
async function ActivityWidget({ config }: { config: unknown }) {
|
||||
const parsed = activityConfigSchema.parse(config);
|
||||
const { household } = await getCurrentSession();
|
||||
|
||||
const entries = await db
|
||||
.select()
|
||||
.from(activityLog)
|
||||
.where(eq(activityLog.householdId, household.id))
|
||||
.orderBy(desc(activityLog.createdAt))
|
||||
.limit(parsed.limit ?? 20);
|
||||
|
||||
if (entries.length === 0) {
|
||||
return <p className="text-sm text-muted-foreground">No recent activity</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<ul className="space-y-2">
|
||||
{entries.map((entry) => {
|
||||
const reg = getEntityType(entry.entityType);
|
||||
const description =
|
||||
reg?.renderActivity?.(entry as ActivityLogEntry) ??
|
||||
`${entry.action} ${entry.entityType}`;
|
||||
return (
|
||||
<li key={entry.id} className="flex items-start gap-2 text-sm">
|
||||
<span className="mt-0.5 shrink-0 text-xs text-muted-foreground">
|
||||
{entry.createdAt.toLocaleDateString(undefined, { month: "short", day: "numeric" })}
|
||||
</span>
|
||||
<span className="leading-snug">{description}</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
const coreManifest: ModuleManifest = {
|
||||
id: "_core",
|
||||
@@ -14,16 +58,10 @@ const coreManifest: ModuleManifest = {
|
||||
defaultSize: { w: 4, h: 3 },
|
||||
minSize: { w: 3, h: 2 },
|
||||
defaultPriority: 50,
|
||||
configSchema: z.object({
|
||||
limit: z.number().int().min(1).max(50).optional(),
|
||||
}),
|
||||
defaultConfig: { limit: 10 },
|
||||
configSchema: activityConfigSchema,
|
||||
defaultConfig: { limit: 20 },
|
||||
resolveConfigOptions: async () => undefined,
|
||||
render: () => (
|
||||
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">
|
||||
Activity log coming in task 22
|
||||
</div>
|
||||
),
|
||||
render: (props) => <ActivityWidget {...props} />,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
import type { ReactNode } from "react";
|
||||
import type { ZodType } from "zod";
|
||||
|
||||
export type ActivityLogEntry = {
|
||||
id: string;
|
||||
householdId: string;
|
||||
entityType: string;
|
||||
entityId: string;
|
||||
actorId: string;
|
||||
action: string;
|
||||
payload: Record<string, unknown> | null;
|
||||
createdAt: Date;
|
||||
};
|
||||
|
||||
export type ShareCapabilities = {
|
||||
canShare: boolean;
|
||||
defaultCapabilities?: string[];
|
||||
@@ -58,6 +69,7 @@ export type EntityTypeRegistration = {
|
||||
search?: SearchAdapter;
|
||||
resolveUrl: (id: string) => string;
|
||||
loadForShare?: (id: string) => Promise<unknown>;
|
||||
renderActivity?: (entry: ActivityLogEntry) => string;
|
||||
};
|
||||
|
||||
export type ModuleManifest = {
|
||||
|
||||
@@ -87,6 +87,25 @@ export const householdMembers = pgTable(
|
||||
(t) => [primaryKey({ columns: [t.householdId, t.userId] })],
|
||||
);
|
||||
|
||||
export const activityLog = pgTable(
|
||||
"activity_log",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
householdId: uuid("household_id")
|
||||
.notNull()
|
||||
.references(() => households.id, { onDelete: "cascade" }),
|
||||
entityType: text("entity_type").notNull(),
|
||||
entityId: uuid("entity_id").notNull(),
|
||||
actorId: uuid("actor_id")
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
action: text("action").notNull(),
|
||||
payload: jsonb("payload").$type<Record<string, unknown> | null>(),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(t) => [index("activity_log_household_created_idx").on(t.householdId, t.createdAt)],
|
||||
);
|
||||
|
||||
export const reminders = pgTable(
|
||||
"reminders",
|
||||
{
|
||||
|
||||
@@ -101,6 +101,12 @@ const manifest: ModuleManifest = {
|
||||
share: { canShare: true, defaultCapabilities: ["read"] },
|
||||
search: { search: searchCalendars },
|
||||
resolveUrl: (id) => `/calendar?id=${id}`,
|
||||
renderActivity: (entry) => {
|
||||
const name = entry.payload?.name as string | undefined;
|
||||
if (entry.action === "create") return `Created calendar${name ? ` "${name}"` : ""}`;
|
||||
if (entry.action === "delete") return "Deleted calendar";
|
||||
return `Updated calendar${name ? ` "${name}"` : ""}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "calendar.event",
|
||||
@@ -109,6 +115,12 @@ const manifest: ModuleManifest = {
|
||||
reminder: { canRemind: true },
|
||||
search: { search: searchEvents },
|
||||
resolveUrl: (id) => `/calendar/events/${id}`,
|
||||
renderActivity: (entry) => {
|
||||
const title = entry.payload?.title as string | undefined;
|
||||
if (entry.action === "create") return `Created event${title ? ` "${title}"` : ""}`;
|
||||
if (entry.action === "delete") return "Deleted event";
|
||||
return `Updated event${title ? ` "${title}"` : ""}`;
|
||||
},
|
||||
},
|
||||
],
|
||||
dashboardWidgets: [
|
||||
|
||||
@@ -5,6 +5,7 @@ import { revalidatePath } from "next/cache";
|
||||
import { z } from "zod";
|
||||
import { db } from "@/lib/db";
|
||||
import { getCurrentSession } from "@/lib/session";
|
||||
import { logActivity } from "@/modules/_core/activity";
|
||||
import { calendarEvents, calendars } from "../schema";
|
||||
import { canSeeCalendar } from "./queries";
|
||||
|
||||
@@ -55,6 +56,7 @@ export async function createCalendar(input: z.input<typeof calendarInput>) {
|
||||
.returning();
|
||||
|
||||
if (!calendar) throw new Error("Calendar was not created");
|
||||
await logActivity({ entityType: "calendar.calendar", entityId: calendar.id, action: "create", payload: { name: calendar.name } });
|
||||
revalidatePath("/calendar");
|
||||
return calendar;
|
||||
}
|
||||
@@ -68,6 +70,7 @@ export async function renameCalendar(input: { id: string; name: string }) {
|
||||
.set({ name: parsed.name, updatedAt: new Date() })
|
||||
.where(eq(calendars.id, parsed.id));
|
||||
|
||||
await logActivity({ entityType: "calendar.calendar", entityId: parsed.id, action: "update", payload: { name: parsed.name } });
|
||||
revalidatePath("/calendar");
|
||||
}
|
||||
|
||||
@@ -85,6 +88,7 @@ export async function setCalendarVisibility(input: {
|
||||
.set({ visibility: parsed.visibility, updatedAt: new Date() })
|
||||
.where(eq(calendars.id, parsed.id));
|
||||
|
||||
await logActivity({ entityType: "calendar.calendar", entityId: parsed.id, action: "update", payload: { visibility: parsed.visibility } });
|
||||
revalidatePath("/calendar");
|
||||
}
|
||||
|
||||
@@ -104,6 +108,7 @@ export async function deleteCalendar(input: { id: string }) {
|
||||
const { user } = await getCurrentSession();
|
||||
const parsed = z.object({ id: z.string().uuid() }).parse(input);
|
||||
await assertOwnsCalendar(user.id, parsed.id);
|
||||
await logActivity({ entityType: "calendar.calendar", entityId: parsed.id, action: "delete" });
|
||||
await db.delete(calendars).where(eq(calendars.id, parsed.id));
|
||||
revalidatePath("/calendar");
|
||||
}
|
||||
@@ -124,6 +129,7 @@ export async function createEvent(input: z.input<typeof eventInput>) {
|
||||
.returning();
|
||||
|
||||
if (!event) throw new Error("Event was not created");
|
||||
await logActivity({ entityType: "calendar.event", entityId: event.id, action: "create", payload: { title: event.title } });
|
||||
revalidatePath("/calendar");
|
||||
return {
|
||||
...event,
|
||||
@@ -159,6 +165,7 @@ export async function updateEvent(input: { id: string } & Partial<z.input<typeof
|
||||
})
|
||||
.where(eq(calendarEvents.id, parsed.id));
|
||||
|
||||
await logActivity({ entityType: "calendar.event", entityId: parsed.id, action: "update", payload: parsed.title ? { title: parsed.title } : undefined });
|
||||
revalidatePath("/calendar");
|
||||
}
|
||||
|
||||
@@ -173,6 +180,7 @@ export async function deleteEvent(input: { id: string }) {
|
||||
|
||||
if (!existing) return;
|
||||
if (!(await canSeeCalendar(user.id, existing.calendarId))) throw new Error("Forbidden");
|
||||
await logActivity({ entityType: "calendar.event", entityId: parsed.id, action: "delete" });
|
||||
await db.delete(calendarEvents).where(eq(calendarEvents.id, parsed.id));
|
||||
revalidatePath("/calendar");
|
||||
}
|
||||
|
||||
@@ -54,6 +54,13 @@ const manifest: ModuleManifest = {
|
||||
share: { canShare: true, defaultCapabilities: ["read", "write"] },
|
||||
search: { search: searchLists },
|
||||
resolveUrl: (id) => `/lists/${id}`,
|
||||
renderActivity: (entry) => {
|
||||
const name = entry.payload?.name as string | undefined;
|
||||
if (entry.action === "create") return `Created list${name ? ` "${name}"` : ""}`;
|
||||
if (entry.action === "archive") return `Archived list${name ? ` "${name}"` : ""}`;
|
||||
if (entry.action === "delete") return "Deleted list";
|
||||
return `Updated list${name ? ` "${name}"` : ""}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "lists.item",
|
||||
@@ -61,6 +68,17 @@ const manifest: ModuleManifest = {
|
||||
share: { canShare: false },
|
||||
search: { search: searchItems },
|
||||
resolveUrl: (id) => `/lists/items/${id}`,
|
||||
renderActivity: (entry) => {
|
||||
const text = entry.payload?.text as string | undefined;
|
||||
if (entry.action === "create") return `Added${text ? ` "${text}"` : " item"}`;
|
||||
if (entry.action === "delete") return `Removed${text ? ` "${text}"` : " item"}`;
|
||||
if (entry.action === "toggle") {
|
||||
const done = entry.payload?.done as boolean | undefined;
|
||||
const label = text ? ` "${text}"` : " item";
|
||||
return done ? `Checked off${label}` : `Unchecked${label}`;
|
||||
}
|
||||
return `Updated${text ? ` "${text}"` : " item"}`;
|
||||
},
|
||||
},
|
||||
],
|
||||
dashboardWidgets: [
|
||||
|
||||
@@ -5,6 +5,7 @@ import { revalidatePath } from "next/cache";
|
||||
import { z } from "zod";
|
||||
import { db } from "@/lib/db";
|
||||
import { getCurrentSession } from "@/lib/session";
|
||||
import { logActivity } from "@/modules/_core/activity";
|
||||
import { listItems, lists } from "../schema";
|
||||
import { getOrCreateDefaultList } from "./defaults";
|
||||
import { canAccessList, getList } from "./queries";
|
||||
@@ -46,6 +47,7 @@ export async function createList(input: z.input<typeof listInput>) {
|
||||
.returning();
|
||||
|
||||
if (!list) throw new Error("List was not created");
|
||||
await logActivity({ entityType: "lists.list", entityId: list.id, action: "create", payload: { name: list.name } });
|
||||
revalidatePath("/lists");
|
||||
return list;
|
||||
}
|
||||
@@ -55,6 +57,7 @@ export async function renameList(input: { id: string; name: string }) {
|
||||
const { household } = await getCurrentSession();
|
||||
await assertCanAccessList(parsed.id, household.id);
|
||||
await db.update(lists).set({ name: parsed.name }).where(eq(lists.id, parsed.id));
|
||||
await logActivity({ entityType: "lists.list", entityId: parsed.id, action: "update", payload: { name: parsed.name } });
|
||||
revalidatePath("/lists");
|
||||
revalidatePath(`/lists/${parsed.id}`);
|
||||
await notifyListChanged(parsed.id);
|
||||
@@ -65,6 +68,7 @@ export async function archiveList(input: { id: string }) {
|
||||
const { household } = await getCurrentSession();
|
||||
await assertCanAccessList(parsed.id, household.id);
|
||||
await db.update(lists).set({ archived: true }).where(eq(lists.id, parsed.id));
|
||||
await logActivity({ entityType: "lists.list", entityId: parsed.id, action: "archive" });
|
||||
revalidatePath("/lists");
|
||||
revalidatePath(`/lists/${parsed.id}`);
|
||||
await notifyListChanged(parsed.id);
|
||||
@@ -94,6 +98,7 @@ export async function addItem(input: z.input<typeof itemInput>) {
|
||||
.returning();
|
||||
|
||||
if (!item) throw new Error("List item was not created");
|
||||
await logActivity({ entityType: "lists.item", entityId: item.id, action: "create", payload: { text: item.text } });
|
||||
revalidatePath(`/lists/${parsed.listId}`);
|
||||
await notifyListChanged(parsed.listId);
|
||||
return getList(parsed.listId);
|
||||
@@ -122,6 +127,7 @@ export async function toggleItem(input: { id: string; done?: boolean }) {
|
||||
.set({ done, updatedAt: new Date() })
|
||||
.where(eq(listItems.id, parsed.id));
|
||||
|
||||
await logActivity({ entityType: "lists.item", entityId: parsed.id, action: "toggle", payload: { done, text: existing.text } });
|
||||
revalidatePath(`/lists/${existing.listId}`);
|
||||
await notifyListChanged(existing.listId);
|
||||
return getList(existing.listId);
|
||||
@@ -144,6 +150,7 @@ export async function updateItem(input: z.input<typeof updateItemInput>) {
|
||||
})
|
||||
.where(eq(listItems.id, parsed.id));
|
||||
|
||||
await logActivity({ entityType: "lists.item", entityId: parsed.id, action: "update", payload: { text: parsed.text ?? existing.text } });
|
||||
revalidatePath(`/lists/${existing.listId}`);
|
||||
await notifyListChanged(existing.listId);
|
||||
return getList(existing.listId);
|
||||
@@ -153,6 +160,7 @@ export async function deleteItem(input: { id: string }) {
|
||||
const parsed = z.object({ id: z.string().uuid() }).parse(input);
|
||||
const { household } = await getCurrentSession();
|
||||
const existing = await getAuthorizedItem(parsed.id, household.id);
|
||||
await logActivity({ entityType: "lists.item", entityId: parsed.id, action: "delete", payload: { text: existing.text } });
|
||||
await db.delete(listItems).where(eq(listItems.id, parsed.id));
|
||||
revalidatePath(`/lists/${existing.listId}`);
|
||||
await notifyListChanged(existing.listId);
|
||||
@@ -190,6 +198,7 @@ async function getAuthorizedItem(itemId: string, householdId: string) {
|
||||
id: listItems.id,
|
||||
listId: listItems.listId,
|
||||
done: listItems.done,
|
||||
text: listItems.text,
|
||||
})
|
||||
.from(listItems)
|
||||
.innerJoin(lists, eq(listItems.listId, lists.id))
|
||||
|
||||
@@ -45,6 +45,14 @@ const manifest: ModuleManifest = {
|
||||
reminder: { canRemind: true },
|
||||
search: { search: searchNotes },
|
||||
resolveUrl: (id) => `/notes/${id}`,
|
||||
renderActivity: (entry) => {
|
||||
const title = entry.payload?.title as string | undefined;
|
||||
if (entry.action === "create") return `Created note${title ? ` "${title}"` : ""}`;
|
||||
if (entry.action === "delete") return `Deleted note${title ? ` "${title}"` : ""}`;
|
||||
if (entry.action === "pin") return `Pinned note${title ? ` "${title}"` : ""}`;
|
||||
if (entry.action === "unpin") return `Unpinned note${title ? ` "${title}"` : ""}`;
|
||||
return `Updated note${title ? ` "${title}"` : ""}`;
|
||||
},
|
||||
},
|
||||
],
|
||||
dashboardWidgets: [
|
||||
|
||||
@@ -5,6 +5,7 @@ import { revalidatePath } from "next/cache";
|
||||
import { z } from "zod";
|
||||
import { db } from "@/lib/db";
|
||||
import { getCurrentSession } from "@/lib/session";
|
||||
import { logActivity } from "@/modules/_core/activity";
|
||||
import { reminders } from "@/modules/_core/schema";
|
||||
import { notes } from "../schema";
|
||||
import { canAccessNote, getNote } from "./queries";
|
||||
@@ -50,6 +51,7 @@ export async function createNote(input: z.input<typeof noteInput>) {
|
||||
return [created];
|
||||
});
|
||||
|
||||
if (note) await logActivity({ entityType: "notes.note", entityId: note.id, action: "create", payload: { title: note.title } });
|
||||
revalidatePath("/notes");
|
||||
return note;
|
||||
}
|
||||
@@ -83,6 +85,7 @@ export async function updateNote(input: z.input<typeof updateNoteInput>) {
|
||||
return [updated];
|
||||
});
|
||||
|
||||
if (note) await logActivity({ entityType: "notes.note", entityId: note.id, action: "update", payload: { title: note.title } });
|
||||
revalidatePath("/notes");
|
||||
revalidatePath(`/notes/${parsed.id}`);
|
||||
return note;
|
||||
@@ -98,9 +101,11 @@ export async function setNotePinned(input: { id: string; pinned: boolean }) {
|
||||
.set({ pinned: parsed.pinned, updatedAt: new Date() })
|
||||
.where(eq(notes.id, parsed.id));
|
||||
|
||||
const note = await getNote(parsed.id);
|
||||
await logActivity({ entityType: "notes.note", entityId: parsed.id, action: parsed.pinned ? "pin" : "unpin", payload: note ? { title: note.title } : undefined });
|
||||
revalidatePath("/notes");
|
||||
revalidatePath(`/notes/${parsed.id}`);
|
||||
return getNote(parsed.id);
|
||||
return note;
|
||||
}
|
||||
|
||||
export async function deleteNote(input: { id: string }) {
|
||||
@@ -108,6 +113,9 @@ export async function deleteNote(input: { id: string }) {
|
||||
const { household } = await getCurrentSession();
|
||||
await assertCanAccessNote(parsed.id, household.id);
|
||||
|
||||
const note = await getNote(parsed.id);
|
||||
await logActivity({ entityType: "notes.note", entityId: parsed.id, action: "delete", payload: note ? { title: note.title } : undefined });
|
||||
|
||||
await db.transaction(async (tx) => {
|
||||
await tx
|
||||
.delete(reminders)
|
||||
|
||||
Reference in New Issue
Block a user