Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions backend/jest.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
module.exports = {
moduleFileExtensions: ['js', 'json', 'ts'],
rootDir: 'src',
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/$1',
},
testRegex: '.*\\.spec\\.ts$',
transform: {
'^.+\\.(t|j)s$': 'ts-jest',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
-- CreateTable
CREATE TABLE "board_templates" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT NOT NULL,
"lists" JSONB NOT NULL,
"workspaceId" TEXT,
"creatorId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,

CONSTRAINT "board_templates_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE INDEX "board_templates_workspaceId_idx" ON "board_templates"("workspaceId");

-- CreateIndex
CREATE INDEX "board_templates_creatorId_idx" ON "board_templates"("creatorId");

-- AddForeignKey
ALTER TABLE "board_templates" ADD CONSTRAINT "board_templates_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "board_templates" ADD CONSTRAINT "board_templates_creatorId_fkey" FOREIGN KEY ("creatorId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "board_templates" ADD COLUMN "visibility" "Visibility" NOT NULL DEFAULT 'PRIVATE';
61 changes: 44 additions & 17 deletions backend/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ model Workspace {
boards Board[]
memberships WorkspaceMember[]
invitations WorkspaceInvitation[]
templates BoardTemplate[]

@@map("workspaces")
}
Expand All @@ -132,18 +133,19 @@ model User {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

createdBoards Board[] @relation("BoardCreator")
boardMembers BoardMember[]
cardAssignees CardAssignee[]
comments Comment[]
attachments Attachment[]
workspaceMemberships WorkspaceMember[]
notifications Notification[]
oauthAccounts OAuthAccount[]
sentInvitations WorkspaceInvitation[] @relation("InvitationInviter")
receivedInvitations WorkspaceInvitation[] @relation("InvitationInvitee")
activities Activity[]
createdBoards Board[] @relation("BoardCreator")
boardMembers BoardMember[]
cardAssignees CardAssignee[]
comments Comment[]
attachments Attachment[]
workspaceMemberships WorkspaceMember[]
notifications Notification[]
oauthAccounts OAuthAccount[]
sentInvitations WorkspaceInvitation[] @relation("InvitationInviter")
receivedInvitations WorkspaceInvitation[] @relation("InvitationInvitee")
activities Activity[]
notificationPreferences UserNotificationPreferences?
createdTemplates BoardTemplate[]

@@map("users")
}
Expand All @@ -154,12 +156,12 @@ model User {
// Per-user notification settings (email frequency, desktop notifications).

model UserNotificationPreferences {
id String @id @default(uuid())
userId String @unique
emailFrequency NotificationEmailFrequency @default(PERIODICALLY)
allowDesktopNotifications Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
id String @id @default(uuid())
userId String @unique
emailFrequency NotificationEmailFrequency @default(PERIODICALLY)
allowDesktopNotifications Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

user User @relation(fields: [userId], references: [id], onDelete: Cascade)

Expand Down Expand Up @@ -526,3 +528,28 @@ model WorkspaceInvitation {
@@index([status])
@@map("workspace_invitations")
}

// ----------------------------------------------------------------------------
// BoardTemplate (CRUD - custom templates)
// ----------------------------------------------------------------------------
// User-created board templates. Lists stored as JSON: [{ title, position, sampleCards?: [{ title, position }] }].
// workspaceId null = global template; otherwise template is scoped to that workspace.

model BoardTemplate {
id String @id @default(uuid())
name String
description String
lists Json
visibility Visibility @default(PRIVATE)
workspaceId String?
creatorId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

workspace Workspace? @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
creator User @relation(fields: [creatorId], references: [id], onDelete: Cascade)

@@index([workspaceId])
@@index([creatorId])
@@map("board_templates")
}
2 changes: 2 additions & 0 deletions backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { AttachmentsModule } from './modules/attachments/attachments.module';
import { UploadModule } from './modules/upload/upload.module';
import { ActivityModule } from './modules/activity/activity.module';
import { NotificationsModule } from './modules/notifications/notifications.module';
import { TemplatesModule } from './modules/templates/templates.module';
import { SubscriptionsModule } from './common/subscriptions/subscriptions.module';
import { validateWsConnection } from './common/subscriptions/ws-auth';
import { GqlAuthGuard } from './common/guards/gql-auth.guard';
Expand Down Expand Up @@ -110,6 +111,7 @@ import { HealthController } from './common/controllers/health.controller';
UploadModule,
ActivityModule,
NotificationsModule,
TemplatesModule,
],
controllers: [HealthController],
providers: [
Expand Down
109 changes: 109 additions & 0 deletions backend/src/graphql/schema.gql
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,15 @@ type BoardMemberWithUser {
userId: ID!
}

type BoardTemplate {
description: String!
id: String!

"""List titles in order"""
listTitles: [String!]!
name: String!
}

type Card {
assignees: [MemberUser!]
background: String
Expand Down Expand Up @@ -173,6 +182,19 @@ type CommentDeletedEvent {
commentId: ID!
}

input CopyBoardInput {
background: String
description: String

"""ID of the board to copy (lists, cards, labels, checklists)."""
sourceBoardId: ID!

"""Title for the new board."""
title: String!
visibility: Visibility
workspaceId: ID
}

input CreateAttachmentInput {
cardId: ID!
filename: String!
Expand All @@ -183,6 +205,9 @@ input CreateAttachmentInput {
input CreateBoardInput {
background: String
description: String

"""Predefined template: blank, kanban, sprint, project"""
templateId: String
title: String!
visibility: Visibility
workspaceId: ID
Expand Down Expand Up @@ -227,6 +252,14 @@ input CreateListInput {
title: String!
}

input CreateTemplateInput {
description: String!
lists: [TemplateListInput!]!
name: String!
visibility: Visibility
workspaceId: ID
}

input CreateUserInput {
avatar: String
description: String
Expand Down Expand Up @@ -356,6 +389,11 @@ type Mutation {
"""
cancelInvitation(invitationId: ID!): Boolean!

"""
Copy a board (lists, cards, labels, checklists). New board has current user as ADMIN.
"""
copyBoard(input: CopyBoardInput!): Board!

"""Create an attachment on a card."""
createAttachment(input: CreateAttachmentInput!): Attachment!

Expand Down Expand Up @@ -383,6 +421,16 @@ type Mutation {
"""
createList(input: CreateListInput!): List!

"""
Create a custom board template. Optional workspaceId to scope to a workspace.
"""
createTemplate(input: CreateTemplateInput!): Template!

"""
Create a template from an existing board (lists and cards become template structure).
"""
createTemplateFromBoard(boardId: ID!, name: String): Template!

"""Create a new user (requires authentication)"""
createUser(input: CreateUserInput!): User!

Expand Down Expand Up @@ -413,6 +461,9 @@ type Mutation {
"""Delete a list. Cards are automatically deleted via cascade."""
deleteList(id: ID!): Boolean!

"""Delete a template. Only creator or workspace admin."""
deleteTemplate(id: ID!): Boolean!

"""Delete a user by ID (requires authentication)"""
deleteUser(id: ID!): Boolean!

Expand Down Expand Up @@ -536,6 +587,9 @@ type Mutation {
"""Update current user notification preferences."""
updateMyNotificationPreferences(input: UpdateNotificationPreferencesInput!): NotificationPreferences!

"""Update a template. Only creator or workspace admin."""
updateTemplate(input: UpdateTemplateInput!): Template!

"""Update an existing user (requires authentication)"""
updateUser(id: ID!, input: UpdateUserInput!): User!

Expand Down Expand Up @@ -647,6 +701,9 @@ type Query {
"""List all labels for a board. User must have access to the board."""
boardLabels(boardId: ID!): [Label!]!

"""List predefined board templates (blank, kanban, sprint, project)."""
boardTemplates: [BoardTemplate!]!

"""Get a card by ID. User must have access to the board."""
card(id: ID!): Card!

Expand Down Expand Up @@ -692,6 +749,16 @@ type Query {
"""Get all workspaces where the current user is a member"""
myWorkspaces: [Workspace!]!

"""
Get a template by ID. User must have access (global or workspace member).
"""
template(id: ID!): Template!

"""
List templates. If workspaceId is provided, returns global + workspace templates; otherwise global only.
"""
templates(workspaceId: ID): [Template!]!

"""Get a user by ID (requires authentication)"""
user(id: ID!): User

Expand Down Expand Up @@ -824,6 +891,40 @@ type Subscription {
workspaceMembersUpdated(workspaceId: ID!): Boolean!
}

type Template {
createdAt: DateTime!
creatorId: ID!
description: String!
id: ID!
lists: [TemplateListType!]!
name: String!
updatedAt: DateTime!
visibility: Visibility!
workspaceId: ID
}

input TemplateListInput {
position: Int!
sampleCards: [TemplateSampleCardInput!]
title: String!
}

type TemplateListType {
position: Int!
sampleCards: [TemplateSampleCard!]
title: String!
}

type TemplateSampleCard {
position: Float!
title: String!
}

input TemplateSampleCardInput {
position: Float!
title: String!
}

input UnassignMemberFromCardInput {
cardId: ID!
userId: ID!
Expand Down Expand Up @@ -903,6 +1004,14 @@ input UpdateNotificationPreferencesInput {
emailFrequency: NotificationEmailFrequency
}

input UpdateTemplateInput {
description: String
id: ID!
lists: [TemplateListInput!]
name: String
visibility: Visibility
}

input UpdateUserInput {
avatar: String
description: String
Expand Down
Loading
Loading