feat: Full project management application scaffold
Complete architecture for a ClickUp/Notion/Miro-class project management app:
- Turborepo monorepo with Next.js 15, TypeScript, PostgreSQL (Drizzle ORM)
- Object-centered database schema (everything is an Object: tasks, projects, docs, whiteboards)
- NextAuth v5 authentication with credentials + OAuth providers
- tRPC v11 API layer with full CRUD for objects, properties, relations, templates, search
- Three-panel UI: collapsible sidebar, center content area, push-in right panel
- Purple/teal theme with light/dark mode via Shadcn/ui + Tailwind CSS
- Multiple views: List, Kanban board (dnd-kit), Table (spreadsheet), Embedded iframe
- TipTap rich text editor with slash commands, custom blocks (callout, toggle, mention, embed, divider), AI block
- Real-time collaboration via Yjs + Hocuspocus with presence/cursors
- tldraw whiteboard with custom shape cards (task, document, project)
- MCP server exposing all app data/tools for AI agents
- AI chat panel, editor AI slash commands, Cmd+K command palette
- Template system with built-in templates (Bug Report, Meeting Notes, Sprint)
- Full-text search with result highlighting
- Docker Compose for full-stack deployment (web + collab + postgres + redis)
Made-with: Cursor
2026-03-26 23:39:16 -04:00
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
import * as React from "react";
|
feat: ECHODO app shell, Coolify deploy, Authentik + Umami
Bundles in-flight ECHODO work with the Coolify deployment configuration:
App
- New routes: ai, forms, planner, settings (templates/types), teams,
doc detail, whiteboard detail
- New components: app shell rework (icon-rail, top-header), forms
builder/renderer/responses, types manager, objects creation dialog,
card primitive, form + overview views
- New tRPC routers: favorites, forms, types, workspaces; updates to
health and objects routers
- Markdown backlog sync (packages/database) + cursor-sync schema/migrations
- Schema additions: forms, types, favorites, markdown_backlog, cursor_sync
- Initial Drizzle migrations checked in
Deployment
- docker/docker-compose.coolify.yml: drops bundled Postgres/Redis
(uses CT 102 shared services), removes host port mappings, adds
Coolify SERVICE_FQDN_* magic vars for web + collab
- .env.example rewritten as the full ECHODO/Coolify variable manifest
- NextAuth gains an Authentik OIDC provider (gated on env presence)
- Root layout injects Umami tracking script when configured;
metadata title flipped to ECHODO
Security
- .gitignore expanded to exclude AGENT-DEPLOY.md, .env.*, secrets/,
credentials.*, *.key, *.crt, *.pem, ssh keys
Made-with: Cursor
2026-04-26 15:34:34 -04:00
|
|
|
import { useParams } from "next/navigation";
|
feat: Full project management application scaffold
Complete architecture for a ClickUp/Notion/Miro-class project management app:
- Turborepo monorepo with Next.js 15, TypeScript, PostgreSQL (Drizzle ORM)
- Object-centered database schema (everything is an Object: tasks, projects, docs, whiteboards)
- NextAuth v5 authentication with credentials + OAuth providers
- tRPC v11 API layer with full CRUD for objects, properties, relations, templates, search
- Three-panel UI: collapsible sidebar, center content area, push-in right panel
- Purple/teal theme with light/dark mode via Shadcn/ui + Tailwind CSS
- Multiple views: List, Kanban board (dnd-kit), Table (spreadsheet), Embedded iframe
- TipTap rich text editor with slash commands, custom blocks (callout, toggle, mention, embed, divider), AI block
- Real-time collaboration via Yjs + Hocuspocus with presence/cursors
- tldraw whiteboard with custom shape cards (task, document, project)
- MCP server exposing all app data/tools for AI agents
- AI chat panel, editor AI slash commands, Cmd+K command palette
- Template system with built-in templates (Bug Report, Meeting Notes, Sprint)
- Full-text search with result highlighting
- Docker Compose for full-stack deployment (web + collab + postgres + redis)
Made-with: Cursor
2026-03-26 23:39:16 -04:00
|
|
|
import {
|
|
|
|
|
DndContext,
|
|
|
|
|
DragOverlay,
|
|
|
|
|
PointerSensor,
|
|
|
|
|
closestCorners,
|
|
|
|
|
useSensor,
|
|
|
|
|
useSensors,
|
|
|
|
|
type DragEndEvent,
|
|
|
|
|
type DragOverEvent,
|
|
|
|
|
type DragStartEvent,
|
|
|
|
|
} from "@dnd-kit/core";
|
|
|
|
|
import { arrayMove } from "@dnd-kit/sortable";
|
|
|
|
|
|
|
|
|
|
import type { ViewConfig, ViewObject } from "@/lib/hooks/use-view-data";
|
|
|
|
|
import { useViewData } from "@/lib/hooks/use-view-data";
|
feat: ECHODO app shell, Coolify deploy, Authentik + Umami
Bundles in-flight ECHODO work with the Coolify deployment configuration:
App
- New routes: ai, forms, planner, settings (templates/types), teams,
doc detail, whiteboard detail
- New components: app shell rework (icon-rail, top-header), forms
builder/renderer/responses, types manager, objects creation dialog,
card primitive, form + overview views
- New tRPC routers: favorites, forms, types, workspaces; updates to
health and objects routers
- Markdown backlog sync (packages/database) + cursor-sync schema/migrations
- Schema additions: forms, types, favorites, markdown_backlog, cursor_sync
- Initial Drizzle migrations checked in
Deployment
- docker/docker-compose.coolify.yml: drops bundled Postgres/Redis
(uses CT 102 shared services), removes host port mappings, adds
Coolify SERVICE_FQDN_* magic vars for web + collab
- .env.example rewritten as the full ECHODO/Coolify variable manifest
- NextAuth gains an Authentik OIDC provider (gated on env presence)
- Root layout injects Umami tracking script when configured;
metadata title flipped to ECHODO
Security
- .gitignore expanded to exclude AGENT-DEPLOY.md, .env.*, secrets/,
credentials.*, *.key, *.crt, *.pem, ssh keys
Made-with: Cursor
2026-04-26 15:34:34 -04:00
|
|
|
import { api } from "@/lib/trpc";
|
feat: Full project management application scaffold
Complete architecture for a ClickUp/Notion/Miro-class project management app:
- Turborepo monorepo with Next.js 15, TypeScript, PostgreSQL (Drizzle ORM)
- Object-centered database schema (everything is an Object: tasks, projects, docs, whiteboards)
- NextAuth v5 authentication with credentials + OAuth providers
- tRPC v11 API layer with full CRUD for objects, properties, relations, templates, search
- Three-panel UI: collapsible sidebar, center content area, push-in right panel
- Purple/teal theme with light/dark mode via Shadcn/ui + Tailwind CSS
- Multiple views: List, Kanban board (dnd-kit), Table (spreadsheet), Embedded iframe
- TipTap rich text editor with slash commands, custom blocks (callout, toggle, mention, embed, divider), AI block
- Real-time collaboration via Yjs + Hocuspocus with presence/cursors
- tldraw whiteboard with custom shape cards (task, document, project)
- MCP server exposing all app data/tools for AI agents
- AI chat panel, editor AI slash commands, Cmd+K command palette
- Template system with built-in templates (Bug Report, Meeting Notes, Sprint)
- Full-text search with result highlighting
- Docker Compose for full-stack deployment (web + collab + postgres + redis)
Made-with: Cursor
2026-03-26 23:39:16 -04:00
|
|
|
import { cn } from "@/lib/utils";
|
|
|
|
|
|
|
|
|
|
import { BoardCardPreview } from "./board-card";
|
|
|
|
|
import { BoardColumn } from "./board-column";
|
|
|
|
|
|
|
|
|
|
const STATUS_ORDER = ["open", "in_progress", "done", "closed"] as const;
|
|
|
|
|
|
|
|
|
|
const COLUMN_THEME: Record<
|
|
|
|
|
string,
|
|
|
|
|
{ dot: string; borderTop: string; label: string }
|
|
|
|
|
> = {
|
|
|
|
|
open: {
|
|
|
|
|
dot: "bg-muted-foreground/55",
|
|
|
|
|
borderTop: "border-t-[3px] border-t-muted-foreground/45",
|
|
|
|
|
label: "Open",
|
|
|
|
|
},
|
|
|
|
|
in_progress: {
|
|
|
|
|
dot: "bg-blue-500",
|
|
|
|
|
borderTop: "border-t-[3px] border-t-blue-500",
|
|
|
|
|
label: "In progress",
|
|
|
|
|
},
|
|
|
|
|
done: {
|
|
|
|
|
dot: "bg-green-500",
|
|
|
|
|
borderTop: "border-t-[3px] border-t-green-500",
|
|
|
|
|
label: "Done",
|
|
|
|
|
},
|
|
|
|
|
closed: {
|
|
|
|
|
dot: "bg-slate-500",
|
|
|
|
|
borderTop: "border-t-[3px] border-t-slate-500",
|
|
|
|
|
label: "Closed",
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function getColumnTheme(columnId: string) {
|
|
|
|
|
return (
|
|
|
|
|
COLUMN_THEME[columnId] ?? {
|
|
|
|
|
dot: "bg-muted-foreground/50",
|
|
|
|
|
borderTop: "border-t-[3px] border-t-muted-foreground/35",
|
|
|
|
|
label: columnId.replace(/_/g, " "),
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getColumnKeys(
|
|
|
|
|
groupBy: string | null,
|
|
|
|
|
grouped: Record<string, ViewObject[]>,
|
|
|
|
|
): string[] {
|
|
|
|
|
if (groupBy === "status" || groupBy === null) {
|
|
|
|
|
return [...STATUS_ORDER];
|
|
|
|
|
}
|
|
|
|
|
return Object.keys(grouped).sort();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildColumnsFromGrouped(
|
|
|
|
|
columnKeys: string[],
|
|
|
|
|
grouped: Record<string, ViewObject[]>,
|
|
|
|
|
): Record<string, ViewObject[]> {
|
|
|
|
|
const out: Record<string, ViewObject[]> = {};
|
|
|
|
|
for (const k of columnKeys) {
|
|
|
|
|
out[k] = grouped[k] ? [...grouped[k]] : [];
|
|
|
|
|
}
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function findContainer(
|
|
|
|
|
id: string,
|
|
|
|
|
cols: Record<string, ViewObject[]>,
|
|
|
|
|
): string | undefined {
|
|
|
|
|
if (id in cols) return id;
|
|
|
|
|
for (const key of Object.keys(cols)) {
|
|
|
|
|
if (cols[key].some((o) => o.id === id)) return key;
|
|
|
|
|
}
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function patchObjectForColumn(
|
|
|
|
|
object: ViewObject,
|
|
|
|
|
columnId: string,
|
|
|
|
|
groupField: string,
|
|
|
|
|
): ViewObject {
|
|
|
|
|
const field = groupField === "status" ? "status" : groupField;
|
|
|
|
|
if (field === "status") {
|
|
|
|
|
return { ...object, status: columnId };
|
|
|
|
|
}
|
|
|
|
|
return { ...object, [field]: columnId } as ViewObject;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface BoardViewProps {
|
|
|
|
|
config: ViewConfig;
|
|
|
|
|
className?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function BoardView({ config, className }: BoardViewProps) {
|
feat: ECHODO app shell, Coolify deploy, Authentik + Umami
Bundles in-flight ECHODO work with the Coolify deployment configuration:
App
- New routes: ai, forms, planner, settings (templates/types), teams,
doc detail, whiteboard detail
- New components: app shell rework (icon-rail, top-header), forms
builder/renderer/responses, types manager, objects creation dialog,
card primitive, form + overview views
- New tRPC routers: favorites, forms, types, workspaces; updates to
health and objects routers
- Markdown backlog sync (packages/database) + cursor-sync schema/migrations
- Schema additions: forms, types, favorites, markdown_backlog, cursor_sync
- Initial Drizzle migrations checked in
Deployment
- docker/docker-compose.coolify.yml: drops bundled Postgres/Redis
(uses CT 102 shared services), removes host port mappings, adds
Coolify SERVICE_FQDN_* magic vars for web + collab
- .env.example rewritten as the full ECHODO/Coolify variable manifest
- NextAuth gains an Authentik OIDC provider (gated on env presence)
- Root layout injects Umami tracking script when configured;
metadata title flipped to ECHODO
Security
- .gitignore expanded to exclude AGENT-DEPLOY.md, .env.*, secrets/,
credentials.*, *.key, *.crt, *.pem, ssh keys
Made-with: Cursor
2026-04-26 15:34:34 -04:00
|
|
|
const params = useParams();
|
multi-tenancy: promote workspaces to top-level table
Block A of the EchoDo plan. Workspaces used to live as `objects(type='workspace')`,
which made it impossible to put a real RLS-friendly tenant boundary on the schema
or to give each workspace a stable URL slug. This commit:
- Adds a top-level `workspaces` table (slug unique, owner FK, plan_tier hook).
- Migrates the 8 anchor tables (objects, workspace_members, object_type_defs,
property_definitions, templates, forms, markdown_backlog_items,
cursor_sync_mappings) to FK into `workspaces.id` instead of `objects.id`,
with a hand-augmented data-copy migration that preserves IDs and slug-collision-
proofs on backfill.
- Introduces a `workspaceProcedure` tRPC middleware + `resolveWorkspace` helper
that take a UUID-or-slug `workspace` handle and expose `ctx.workspace`. All
tenant-scoped routers (objects, types, properties, templates, forms, search,
ai, relations, favorites) now flow through it.
- Updates the web app to pass `workspace` slugs from the URL (or store) instead
of the old `workspaceId`, including a workspace-sync layer that rewrites
/<UUID>/... links to /<slug>/...
- Updates the MCP tools (list_objects, create_object, search_objects) and the
workspace://{handle}/tree resource to accept either a slug or UUID so existing
agents keep working.
- Adds a Create Workspace dialog and a Workspace Settings page (rename + slug
rename with redirect, owner-only archive).
Verified locally against a fresh Postgres: migration applies cleanly, slug
uniqueness holds, tenant data is isolated by workspace_id, slug↔UUID resolution
works in both directions, and ON DELETE CASCADE cleans up child rows in the
correct workspace only.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-07 00:02:55 -04:00
|
|
|
const workspaceHandle =
|
feat: ECHODO app shell, Coolify deploy, Authentik + Umami
Bundles in-flight ECHODO work with the Coolify deployment configuration:
App
- New routes: ai, forms, planner, settings (templates/types), teams,
doc detail, whiteboard detail
- New components: app shell rework (icon-rail, top-header), forms
builder/renderer/responses, types manager, objects creation dialog,
card primitive, form + overview views
- New tRPC routers: favorites, forms, types, workspaces; updates to
health and objects routers
- Markdown backlog sync (packages/database) + cursor-sync schema/migrations
- Schema additions: forms, types, favorites, markdown_backlog, cursor_sync
- Initial Drizzle migrations checked in
Deployment
- docker/docker-compose.coolify.yml: drops bundled Postgres/Redis
(uses CT 102 shared services), removes host port mappings, adds
Coolify SERVICE_FQDN_* magic vars for web + collab
- .env.example rewritten as the full ECHODO/Coolify variable manifest
- NextAuth gains an Authentik OIDC provider (gated on env presence)
- Root layout injects Umami tracking script when configured;
metadata title flipped to ECHODO
Security
- .gitignore expanded to exclude AGENT-DEPLOY.md, .env.*, secrets/,
credentials.*, *.key, *.crt, *.pem, ssh keys
Made-with: Cursor
2026-04-26 15:34:34 -04:00
|
|
|
typeof params?.workspaceSlug === "string" ? params.workspaceSlug : undefined;
|
|
|
|
|
const parentId =
|
|
|
|
|
typeof params?.projectId === "string" ? params.projectId : undefined;
|
|
|
|
|
|
feat: Full project management application scaffold
Complete architecture for a ClickUp/Notion/Miro-class project management app:
- Turborepo monorepo with Next.js 15, TypeScript, PostgreSQL (Drizzle ORM)
- Object-centered database schema (everything is an Object: tasks, projects, docs, whiteboards)
- NextAuth v5 authentication with credentials + OAuth providers
- tRPC v11 API layer with full CRUD for objects, properties, relations, templates, search
- Three-panel UI: collapsible sidebar, center content area, push-in right panel
- Purple/teal theme with light/dark mode via Shadcn/ui + Tailwind CSS
- Multiple views: List, Kanban board (dnd-kit), Table (spreadsheet), Embedded iframe
- TipTap rich text editor with slash commands, custom blocks (callout, toggle, mention, embed, divider), AI block
- Real-time collaboration via Yjs + Hocuspocus with presence/cursors
- tldraw whiteboard with custom shape cards (task, document, project)
- MCP server exposing all app data/tools for AI agents
- AI chat panel, editor AI slash commands, Cmd+K command palette
- Template system with built-in templates (Bug Report, Meeting Notes, Sprint)
- Full-text search with result highlighting
- Docker Compose for full-stack deployment (web + collab + postgres + redis)
Made-with: Cursor
2026-03-26 23:39:16 -04:00
|
|
|
const effectiveConfig = React.useMemo(
|
|
|
|
|
() => ({
|
|
|
|
|
...config,
|
|
|
|
|
groupBy: config.groupBy ?? "status",
|
|
|
|
|
}),
|
|
|
|
|
[config],
|
|
|
|
|
);
|
|
|
|
|
|
feat: ECHODO app shell, Coolify deploy, Authentik + Umami
Bundles in-flight ECHODO work with the Coolify deployment configuration:
App
- New routes: ai, forms, planner, settings (templates/types), teams,
doc detail, whiteboard detail
- New components: app shell rework (icon-rail, top-header), forms
builder/renderer/responses, types manager, objects creation dialog,
card primitive, form + overview views
- New tRPC routers: favorites, forms, types, workspaces; updates to
health and objects routers
- Markdown backlog sync (packages/database) + cursor-sync schema/migrations
- Schema additions: forms, types, favorites, markdown_backlog, cursor_sync
- Initial Drizzle migrations checked in
Deployment
- docker/docker-compose.coolify.yml: drops bundled Postgres/Redis
(uses CT 102 shared services), removes host port mappings, adds
Coolify SERVICE_FQDN_* magic vars for web + collab
- .env.example rewritten as the full ECHODO/Coolify variable manifest
- NextAuth gains an Authentik OIDC provider (gated on env presence)
- Root layout injects Umami tracking script when configured;
metadata title flipped to ECHODO
Security
- .gitignore expanded to exclude AGENT-DEPLOY.md, .env.*, secrets/,
credentials.*, *.key, *.crt, *.pem, ssh keys
Made-with: Cursor
2026-04-26 15:34:34 -04:00
|
|
|
const { grouped, isLoading, total } = useViewData(
|
|
|
|
|
effectiveConfig,
|
multi-tenancy: promote workspaces to top-level table
Block A of the EchoDo plan. Workspaces used to live as `objects(type='workspace')`,
which made it impossible to put a real RLS-friendly tenant boundary on the schema
or to give each workspace a stable URL slug. This commit:
- Adds a top-level `workspaces` table (slug unique, owner FK, plan_tier hook).
- Migrates the 8 anchor tables (objects, workspace_members, object_type_defs,
property_definitions, templates, forms, markdown_backlog_items,
cursor_sync_mappings) to FK into `workspaces.id` instead of `objects.id`,
with a hand-augmented data-copy migration that preserves IDs and slug-collision-
proofs on backfill.
- Introduces a `workspaceProcedure` tRPC middleware + `resolveWorkspace` helper
that take a UUID-or-slug `workspace` handle and expose `ctx.workspace`. All
tenant-scoped routers (objects, types, properties, templates, forms, search,
ai, relations, favorites) now flow through it.
- Updates the web app to pass `workspace` slugs from the URL (or store) instead
of the old `workspaceId`, including a workspace-sync layer that rewrites
/<UUID>/... links to /<slug>/...
- Updates the MCP tools (list_objects, create_object, search_objects) and the
workspace://{handle}/tree resource to accept either a slug or UUID so existing
agents keep working.
- Adds a Create Workspace dialog and a Workspace Settings page (rename + slug
rename with redirect, owner-only archive).
Verified locally against a fresh Postgres: migration applies cleanly, slug
uniqueness holds, tenant data is isolated by workspace_id, slug↔UUID resolution
works in both directions, and ON DELETE CASCADE cleans up child rows in the
correct workspace only.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-07 00:02:55 -04:00
|
|
|
workspaceHandle,
|
feat: ECHODO app shell, Coolify deploy, Authentik + Umami
Bundles in-flight ECHODO work with the Coolify deployment configuration:
App
- New routes: ai, forms, planner, settings (templates/types), teams,
doc detail, whiteboard detail
- New components: app shell rework (icon-rail, top-header), forms
builder/renderer/responses, types manager, objects creation dialog,
card primitive, form + overview views
- New tRPC routers: favorites, forms, types, workspaces; updates to
health and objects routers
- Markdown backlog sync (packages/database) + cursor-sync schema/migrations
- Schema additions: forms, types, favorites, markdown_backlog, cursor_sync
- Initial Drizzle migrations checked in
Deployment
- docker/docker-compose.coolify.yml: drops bundled Postgres/Redis
(uses CT 102 shared services), removes host port mappings, adds
Coolify SERVICE_FQDN_* magic vars for web + collab
- .env.example rewritten as the full ECHODO/Coolify variable manifest
- NextAuth gains an Authentik OIDC provider (gated on env presence)
- Root layout injects Umami tracking script when configured;
metadata title flipped to ECHODO
Security
- .gitignore expanded to exclude AGENT-DEPLOY.md, .env.*, secrets/,
credentials.*, *.key, *.crt, *.pem, ssh keys
Made-with: Cursor
2026-04-26 15:34:34 -04:00
|
|
|
parentId,
|
|
|
|
|
);
|
feat: Full project management application scaffold
Complete architecture for a ClickUp/Notion/Miro-class project management app:
- Turborepo monorepo with Next.js 15, TypeScript, PostgreSQL (Drizzle ORM)
- Object-centered database schema (everything is an Object: tasks, projects, docs, whiteboards)
- NextAuth v5 authentication with credentials + OAuth providers
- tRPC v11 API layer with full CRUD for objects, properties, relations, templates, search
- Three-panel UI: collapsible sidebar, center content area, push-in right panel
- Purple/teal theme with light/dark mode via Shadcn/ui + Tailwind CSS
- Multiple views: List, Kanban board (dnd-kit), Table (spreadsheet), Embedded iframe
- TipTap rich text editor with slash commands, custom blocks (callout, toggle, mention, embed, divider), AI block
- Real-time collaboration via Yjs + Hocuspocus with presence/cursors
- tldraw whiteboard with custom shape cards (task, document, project)
- MCP server exposing all app data/tools for AI agents
- AI chat panel, editor AI slash commands, Cmd+K command palette
- Template system with built-in templates (Bug Report, Meeting Notes, Sprint)
- Full-text search with result highlighting
- Docker Compose for full-stack deployment (web + collab + postgres + redis)
Made-with: Cursor
2026-03-26 23:39:16 -04:00
|
|
|
const groupField = effectiveConfig.groupBy ?? "status";
|
|
|
|
|
|
|
|
|
|
const columnKeys = React.useMemo(
|
|
|
|
|
() => getColumnKeys(effectiveConfig.groupBy, grouped),
|
|
|
|
|
[effectiveConfig.groupBy, grouped],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const initialColumns = React.useMemo(
|
|
|
|
|
() => buildColumnsFromGrouped(columnKeys, grouped),
|
|
|
|
|
[columnKeys, grouped],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const [columns, setColumns] =
|
|
|
|
|
React.useState<Record<string, ViewObject[]>>(initialColumns);
|
|
|
|
|
const [activeId, setActiveId] = React.useState<string | null>(null);
|
|
|
|
|
|
feat: ECHODO app shell, Coolify deploy, Authentik + Umami
Bundles in-flight ECHODO work with the Coolify deployment configuration:
App
- New routes: ai, forms, planner, settings (templates/types), teams,
doc detail, whiteboard detail
- New components: app shell rework (icon-rail, top-header), forms
builder/renderer/responses, types manager, objects creation dialog,
card primitive, form + overview views
- New tRPC routers: favorites, forms, types, workspaces; updates to
health and objects routers
- Markdown backlog sync (packages/database) + cursor-sync schema/migrations
- Schema additions: forms, types, favorites, markdown_backlog, cursor_sync
- Initial Drizzle migrations checked in
Deployment
- docker/docker-compose.coolify.yml: drops bundled Postgres/Redis
(uses CT 102 shared services), removes host port mappings, adds
Coolify SERVICE_FQDN_* magic vars for web + collab
- .env.example rewritten as the full ECHODO/Coolify variable manifest
- NextAuth gains an Authentik OIDC provider (gated on env presence)
- Root layout injects Umami tracking script when configured;
metadata title flipped to ECHODO
Security
- .gitignore expanded to exclude AGENT-DEPLOY.md, .env.*, secrets/,
credentials.*, *.key, *.crt, *.pem, ssh keys
Made-with: Cursor
2026-04-26 15:34:34 -04:00
|
|
|
const [creatingColumnId, setCreatingColumnId] = React.useState<string | null>(null);
|
|
|
|
|
const [newTitle, setNewTitle] = React.useState("");
|
|
|
|
|
const utils = api.useUtils();
|
|
|
|
|
const createObject = api.objects.create.useMutation({
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
utils.objects.list.invalidate();
|
|
|
|
|
setNewTitle("");
|
|
|
|
|
setCreatingColumnId(null);
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
feat: Full project management application scaffold
Complete architecture for a ClickUp/Notion/Miro-class project management app:
- Turborepo monorepo with Next.js 15, TypeScript, PostgreSQL (Drizzle ORM)
- Object-centered database schema (everything is an Object: tasks, projects, docs, whiteboards)
- NextAuth v5 authentication with credentials + OAuth providers
- tRPC v11 API layer with full CRUD for objects, properties, relations, templates, search
- Three-panel UI: collapsible sidebar, center content area, push-in right panel
- Purple/teal theme with light/dark mode via Shadcn/ui + Tailwind CSS
- Multiple views: List, Kanban board (dnd-kit), Table (spreadsheet), Embedded iframe
- TipTap rich text editor with slash commands, custom blocks (callout, toggle, mention, embed, divider), AI block
- Real-time collaboration via Yjs + Hocuspocus with presence/cursors
- tldraw whiteboard with custom shape cards (task, document, project)
- MCP server exposing all app data/tools for AI agents
- AI chat panel, editor AI slash commands, Cmd+K command palette
- Template system with built-in templates (Bug Report, Meeting Notes, Sprint)
- Full-text search with result highlighting
- Docker Compose for full-stack deployment (web + collab + postgres + redis)
Made-with: Cursor
2026-03-26 23:39:16 -04:00
|
|
|
React.useEffect(() => {
|
|
|
|
|
setColumns(initialColumns);
|
|
|
|
|
}, [initialColumns]);
|
|
|
|
|
|
|
|
|
|
const sensors = useSensors(
|
|
|
|
|
useSensor(PointerSensor, {
|
|
|
|
|
activationConstraint: { distance: 8 },
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const activeObject = React.useMemo(() => {
|
|
|
|
|
if (!activeId) return null;
|
|
|
|
|
for (const list of Object.values(columns)) {
|
|
|
|
|
const found = list.find((o) => o.id === activeId);
|
|
|
|
|
if (found) return found;
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}, [activeId, columns]);
|
|
|
|
|
|
|
|
|
|
const handleDragStart = (event: DragStartEvent) => {
|
|
|
|
|
setActiveId(String(event.active.id));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleDragCancel = () => {
|
|
|
|
|
setActiveId(null);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleDragOver = (event: DragOverEvent) => {
|
|
|
|
|
const { active, over } = event;
|
|
|
|
|
if (!over) return;
|
|
|
|
|
|
|
|
|
|
const activeIdStr = String(active.id);
|
|
|
|
|
const overIdStr = String(over.id);
|
|
|
|
|
|
|
|
|
|
if (activeIdStr === overIdStr) return;
|
|
|
|
|
|
|
|
|
|
setColumns((prev) => {
|
|
|
|
|
const activeContainer = findContainer(activeIdStr, prev);
|
|
|
|
|
const overContainer = findContainer(overIdStr, prev);
|
|
|
|
|
|
|
|
|
|
if (!activeContainer || !overContainer) return prev;
|
|
|
|
|
if (activeContainer === overContainer) return prev;
|
|
|
|
|
|
|
|
|
|
const activeItems = [...prev[activeContainer]];
|
|
|
|
|
const overItems = [...prev[overContainer]];
|
|
|
|
|
const activeIndex = activeItems.findIndex((i) => i.id === activeIdStr);
|
|
|
|
|
if (activeIndex === -1) return prev;
|
|
|
|
|
|
|
|
|
|
let newIndex: number;
|
|
|
|
|
if (overIdStr in prev) {
|
|
|
|
|
newIndex = overItems.length;
|
|
|
|
|
} else {
|
|
|
|
|
const overItemIndex = overItems.findIndex((i) => i.id === overIdStr);
|
|
|
|
|
const isBelowOverItem =
|
|
|
|
|
over.rect &&
|
|
|
|
|
active.rect.current.translated &&
|
|
|
|
|
active.rect.current.translated.top > over.rect.top + over.rect.height;
|
|
|
|
|
|
|
|
|
|
const modifier = isBelowOverItem ? 1 : 0;
|
|
|
|
|
newIndex =
|
|
|
|
|
overItemIndex >= 0 ? overItemIndex + modifier : overItems.length;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const [removed] = activeItems.splice(activeIndex, 1);
|
|
|
|
|
const patched = patchObjectForColumn(removed, overContainer, groupField);
|
|
|
|
|
const nextOver = [...overItems];
|
|
|
|
|
nextOver.splice(newIndex, 0, patched);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...prev,
|
|
|
|
|
[activeContainer]: activeItems,
|
|
|
|
|
[overContainer]: nextOver,
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleDragEnd = (event: DragEndEvent) => {
|
|
|
|
|
const { active, over } = event;
|
|
|
|
|
setActiveId(null);
|
|
|
|
|
|
|
|
|
|
if (!over) return;
|
|
|
|
|
|
|
|
|
|
const activeIdStr = String(active.id);
|
|
|
|
|
const overIdStr = String(over.id);
|
|
|
|
|
|
|
|
|
|
setColumns((prev) => {
|
|
|
|
|
const activeContainer = findContainer(activeIdStr, prev);
|
|
|
|
|
const overContainer = findContainer(overIdStr, prev);
|
|
|
|
|
|
|
|
|
|
if (!activeContainer || !overContainer) return prev;
|
|
|
|
|
|
|
|
|
|
if (activeContainer !== overContainer) {
|
|
|
|
|
const activeItems = [...prev[activeContainer]];
|
|
|
|
|
const overItems = [...prev[overContainer]];
|
|
|
|
|
const activeIndex = activeItems.findIndex((i) => i.id === activeIdStr);
|
|
|
|
|
if (activeIndex === -1) return prev;
|
|
|
|
|
|
|
|
|
|
const [removed] = activeItems.splice(activeIndex, 1);
|
|
|
|
|
const patched = patchObjectForColumn(removed, overContainer, groupField);
|
|
|
|
|
let newIndex = overItems.length;
|
|
|
|
|
if (!(overIdStr in prev)) {
|
|
|
|
|
const overItemIndex = overItems.findIndex((i) => i.id === overIdStr);
|
|
|
|
|
if (overItemIndex >= 0) newIndex = overItemIndex;
|
|
|
|
|
}
|
|
|
|
|
const nextOver = [...overItems];
|
|
|
|
|
nextOver.splice(newIndex, 0, patched);
|
|
|
|
|
return {
|
|
|
|
|
...prev,
|
|
|
|
|
[activeContainer]: activeItems,
|
|
|
|
|
[overContainer]: nextOver,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const list = [...prev[activeContainer]];
|
|
|
|
|
const oldIndex = list.findIndex((i) => i.id === activeIdStr);
|
|
|
|
|
|
|
|
|
|
if (oldIndex === -1) return prev;
|
|
|
|
|
|
|
|
|
|
if (overIdStr in prev) {
|
|
|
|
|
return prev;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const newIndex = list.findIndex((i) => i.id === overIdStr);
|
|
|
|
|
if (newIndex === -1 || oldIndex === newIndex) return prev;
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...prev,
|
|
|
|
|
[activeContainer]: arrayMove(list, oldIndex, newIndex),
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (isLoading) {
|
|
|
|
|
return (
|
|
|
|
|
<div className={cn("flex flex-1 items-center justify-center p-8", className)}>
|
|
|
|
|
<p className="text-sm text-muted-foreground">Loading board…</p>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className={cn("flex min-h-0 flex-1 flex-col gap-3", className)}>
|
|
|
|
|
<div className="shrink-0 px-1 text-xs text-muted-foreground">
|
|
|
|
|
{total} task{total === 1 ? "" : "s"}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<DndContext
|
|
|
|
|
sensors={sensors}
|
|
|
|
|
collisionDetection={closestCorners}
|
|
|
|
|
onDragStart={handleDragStart}
|
|
|
|
|
onDragOver={handleDragOver}
|
|
|
|
|
onDragEnd={handleDragEnd}
|
|
|
|
|
onDragCancel={handleDragCancel}
|
|
|
|
|
>
|
|
|
|
|
<div className="min-h-0 flex-1 overflow-x-auto overflow-y-hidden pb-2">
|
|
|
|
|
<div className="flex h-full min-h-[min(420px,70vh)] gap-3 px-1 pb-1">
|
|
|
|
|
{columnKeys.map((columnId) => {
|
|
|
|
|
const theme = getColumnTheme(columnId);
|
|
|
|
|
return (
|
|
|
|
|
<BoardColumn
|
|
|
|
|
key={columnId}
|
|
|
|
|
columnId={columnId}
|
|
|
|
|
label={theme.label}
|
|
|
|
|
items={columns[columnId] ?? []}
|
|
|
|
|
dotClass={theme.dot}
|
|
|
|
|
borderTopClass={theme.borderTop}
|
feat: ECHODO app shell, Coolify deploy, Authentik + Umami
Bundles in-flight ECHODO work with the Coolify deployment configuration:
App
- New routes: ai, forms, planner, settings (templates/types), teams,
doc detail, whiteboard detail
- New components: app shell rework (icon-rail, top-header), forms
builder/renderer/responses, types manager, objects creation dialog,
card primitive, form + overview views
- New tRPC routers: favorites, forms, types, workspaces; updates to
health and objects routers
- Markdown backlog sync (packages/database) + cursor-sync schema/migrations
- Schema additions: forms, types, favorites, markdown_backlog, cursor_sync
- Initial Drizzle migrations checked in
Deployment
- docker/docker-compose.coolify.yml: drops bundled Postgres/Redis
(uses CT 102 shared services), removes host port mappings, adds
Coolify SERVICE_FQDN_* magic vars for web + collab
- .env.example rewritten as the full ECHODO/Coolify variable manifest
- NextAuth gains an Authentik OIDC provider (gated on env presence)
- Root layout injects Umami tracking script when configured;
metadata title flipped to ECHODO
Security
- .gitignore expanded to exclude AGENT-DEPLOY.md, .env.*, secrets/,
credentials.*, *.key, *.crt, *.pem, ssh keys
Made-with: Cursor
2026-04-26 15:34:34 -04:00
|
|
|
inlineCreate={{
|
|
|
|
|
isCreating: creatingColumnId === columnId,
|
|
|
|
|
newTitle,
|
|
|
|
|
onNewTitleChange: setNewTitle,
|
|
|
|
|
onOpen: () => {
|
|
|
|
|
setCreatingColumnId(columnId);
|
|
|
|
|
setNewTitle("");
|
|
|
|
|
},
|
|
|
|
|
onSubmit: () => {
|
|
|
|
|
const t = newTitle.trim();
|
multi-tenancy: promote workspaces to top-level table
Block A of the EchoDo plan. Workspaces used to live as `objects(type='workspace')`,
which made it impossible to put a real RLS-friendly tenant boundary on the schema
or to give each workspace a stable URL slug. This commit:
- Adds a top-level `workspaces` table (slug unique, owner FK, plan_tier hook).
- Migrates the 8 anchor tables (objects, workspace_members, object_type_defs,
property_definitions, templates, forms, markdown_backlog_items,
cursor_sync_mappings) to FK into `workspaces.id` instead of `objects.id`,
with a hand-augmented data-copy migration that preserves IDs and slug-collision-
proofs on backfill.
- Introduces a `workspaceProcedure` tRPC middleware + `resolveWorkspace` helper
that take a UUID-or-slug `workspace` handle and expose `ctx.workspace`. All
tenant-scoped routers (objects, types, properties, templates, forms, search,
ai, relations, favorites) now flow through it.
- Updates the web app to pass `workspace` slugs from the URL (or store) instead
of the old `workspaceId`, including a workspace-sync layer that rewrites
/<UUID>/... links to /<slug>/...
- Updates the MCP tools (list_objects, create_object, search_objects) and the
workspace://{handle}/tree resource to accept either a slug or UUID so existing
agents keep working.
- Adds a Create Workspace dialog and a Workspace Settings page (rename + slug
rename with redirect, owner-only archive).
Verified locally against a fresh Postgres: migration applies cleanly, slug
uniqueness holds, tenant data is isolated by workspace_id, slug↔UUID resolution
works in both directions, and ON DELETE CASCADE cleans up child rows in the
correct workspace only.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-07 00:02:55 -04:00
|
|
|
if (!t || !workspaceHandle || createObject.isPending) return;
|
feat: ECHODO app shell, Coolify deploy, Authentik + Umami
Bundles in-flight ECHODO work with the Coolify deployment configuration:
App
- New routes: ai, forms, planner, settings (templates/types), teams,
doc detail, whiteboard detail
- New components: app shell rework (icon-rail, top-header), forms
builder/renderer/responses, types manager, objects creation dialog,
card primitive, form + overview views
- New tRPC routers: favorites, forms, types, workspaces; updates to
health and objects routers
- Markdown backlog sync (packages/database) + cursor-sync schema/migrations
- Schema additions: forms, types, favorites, markdown_backlog, cursor_sync
- Initial Drizzle migrations checked in
Deployment
- docker/docker-compose.coolify.yml: drops bundled Postgres/Redis
(uses CT 102 shared services), removes host port mappings, adds
Coolify SERVICE_FQDN_* magic vars for web + collab
- .env.example rewritten as the full ECHODO/Coolify variable manifest
- NextAuth gains an Authentik OIDC provider (gated on env presence)
- Root layout injects Umami tracking script when configured;
metadata title flipped to ECHODO
Security
- .gitignore expanded to exclude AGENT-DEPLOY.md, .env.*, secrets/,
credentials.*, *.key, *.crt, *.pem, ssh keys
Made-with: Cursor
2026-04-26 15:34:34 -04:00
|
|
|
createObject.mutate({
|
|
|
|
|
type: "task",
|
|
|
|
|
title: t,
|
multi-tenancy: promote workspaces to top-level table
Block A of the EchoDo plan. Workspaces used to live as `objects(type='workspace')`,
which made it impossible to put a real RLS-friendly tenant boundary on the schema
or to give each workspace a stable URL slug. This commit:
- Adds a top-level `workspaces` table (slug unique, owner FK, plan_tier hook).
- Migrates the 8 anchor tables (objects, workspace_members, object_type_defs,
property_definitions, templates, forms, markdown_backlog_items,
cursor_sync_mappings) to FK into `workspaces.id` instead of `objects.id`,
with a hand-augmented data-copy migration that preserves IDs and slug-collision-
proofs on backfill.
- Introduces a `workspaceProcedure` tRPC middleware + `resolveWorkspace` helper
that take a UUID-or-slug `workspace` handle and expose `ctx.workspace`. All
tenant-scoped routers (objects, types, properties, templates, forms, search,
ai, relations, favorites) now flow through it.
- Updates the web app to pass `workspace` slugs from the URL (or store) instead
of the old `workspaceId`, including a workspace-sync layer that rewrites
/<UUID>/... links to /<slug>/...
- Updates the MCP tools (list_objects, create_object, search_objects) and the
workspace://{handle}/tree resource to accept either a slug or UUID so existing
agents keep working.
- Adds a Create Workspace dialog and a Workspace Settings page (rename + slug
rename with redirect, owner-only archive).
Verified locally against a fresh Postgres: migration applies cleanly, slug
uniqueness holds, tenant data is isolated by workspace_id, slug↔UUID resolution
works in both directions, and ON DELETE CASCADE cleans up child rows in the
correct workspace only.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-07 00:02:55 -04:00
|
|
|
workspace: workspaceHandle,
|
feat: ECHODO app shell, Coolify deploy, Authentik + Umami
Bundles in-flight ECHODO work with the Coolify deployment configuration:
App
- New routes: ai, forms, planner, settings (templates/types), teams,
doc detail, whiteboard detail
- New components: app shell rework (icon-rail, top-header), forms
builder/renderer/responses, types manager, objects creation dialog,
card primitive, form + overview views
- New tRPC routers: favorites, forms, types, workspaces; updates to
health and objects routers
- Markdown backlog sync (packages/database) + cursor-sync schema/migrations
- Schema additions: forms, types, favorites, markdown_backlog, cursor_sync
- Initial Drizzle migrations checked in
Deployment
- docker/docker-compose.coolify.yml: drops bundled Postgres/Redis
(uses CT 102 shared services), removes host port mappings, adds
Coolify SERVICE_FQDN_* magic vars for web + collab
- .env.example rewritten as the full ECHODO/Coolify variable manifest
- NextAuth gains an Authentik OIDC provider (gated on env presence)
- Root layout injects Umami tracking script when configured;
metadata title flipped to ECHODO
Security
- .gitignore expanded to exclude AGENT-DEPLOY.md, .env.*, secrets/,
credentials.*, *.key, *.crt, *.pem, ssh keys
Made-with: Cursor
2026-04-26 15:34:34 -04:00
|
|
|
parentId: parentId ?? undefined,
|
|
|
|
|
...(groupField === "status" ? { status: columnId } : {}),
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
onCancel: () => {
|
|
|
|
|
setCreatingColumnId(null);
|
|
|
|
|
setNewTitle("");
|
|
|
|
|
},
|
|
|
|
|
isPending: createObject.isPending,
|
|
|
|
|
}}
|
feat: Full project management application scaffold
Complete architecture for a ClickUp/Notion/Miro-class project management app:
- Turborepo monorepo with Next.js 15, TypeScript, PostgreSQL (Drizzle ORM)
- Object-centered database schema (everything is an Object: tasks, projects, docs, whiteboards)
- NextAuth v5 authentication with credentials + OAuth providers
- tRPC v11 API layer with full CRUD for objects, properties, relations, templates, search
- Three-panel UI: collapsible sidebar, center content area, push-in right panel
- Purple/teal theme with light/dark mode via Shadcn/ui + Tailwind CSS
- Multiple views: List, Kanban board (dnd-kit), Table (spreadsheet), Embedded iframe
- TipTap rich text editor with slash commands, custom blocks (callout, toggle, mention, embed, divider), AI block
- Real-time collaboration via Yjs + Hocuspocus with presence/cursors
- tldraw whiteboard with custom shape cards (task, document, project)
- MCP server exposing all app data/tools for AI agents
- AI chat panel, editor AI slash commands, Cmd+K command palette
- Template system with built-in templates (Bug Report, Meeting Notes, Sprint)
- Full-text search with result highlighting
- Docker Compose for full-stack deployment (web + collab + postgres + redis)
Made-with: Cursor
2026-03-26 23:39:16 -04:00
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<DragOverlay dropAnimation={null}>
|
|
|
|
|
{activeObject ? (
|
|
|
|
|
<div className="w-[min(100%,290px)] min-w-[260px] max-w-[300px] cursor-grabbing">
|
|
|
|
|
<BoardCardPreview object={activeObject} />
|
|
|
|
|
</div>
|
|
|
|
|
) : null}
|
|
|
|
|
</DragOverlay>
|
|
|
|
|
</DndContext>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|