feat: add 5 PM features — custom statuses, Gantt, quick-add, automations, notifications
- 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.
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
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 });
|
||||
}
|
||||
Reference in New Issue
Block a user