- Custom workflow statuses: per-project configurable status definitions replacing the fixed task_status enum. Each project defines its own workflow with drag-to-reorder, color coding, and category mapping. - Gantt/timeline view: full project roadmap with task bars, dependency arrows, milestone diamonds, zoom levels (day/week/month), and drag to reschedule. - Natural-language quick-add: NLP parser extracts dates, priorities, projects, labels, and recurrence from free text. Floating bar with 'n' shortcut and live parsed preview. - Automation rules: no-code trigger-action system per project. Triggers on status change, task creation, due date approaching. Actions set status/priority, add labels, create notifications. - Notification center: in-app bell icon with unread badge, slide-out panel, real-time SSE updates, mark read/all read. Replaces raw activity feed dropdown. Schema: adds status_definitions, automation_rules, notifications tables. Migrations: 0007, 0008, 0009. 41 NLP parser tests pass.
66 lines
2.1 KiB
TypeScript
66 lines
2.1 KiB
TypeScript
import { db, sql, notifications, domains } from "@project-e/db";
|
|
import { eq } from "drizzle-orm";
|
|
|
|
export type NotificationType =
|
|
| "mention"
|
|
| "status_change"
|
|
| "due_soon"
|
|
| "automation"
|
|
| "assignment";
|
|
|
|
export interface CreateNotificationParams {
|
|
userId: string;
|
|
workspaceId?: string | null;
|
|
type: NotificationType | string;
|
|
title: string;
|
|
body?: string | null;
|
|
entityType?: string | null;
|
|
entityId?: string | null;
|
|
}
|
|
|
|
/**
|
|
* Insert an in-app notification and fan it out over the realtime SSE stream so
|
|
* open clients refresh their bell count and list without polling. The event
|
|
* mirrors the pg_notify shape used by recordActivity: `{ type, action, id,
|
|
* workspace_id }` with `type: "notification"`.
|
|
*/
|
|
export async function createNotification(params: CreateNotificationParams): Promise<typeof notifications.$inferSelect> {
|
|
const { userId, workspaceId, type, title, body, entityType, entityId } = params;
|
|
|
|
const [notification] = await db.insert(notifications).values({
|
|
userId,
|
|
workspaceId: workspaceId ?? null,
|
|
type,
|
|
title,
|
|
body: body ?? null,
|
|
entityType: entityType ?? null,
|
|
entityId: entityId ?? null,
|
|
}).returning();
|
|
|
|
if (workspaceId) {
|
|
const payload = JSON.stringify({ type: "notification", action: "created", id: notification.id, workspace_id: workspaceId });
|
|
await sql`SELECT pg_notify('project_e_events', ${payload}::text)`;
|
|
}
|
|
|
|
return notification;
|
|
}
|
|
|
|
/**
|
|
* MVP convenience for the single-user app: resolve the workspace owner and send
|
|
* them the notification. Returns null when the workspace has no owner, so
|
|
* callers can rely on this never throwing for missing ownership.
|
|
*/
|
|
export async function notifyWorkspaceOwner(params: Omit<CreateNotificationParams, "userId">): Promise<typeof notifications.$inferSelect | null> {
|
|
const { workspaceId, ...rest } = params;
|
|
if (!workspaceId) return null;
|
|
|
|
const [domain] = await db
|
|
.select({ ownerId: domains.ownerId })
|
|
.from(domains)
|
|
.where(eq(domains.id, workspaceId))
|
|
.limit(1);
|
|
|
|
if (!domain?.ownerId) return null;
|
|
return createNotification({ ...rest, workspaceId, userId: domain.ownerId });
|
|
}
|