From d261308ca56ced4da932fdb588cf034c7acb4991 Mon Sep 17 00:00:00 2001 From: Daniel Prokopowicz Date: Mon, 4 May 2026 17:14:47 +0200 Subject: [PATCH 1/5] Resolves #14 --- README.md | 26 ++++++++++++++++++++++++++ scripts/setup.ps1 | 20 ++++++++++++++++++++ scripts/setup.sh | 22 ++++++++++++++++++++++ 3 files changed, 68 insertions(+) create mode 100644 scripts/setup.ps1 create mode 100644 scripts/setup.sh diff --git a/README.md b/README.md index 821100f..f463daf 100644 --- a/README.md +++ b/README.md @@ -6,3 +6,29 @@ This project is still under restructuring. The web client is currently not functioning properly due to changes in the API server. If you are looking for a working version of the application, please check the [proof-of-concept branch](https://github.com/Pandetthe/Snapflow/tree/proof-of-concept). Alternatively, if you are interested in tracking progress towards the MVP, please refer to this [roadmap](https://github.com/users/Pandetthe/projects/2). + +## Unified Deployment + +This project includes automated setup scripts to spin up the entire environment using Docker Compose. + +### Prerequisites +* Docker and Docker Compose installed and running. +* On Windows, ensure Docker Desktop is active. + +### How to Run +**Windows (PowerShell):** +Open your terminal and run: +```powershell +powershell -ExecutionPolicy Bypass -File .\scripts\setup.ps1 + +**Linux / macOS (Bash):** +Open your terminal and run: + +```Bash +chmod +x scripts/setup.sh +./scripts/setup.sh + +Management +View Logs: docker-compose -f deployment/docker-compose.yml logs -f + +Stop Services: docker-compose -f deployment/docker-compose.yml down \ No newline at end of file diff --git a/scripts/setup.ps1 b/scripts/setup.ps1 new file mode 100644 index 0000000..b471040 --- /dev/null +++ b/scripts/setup.ps1 @@ -0,0 +1,20 @@ +Write-Host "Starting Snapflow deployment..." -ForegroundColor Cyan + +# Navigate to the project root directory +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition +Set-Location -Path "$ScriptDir\.." + +# Check if Docker is running +docker info > $null 2>&1 +if ($LASTEXITCODE -ne 0) { + Write-Host "Error: Docker is not running or not recognized. Please start Docker Desktop." -ForegroundColor Red + Write-Host "Note: If you just installed Docker, you may need to restart VS Code so the terminal can see it." -ForegroundColor Yellow + exit 1 +} + +Write-Host "Building and starting containers (Docker Compose)..." -ForegroundColor Yellow +docker-compose -f deployment/docker-compose.yml up -d --build + +Write-Host "Deployment successful!" -ForegroundColor Green +Write-Host "Web client and API are now running." +Write-Host "To view logs, run: docker-compose -f deployment/docker-compose.yml logs -f" \ No newline at end of file diff --git a/scripts/setup.sh b/scripts/setup.sh new file mode 100644 index 0000000..aee34be --- /dev/null +++ b/scripts/setup.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +# Exit immediately if a command exits with a non-zero status +set -e + +echo "Starting Snapflow deployment..." + +# Navigate to the project root directory (assuming script is in scripts/) +cd "$(dirname "$0")/.." + +# Check if Docker is running +if ! docker info > /dev/null 2>&1; then + echo "Error: Docker is not running or not installed. Please start Docker and try again." + exit 1 +fi + +echo "Building and starting containers..." +docker-compose -f deployment/docker-compose.yml up -d --build + +echo "Deployment successful!" +echo "Web client and API are now running via Nginx reverse proxy." +echo "To view logs, run: docker-compose -f deployment/docker-compose.yml logs -f" \ No newline at end of file From 7eb1e211eeece4f43865c808b913867afee6634d Mon Sep 17 00:00:00 2001 From: danielprokopowicz <146735185+danielprokopowicz@users.noreply.github.com> Date: Mon, 4 May 2026 17:37:22 +0200 Subject: [PATCH 2/5] Update README.md Signed-off-by: danielprokopowicz <146735185+danielprokopowicz@users.noreply.github.com> --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f463daf..1a3205d 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ This project includes automated setup scripts to spin up the entire environment Open your terminal and run: ```powershell powershell -ExecutionPolicy Bypass -File .\scripts\setup.ps1 +``` **Linux / macOS (Bash):** Open your terminal and run: @@ -27,8 +28,9 @@ Open your terminal and run: ```Bash chmod +x scripts/setup.sh ./scripts/setup.sh +``` Management View Logs: docker-compose -f deployment/docker-compose.yml logs -f -Stop Services: docker-compose -f deployment/docker-compose.yml down \ No newline at end of file +Stop Services: docker-compose -f deployment/docker-compose.yml down From 1af7adfb53cd11e44a372e152727fe2395f777c6 Mon Sep 17 00:00:00 2001 From: Jakub Jurczyk Date: Mon, 4 May 2026 20:55:34 +0200 Subject: [PATCH 3/5] fix: unify dialog styling, fix layout shifts --- web/src/app.css | 2 +- .../auth/components/SignInModal.svelte | 2 +- .../features/boards/components/List.svelte | 2 +- .../boards/components/Swimlane.svelte | 2 +- web/src/lib/ui/_utils/form.svelte.ts | 2 +- .../dialogs/ResponsiveAlertDialog.svelte | 209 +++----------- .../dialogs/ResponsiveDialog.svelte | 267 ++--------------- .../dialogs/dialogBehavior.svelte.ts | 271 ++++++++++++++++++ .../lib/ui/components/layout/AppHeader.svelte | 2 +- web/src/lib/ui/stores/error.svelte.ts | 19 ++ web/src/routes/(auth)/boards/+page.svelte | 2 +- .../routes/(auth)/boards/[id]/+page.svelte | 2 +- .../(auth)/boards/[id]/edit/+page.svelte | 2 +- web/src/routes/(auth)/profile/+page.svelte | 4 +- web/src/routes/+layout.svelte | 22 +- 15 files changed, 362 insertions(+), 448 deletions(-) create mode 100644 web/src/lib/ui/components/dialogs/dialogBehavior.svelte.ts create mode 100644 web/src/lib/ui/stores/error.svelte.ts diff --git a/web/src/app.css b/web/src/app.css index 9de82b9..9eb9eee 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -129,7 +129,7 @@ @layer base { html { - scrollbar-gutter: auto; + scrollbar-gutter: stable; } html, body { diff --git a/web/src/lib/features/auth/components/SignInModal.svelte b/web/src/lib/features/auth/components/SignInModal.svelte index 67d7017..f226e52 100644 --- a/web/src/lib/features/auth/components/SignInModal.svelte +++ b/web/src/lib/features/auth/components/SignInModal.svelte @@ -4,7 +4,7 @@ import { cn } from '$lib/ui/utils'; import { AuthService } from '../api/auth'; import { apiClient } from '$lib/core/api.client'; - import { errorStore } from '$lib/ui/stores/error'; + import { errorStore } from '$lib/ui/stores/error.svelte'; import { ShieldAlert, KeyRound, CircleX, MailQuestionMark, UserX } from 'lucide-svelte'; import { fade, scale } from 'svelte/transition'; import { quintOut } from 'svelte/easing'; diff --git a/web/src/lib/features/boards/components/List.svelte b/web/src/lib/features/boards/components/List.svelte index b590fb2..8c52660 100644 --- a/web/src/lib/features/boards/components/List.svelte +++ b/web/src/lib/features/boards/components/List.svelte @@ -6,7 +6,7 @@ import type { BoardsHub } from '$lib/features/boards/hub/boards.hub'; import Card from './Card.svelte'; import { ScrollArea } from 'bits-ui'; - import { errorStore } from '$lib/ui/stores/error'; + import { errorStore } from '$lib/ui/stores/error.svelte'; import { triggerHaptic } from '$lib/ui/utils'; import { Button } from '$lib/ui/components'; import { GripVertical, Pencil, Plus } from 'lucide-svelte'; diff --git a/web/src/lib/features/boards/components/Swimlane.svelte b/web/src/lib/features/boards/components/Swimlane.svelte index aa99add..081dc39 100644 --- a/web/src/lib/features/boards/components/Swimlane.svelte +++ b/web/src/lib/features/boards/components/Swimlane.svelte @@ -6,7 +6,7 @@ import { getContext } from 'svelte'; import { BoardsHub } from '$lib/features/boards/hub/boards.hub'; import type { GetBoardByIdResponse } from '$lib/features/boards/types/boards.api'; - import { errorStore } from '$lib/ui/stores/error'; + import { errorStore } from '$lib/ui/stores/error.svelte'; import { Button } from '$lib/ui/components'; import { ScrollArea } from 'bits-ui'; import { triggerHaptic } from '$lib/ui/utils'; diff --git a/web/src/lib/ui/_utils/form.svelte.ts b/web/src/lib/ui/_utils/form.svelte.ts index 2a135e1..e49a2d9 100644 --- a/web/src/lib/ui/_utils/form.svelte.ts +++ b/web/src/lib/ui/_utils/form.svelte.ts @@ -1,5 +1,5 @@ import { triggerHaptic } from './haptics'; -import { errorStore } from '$lib/ui/stores/error'; +import { errorStore } from '$lib/ui/stores/error.svelte'; import type { Response as ApiResponse, ProblemDetails as ApiProblemDetails diff --git a/web/src/lib/ui/components/dialogs/ResponsiveAlertDialog.svelte b/web/src/lib/ui/components/dialogs/ResponsiveAlertDialog.svelte index ffede26..484197c 100644 --- a/web/src/lib/ui/components/dialogs/ResponsiveAlertDialog.svelte +++ b/web/src/lib/ui/components/dialogs/ResponsiveAlertDialog.svelte @@ -1,204 +1,61 @@ - + + {#if behavior.activeMode === 'drawer' && behavior.activeDrawerSide === 'bottom'} +
behavior.handleHandleTouchEnd(() => handleOpenChange(false))} + onmousedown={(e) => behavior.handleHandleMouseDown(e, () => handleOpenChange(false))} + role="button" + tabindex="0" + aria-label="Drag down to close" + > +
+
+ {/if} {@render children()}
diff --git a/web/src/lib/ui/components/dialogs/ResponsiveDialog.svelte b/web/src/lib/ui/components/dialogs/ResponsiveDialog.svelte index 1d2afe0..14342fa 100644 --- a/web/src/lib/ui/components/dialogs/ResponsiveDialog.svelte +++ b/web/src/lib/ui/components/dialogs/ResponsiveDialog.svelte @@ -1,281 +1,54 @@ - + - {#if activeMode === 'drawer' && activeDrawerSide === 'bottom'} + {#if behavior.activeMode === 'drawer' && behavior.activeDrawerSide === 'bottom'}
behavior.handleHandleTouchEnd(() => handleOpenChange(false))} + onmousedown={(e) => behavior.handleHandleMouseDown(e, () => handleOpenChange(false))} role="button" tabindex="0" aria-label="Drag down to close" diff --git a/web/src/lib/ui/components/dialogs/dialogBehavior.svelte.ts b/web/src/lib/ui/components/dialogs/dialogBehavior.svelte.ts new file mode 100644 index 0000000..4cee2c8 --- /dev/null +++ b/web/src/lib/ui/components/dialogs/dialogBehavior.svelte.ts @@ -0,0 +1,271 @@ +import { cn } from '$lib/ui/utils'; + +export type DialogMode = 'modal' | 'drawer'; +export type DialogPlacement = 'center' | 'trigger'; +export type DialogSize = 'sm' | 'md' | 'lg' | 'xl' | 'full'; +export type DrawerSide = 'top' | 'right' | 'bottom' | 'left'; +export type DialogAnimation = 'fade-zoom' | 'slide-up' | 'slide-down' | 'slide-left' | 'slide-right' | 'none'; + +export const DIALOG_SIZE_CLASS: Record = { + sm: 'max-w-sm', + md: 'max-w-md', + lg: 'max-w-lg', + xl: 'max-w-xl', + full: 'max-w-none' +}; + +export interface DialogBehaviorOptions { + desktopMode?: DialogMode; + mobileMode?: DialogMode; + desktopPlacement?: DialogPlacement; + mobilePlacement?: DialogPlacement; + desktopAnimation?: DialogAnimation; + mobileAnimation?: DialogAnimation; + desktopDrawerSide?: DrawerSide; + mobileDrawerSide?: DrawerSide; + size?: DialogSize; + sizeClass?: string; + overlayClass?: string; + contentClass?: string; + escapeClosable?: boolean; + overlayClosable?: boolean; + breakpoint?: number; + triggerElement?: HTMLElement | null; + triggerOffset?: number; + maxHeight?: string; + zIndex?: string; +} + +function getAnimationClasses(animation: DialogAnimation): string { + switch (animation) { + case 'slide-up': + return 'duration-200 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-bottom-4 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:slide-in-from-bottom-4'; + case 'slide-down': + return 'duration-200 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-4 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:slide-in-from-top-4'; + case 'slide-left': + return 'duration-200 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-right-4 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:slide-in-from-right-4'; + case 'slide-right': + return 'duration-200 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-left-4 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:slide-in-from-left-4'; + case 'none': + return ''; + case 'fade-zoom': + default: + return 'duration-200 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95'; + } +} + +function getDrawerSideClasses(side: DrawerSide): string { + switch (side) { + case 'top': + return 'top-0 left-0 right-0 rounded-b-2xl border-x-0 border-t-0'; + case 'right': + return 'top-0 right-0 h-full max-h-none w-[min(100%,26rem)] rounded-l-2xl border-y-0 border-r-0'; + case 'left': + return 'top-0 left-0 h-full max-h-none w-[min(100%,26rem)] rounded-r-2xl border-y-0 border-l-0'; + case 'bottom': + default: + return 'right-0 bottom-0 left-0 rounded-t-2xl border-x-0 border-b-0'; + } +} + +export class DialogBehavior { + isMobile = $state(false); + triggerTop = $state(0); + triggerLeft = $state(0); + isDragging = $state(false); + dragStartY = $state(0); + dragY = $state(0); + + #isDragDismissing = false; + + readonly desktopMode: DialogMode; + readonly mobileMode: DialogMode; + readonly desktopPlacement: DialogPlacement; + readonly mobilePlacement: DialogPlacement; + readonly desktopAnimation: DialogAnimation; + readonly mobileAnimation: DialogAnimation; + readonly desktopDrawerSide: DrawerSide; + readonly mobileDrawerSide: DrawerSide; + readonly size: DialogSize; + readonly sizeClass: string; + readonly overlayClass: string; + readonly contentClass: string; + readonly escapeClosable: boolean; + readonly overlayClosable: boolean; + readonly breakpoint: number; + readonly triggerElement: HTMLElement | null | undefined; + readonly triggerOffset: number; + readonly maxHeight: string; + readonly zIndex: string; + + constructor(options: DialogBehaviorOptions = {}) { + this.desktopMode = options.desktopMode ?? 'modal'; + this.mobileMode = options.mobileMode ?? 'drawer'; + this.desktopPlacement = options.desktopPlacement ?? 'center'; + this.mobilePlacement = options.mobilePlacement ?? 'center'; + this.desktopAnimation = options.desktopAnimation ?? 'fade-zoom'; + this.mobileAnimation = options.mobileAnimation ?? 'slide-up'; + this.desktopDrawerSide = options.desktopDrawerSide ?? 'right'; + this.mobileDrawerSide = options.mobileDrawerSide ?? 'bottom'; + this.size = options.size ?? 'md'; + this.sizeClass = options.sizeClass ?? ''; + this.overlayClass = options.overlayClass ?? ''; + this.contentClass = options.contentClass ?? ''; + this.escapeClosable = options.escapeClosable ?? true; + this.overlayClosable = options.overlayClosable ?? true; + this.breakpoint = options.breakpoint ?? 768; + this.triggerElement = options.triggerElement; + this.triggerOffset = options.triggerOffset ?? 8; + this.maxHeight = options.maxHeight ?? '88vh'; + this.zIndex = options.zIndex ?? 'z-50'; + } + + get activeMode(): DialogMode { + return this.isMobile ? this.mobileMode : this.desktopMode; + } + + get activePlacement(): DialogPlacement { + return this.isMobile ? this.mobilePlacement : this.desktopPlacement; + } + + get activeAnimation(): DialogAnimation { + return this.isMobile ? this.mobileAnimation : this.desktopAnimation; + } + + get activeDrawerSide(): DrawerSide { + return this.isMobile ? this.mobileDrawerSide : this.desktopDrawerSide; + } + + get useTriggerPosition(): boolean { + return this.activeMode === 'modal' && this.activePlacement === 'trigger'; + } + + get overlayClasses(): string { + return cn( + `fixed inset-0 ${this.zIndex} bg-black/40 backdrop-blur-md`, + 'data-[state=closed]:animate-out data-[state=closed]:fade-out-0', + 'data-[state=open]:animate-in data-[state=open]:fade-in-0', + this.overlayClass + ); + } + + get baseContentClasses(): string { + return cn( + `fixed ${this.zIndex} w-[calc(100%-1.25rem)] overflow-y-auto border border-gray-200 bg-white shadow-2xl dark:border-gray-700 dark:bg-gray-900`, + DIALOG_SIZE_CLASS[this.size], + this.sizeClass, + getAnimationClasses(this.activeAnimation), + this.activeMode === 'modal' + ? this.useTriggerPosition + ? 'translate-x-0 translate-y-0 rounded-2xl p-5 sm:p-6' + : 'top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 rounded-2xl p-5 sm:p-6' + : cn('w-full max-w-none p-6 pb-12 sm:p-8 sm:pb-14', getDrawerSideClasses(this.activeDrawerSide)), + this.contentClass + ); + } + + get positionedStyle(): string { + let style = ''; + if (!this.useTriggerPosition) { + style += `max-height: ${this.maxHeight};`; + } else { + style += `top: ${this.triggerTop}px; left: ${this.triggerLeft}px; max-height: ${this.maxHeight};`; + } + if (this.dragY > 0) { + style += ` transform: translateY(${this.dragY}px) !important; transition: ${this.isDragging ? 'none' : 'transform 0.2s ease-out'} !important;`; + } + return style; + } + + onClose() { + if (!this.#isDragDismissing) { + this.isDragging = false; + this.dragY = 0; + } + } + + updateTriggerPosition() { + if (!this.triggerElement) return; + const rect = this.triggerElement.getBoundingClientRect(); + this.triggerTop = rect.bottom + this.triggerOffset; + this.triggerLeft = rect.left; + } + + handleHandleTouchStart = (e: TouchEvent) => { + this.isDragging = true; + this.dragStartY = e.touches[0].clientY; + }; + + handleHandleTouchMove = (e: TouchEvent) => { + if (!this.isDragging) return; + const deltaY = e.touches[0].clientY - this.dragStartY; + if (deltaY > 0) this.dragY = deltaY; + }; + + handleHandleTouchEnd = (close: () => void) => { + if (!this.isDragging) return; + this.isDragging = false; + if (this.dragY > 100) { + this.#dismissWithDrag(close); + } else { + this.dragY = 0; + } + }; + + handleHandleMouseDown = (e: MouseEvent, close: () => void) => { + this.isDragging = true; + this.dragStartY = e.clientY; + + const onMouseMove = (ev: MouseEvent) => { + if (!this.isDragging) return; + const deltaY = ev.clientY - this.dragStartY; + if (deltaY > 0) this.dragY = deltaY; + }; + + const onMouseUp = () => { + if (!this.isDragging) return; + this.isDragging = false; + window.removeEventListener('mousemove', onMouseMove); + window.removeEventListener('mouseup', onMouseUp); + if (this.dragY > 100) { + this.#dismissWithDrag(close); + } else { + this.dragY = 0; + } + }; + + window.addEventListener('mousemove', onMouseMove); + window.addEventListener('mouseup', onMouseUp); + }; + + #dismissWithDrag(close: () => void) { + this.#isDragDismissing = true; + this.dragY = window.innerHeight; + setTimeout(() => { + close(); + setTimeout(() => { + this.dragY = 0; + this.#isDragDismissing = false; + }, 250); + }, 220); + } + + mount(): () => void { + this.isMobile = window.innerWidth < this.breakpoint; + this.updateTriggerPosition(); + + const onResize = () => { + this.isMobile = window.innerWidth < this.breakpoint; + this.updateTriggerPosition(); + }; + const onScroll = () => this.updateTriggerPosition(); + + window.addEventListener('resize', onResize); + window.addEventListener('scroll', onScroll, true); + + return () => { + window.removeEventListener('resize', onResize); + window.removeEventListener('scroll', onScroll, true); + }; + } +} diff --git a/web/src/lib/ui/components/layout/AppHeader.svelte b/web/src/lib/ui/components/layout/AppHeader.svelte index 4a82dad..177b37c 100644 --- a/web/src/lib/ui/components/layout/AppHeader.svelte +++ b/web/src/lib/ui/components/layout/AppHeader.svelte @@ -3,7 +3,7 @@ import { ThemeToggle, UserMenu, GithubButton, Button } from '$lib/ui/components'; import { AuthService } from '$lib/features/auth/api/auth'; import { apiClient } from '$lib/core/api.client'; - import { errorStore } from '$lib/ui/stores/error'; + import { errorStore } from '$lib/ui/stores/error.svelte'; import type { User } from '$lib/features/users/api/users'; import { Dialog } from 'bits-ui'; diff --git a/web/src/lib/ui/stores/error.svelte.ts b/web/src/lib/ui/stores/error.svelte.ts new file mode 100644 index 0000000..749242c --- /dev/null +++ b/web/src/lib/ui/stores/error.svelte.ts @@ -0,0 +1,19 @@ +import type { AppError } from '$lib/core/types/app'; + +class ErrorState { + errors = $state([]); + + addError(code: string | null, description: string | null) { + this.errors = [...this.errors, { code, description }]; + } + + addErrors(newErrors: AppError[]) { + this.errors = [...this.errors, ...newErrors]; + } + + reset() { + this.errors = []; + } +} + +export const errorStore = new ErrorState(); diff --git a/web/src/routes/(auth)/boards/+page.svelte b/web/src/routes/(auth)/boards/+page.svelte index 32aed00..537af98 100644 --- a/web/src/routes/(auth)/boards/+page.svelte +++ b/web/src/routes/(auth)/boards/+page.svelte @@ -193,7 +193,7 @@ size="lg" startIcon={Plus} haptic="light" - class="fixed right-4 bottom-4 z-40 h-14 w-14 min-w-14 rounded-full p-0 shadow-lg sm:hidden" + class="fixed right-4 bottom-4 z-30 h-14 w-14 min-w-14 rounded-full p-0 shadow-lg sm:hidden" href="/boards/new" aria-label="Create board" > diff --git a/web/src/routes/(auth)/boards/[id]/+page.svelte b/web/src/routes/(auth)/boards/[id]/+page.svelte index 3bc4e52..b6bfe8a 100644 --- a/web/src/routes/(auth)/boards/[id]/+page.svelte +++ b/web/src/routes/(auth)/boards/[id]/+page.svelte @@ -7,7 +7,7 @@ import CardModal from '$lib/features/boards/components/CardModal.svelte'; import { onDestroy, onMount, setContext } from 'svelte'; import { BoardsHub } from '$lib/features/boards/hub/boards.hub'; - import { errorStore } from '$lib/ui/stores/error'; + import { errorStore } from '$lib/ui/stores/error.svelte'; import { recentBoards } from '$lib/features/boards/stores/recent'; import type { GetBoardByIdResponse } from '$lib/features/boards/types/boards.api'; import { Button, FullBleedLayout, GoBackButton, LoadingDots } from '$lib/ui/components'; diff --git a/web/src/routes/(auth)/boards/[id]/edit/+page.svelte b/web/src/routes/(auth)/boards/[id]/edit/+page.svelte index bff7706..6cf80f4 100644 --- a/web/src/routes/(auth)/boards/[id]/edit/+page.svelte +++ b/web/src/routes/(auth)/boards/[id]/edit/+page.svelte @@ -2,7 +2,7 @@ import { BoardsService } from '$lib/features/boards/api/boards.api'; import { UsersService, type SearchUserDto } from '$lib/features/users/api/users'; import { apiClient } from '$lib/core/api.client'; - import { errorStore } from '$lib/ui/stores/error'; + import { errorStore } from '$lib/ui/stores/error.svelte'; import { Button, FullLayout, diff --git a/web/src/routes/(auth)/profile/+page.svelte b/web/src/routes/(auth)/profile/+page.svelte index 76819a4..4fcf304 100644 --- a/web/src/routes/(auth)/profile/+page.svelte +++ b/web/src/routes/(auth)/profile/+page.svelte @@ -16,7 +16,7 @@ import PasswordStrength from '$lib/features/auth/components/PasswordStrength.svelte'; import { validateEmail, validateUsername, validatePassword } from '$lib/features/auth/validation'; import { createForm } from '$lib/ui/utils'; - import { errorStore } from '$lib/ui/stores/error'; + import { errorStore } from '$lib/ui/stores/error.svelte'; import { Check, Upload, @@ -206,7 +206,7 @@ -
+
diff --git a/web/src/routes/+layout.svelte b/web/src/routes/+layout.svelte index 5dd0ce1..0bb9076 100644 --- a/web/src/routes/+layout.svelte +++ b/web/src/routes/+layout.svelte @@ -3,14 +3,13 @@ import '../app.css'; import favicon from '$lib/assets/favicon.svg'; import { AppHeader, ErrorModal } from '$lib/ui/components'; - import { errorStore } from '$lib/ui/stores/error'; + import { errorStore } from '$lib/ui/stores/error.svelte'; import type { AppError } from '$lib/core/types/app.js'; import { theme } from '$lib/ui/stores/theme'; - import { onDestroy, onMount } from 'svelte'; + import { onMount } from 'svelte'; import { pwaInfo } from 'virtual:pwa-info'; let { children, data } = $props(); - onDestroy(() => {}); onMount(async () => { if (pwaInfo) { const { registerSW } = await import('virtual:pwa-register'); @@ -41,20 +40,15 @@ let showErrorModal = $state(false); let modalErrors = $state([] as AppError[]); - let unsubscribe: () => void; - onMount(() => { - unsubscribe = errorStore.subscribe((errors) => { - if (errors && errors.length > 0) { - modalErrors = [...modalErrors, ...errors]; - showErrorModal = true; - errorStore.reset(); - } - }); + $effect(() => { + if (errorStore.errors.length > 0) { + modalErrors = [...modalErrors, ...errorStore.errors]; + showErrorModal = true; + errorStore.reset(); + } }); let isSidebarOpen = $state(false); - - onDestroy(() => unsubscribe?.()); From e3a68983a063711bafe134a175705e28656db199 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 5 May 2026 08:06:17 +0000 Subject: [PATCH 4/5] chore(release): 2026.5.2 [skip ci] --- server/Directory.Build.props | 2 +- web/package-lock.json | 4 ++-- web/package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/server/Directory.Build.props b/server/Directory.Build.props index ef20d2e..3e7d23e 100644 --- a/server/Directory.Build.props +++ b/server/Directory.Build.props @@ -2,7 +2,7 @@ net10.0 - 2026.5.1 + 2026.5.2 Snapflow.$(MSBuildProjectName) diff --git a/web/package-lock.json b/web/package-lock.json index 6eb19fc..05e5db2 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "snapflow-web", - "version": "2026.5.1", + "version": "2026.5.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "snapflow-web", - "version": "2026.5.1", + "version": "2026.5.2", "dependencies": { "@azure/monitor-opentelemetry": "^1.15.1", "@microsoft/signalr": "^10.0.0", diff --git a/web/package.json b/web/package.json index 41be894..20b0bc0 100644 --- a/web/package.json +++ b/web/package.json @@ -1,7 +1,7 @@ { "name": "snapflow-web", "private": true, - "version": "2026.5.1", + "version": "2026.5.2", "type": "module", "scripts": { "dev": "vite dev", From a68f116e181ced18316d8d2d6970bb64d9477234 Mon Sep 17 00:00:00 2001 From: Daniel Prokopowicz Date: Wed, 6 May 2026 10:21:44 +0200 Subject: [PATCH 5/5] Zmiany wewnatrz board dodane komentarze zmiana widoku --- server/Directory.Packages.props | 2 + .../Abstractions/Persistence/IAppDbContext.cs | 2 +- .../Boards/Create/CreateBoardCommand.cs | 2 +- .../Boards/Get/GetBoardsResponse.cs | 2 +- .../Boards/GetById/GetBoardByIdHandler.cs | 11 +- .../Boards/GetById/GetBoardByIdResponse.cs | 14 +- .../GetDetails/GetBoardDetailsResponse.cs | 2 +- .../Cards/AppComment/AddCardCommentCommand.cs | 7 + .../Cards/AppComment/AddCardCommentHandler.cs | 46 + .../Cards/AppComment/CardCommentDto.cs | 8 + .../GetByBoardId/GetCardsByBoardIdResponse.cs | 2 +- .../Cards/GetById/GetCardByIdResponse.cs | 2 +- .../GetByListId/GetCardsByListIdResponse.cs | 2 +- .../GetCardsBySwimlaneIdResponse.cs | 2 +- .../Cards/Update/UpdateCardCommand.cs | 2 +- server/src/Domain/Boards/Board.cs | 6 +- server/src/Domain/Boards/BoardEvents.cs | 2 +- server/src/Domain/Cards/Card.cs | 15 +- server/src/Domain/Cards/CardComment.cs | 24 + server/src/Domain/Cards/CardEvents.cs | 4 +- .../src/Infrastructure/Infrastructure.csproj | 4 + .../Persistence/AppDbContext.cs | 12 +- .../Configurations/CardConfiguration.cs | 3 + ...60505113154_AddCardDescription.Designer.cs | 1124 ++++++++++++++++ .../20260505113154_AddCardDescription.cs | 40 + ...20260505141557_AddCardComments.Designer.cs | 1186 +++++++++++++++++ .../20260505141557_AddCardComments.cs | 66 + ...193022_AddCardCommentsRelation.Designer.cs | 1186 +++++++++++++++++ .../20260505193022_AddCardCommentsRelation.cs | 22 + .../Migrations/AppDbContextModelSnapshot.cs | 66 +- .../Presentation/Endpoints/Cards/Update.cs | 2 +- server/src/Presentation/Hubs/Board/Hub.cs | 66 +- .../Hubs/Board/IBoardHubClient.cs | 13 +- server/src/Presentation/Presentation.csproj | 1 + web/src/lib/features/boards/api/boards.api.ts | 9 + .../boards/components/BoardCard.svelte | 37 +- .../features/boards/components/Card.svelte | 93 +- .../boards/components/CardModal.svelte | 77 +- .../features/boards/components/List.svelte | 113 +- .../boards/components/Swimlane.svelte | 183 +-- web/src/lib/features/boards/hub/boards.hub.ts | 13 +- .../lib/features/boards/types/boards.api.ts | 9 +- .../lib/features/boards/types/boards.hub.ts | 2 +- .../routes/(auth)/boards/[id]/+page.svelte | 25 +- web/src/routes/(auth)/profile/+page.svelte | 4 - 45 files changed, 4216 insertions(+), 297 deletions(-) create mode 100644 server/src/Application/Cards/AppComment/AddCardCommentCommand.cs create mode 100644 server/src/Application/Cards/AppComment/AddCardCommentHandler.cs create mode 100644 server/src/Application/Cards/AppComment/CardCommentDto.cs create mode 100644 server/src/Domain/Cards/CardComment.cs create mode 100644 server/src/Infrastructure/Persistence/Migrations/20260505113154_AddCardDescription.Designer.cs create mode 100644 server/src/Infrastructure/Persistence/Migrations/20260505113154_AddCardDescription.cs create mode 100644 server/src/Infrastructure/Persistence/Migrations/20260505141557_AddCardComments.Designer.cs create mode 100644 server/src/Infrastructure/Persistence/Migrations/20260505141557_AddCardComments.cs create mode 100644 server/src/Infrastructure/Persistence/Migrations/20260505193022_AddCardCommentsRelation.Designer.cs create mode 100644 server/src/Infrastructure/Persistence/Migrations/20260505193022_AddCardCommentsRelation.cs diff --git a/server/Directory.Packages.props b/server/Directory.Packages.props index 8199c09..a803184 100644 --- a/server/Directory.Packages.props +++ b/server/Directory.Packages.props @@ -9,10 +9,12 @@ + + diff --git a/server/src/Application/Abstractions/Persistence/IAppDbContext.cs b/server/src/Application/Abstractions/Persistence/IAppDbContext.cs index f5adbb6..cdf5682 100644 --- a/server/src/Application/Abstractions/Persistence/IAppDbContext.cs +++ b/server/src/Application/Abstractions/Persistence/IAppDbContext.cs @@ -28,6 +28,6 @@ public interface IAppDbContext DbSet Tags { get; } DbSet Cards { get; } - + DbSet CardComments { get; } Task SaveChangesAsync(CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/server/src/Application/Boards/Create/CreateBoardCommand.cs b/server/src/Application/Boards/Create/CreateBoardCommand.cs index 6a3a2e4..e84ed38 100644 --- a/server/src/Application/Boards/Create/CreateBoardCommand.cs +++ b/server/src/Application/Boards/Create/CreateBoardCommand.cs @@ -7,5 +7,5 @@ public sealed record CreateBoardMemberRequest(int UserId, MemberRole Role); public sealed record CreateBoardCommand( string Title, - string Description, + string? Description, IReadOnlyList? Members = null) : ICommand; \ No newline at end of file diff --git a/server/src/Application/Boards/Get/GetBoardsResponse.cs b/server/src/Application/Boards/Get/GetBoardsResponse.cs index aaac80c..9a10fc9 100644 --- a/server/src/Application/Boards/Get/GetBoardsResponse.cs +++ b/server/src/Application/Boards/Get/GetBoardsResponse.cs @@ -15,7 +15,7 @@ public sealed record UserDto(int Id, string UserName) public sealed record BoardDto( int Id, string Title, - string Description, + string? Description, MemberRole YourRole, DateTimeOffset CreatedAt, UserDto CreatedBy, diff --git a/server/src/Application/Boards/GetById/GetBoardByIdHandler.cs b/server/src/Application/Boards/GetById/GetBoardByIdHandler.cs index 14a8b0f..f78aab8 100644 --- a/server/src/Application/Boards/GetById/GetBoardByIdHandler.cs +++ b/server/src/Application/Boards/GetById/GetBoardByIdHandler.cs @@ -46,7 +46,16 @@ public async Task> Handle(GetBoardByIdQuery query, c.CreatedAt, UserDto.From(c.CreatedBy), c.UpdatedAt, - UserDto.From(c.UpdatedBy))) + UserDto.From(c.UpdatedBy), + c.Comments + .OrderBy(comm => comm.CreatedAt) + .Select(comm => new CardCommentDto( + comm.Id, + comm.UserId, + comm.User.UserName ?? "Unknown", + comm.Content, + comm.CreatedAt)) + .ToList())) .ToList())) .ToList())) .ToList())) diff --git a/server/src/Application/Boards/GetById/GetBoardByIdResponse.cs b/server/src/Application/Boards/GetById/GetBoardByIdResponse.cs index 0caa6b4..2ef7c35 100644 --- a/server/src/Application/Boards/GetById/GetBoardByIdResponse.cs +++ b/server/src/Application/Boards/GetById/GetBoardByIdResponse.cs @@ -1,4 +1,5 @@ using System.Diagnostics.CodeAnalysis; +using Snapflow.Application.Cards.AddComment; using static Snapflow.Application.Boards.GetById.GetBoardByIdResponse; namespace Snapflow.Application.Boards.GetById; @@ -6,7 +7,7 @@ namespace Snapflow.Application.Boards.GetById; public sealed record GetBoardByIdResponse( int Id, string Title, - string Description, + string? Description, IReadOnlyList Swimlanes) { public sealed record UserDto(int Id, string UserName) @@ -34,10 +35,17 @@ public sealed record ListDto( public sealed record CardDto( int Id, string Title, - string Description, + string? Description, string Rank, DateTimeOffset CreatedAt, UserDto CreatedBy, DateTimeOffset? UpdatedAt, - UserDto? UpdatedBy); + UserDto? UpdatedBy, + List Comments); + public sealed record CardCommentDto( + int Id, + int UserId, + string UserName, + string Content, + DateTimeOffset CreatedAt); } \ No newline at end of file diff --git a/server/src/Application/Boards/GetDetails/GetBoardDetailsResponse.cs b/server/src/Application/Boards/GetDetails/GetBoardDetailsResponse.cs index 0453e20..b56356b 100644 --- a/server/src/Application/Boards/GetDetails/GetBoardDetailsResponse.cs +++ b/server/src/Application/Boards/GetDetails/GetBoardDetailsResponse.cs @@ -5,7 +5,7 @@ namespace Snapflow.Application.Boards.GetDetails; public sealed record GetBoardDetailsResponse( int Id, string Title, - string Description, + string? Description, IReadOnlyList Members); public sealed record GetBoardDetailsMemberResponse( diff --git a/server/src/Application/Cards/AppComment/AddCardCommentCommand.cs b/server/src/Application/Cards/AppComment/AddCardCommentCommand.cs new file mode 100644 index 0000000..09d1d42 --- /dev/null +++ b/server/src/Application/Cards/AppComment/AddCardCommentCommand.cs @@ -0,0 +1,7 @@ +using Snapflow.Application.Abstractions.Messaging; +using Snapflow.Application.Boards.GetById; + +namespace Snapflow.Application.Cards.AddComment; + +public sealed record AddCardCommentCommand(int CardId, int UserId, string Content) + : ICommand; \ No newline at end of file diff --git a/server/src/Application/Cards/AppComment/AddCardCommentHandler.cs b/server/src/Application/Cards/AppComment/AddCardCommentHandler.cs new file mode 100644 index 0000000..f8e270a --- /dev/null +++ b/server/src/Application/Cards/AppComment/AddCardCommentHandler.cs @@ -0,0 +1,46 @@ +using Microsoft.EntityFrameworkCore; +using Snapflow.Application.Abstractions.Messaging; +using Snapflow.Application.Abstractions.Persistence; +using Snapflow.Application.Boards.GetById; +using Snapflow.Common; +using Snapflow.Domain.Cards; + +namespace Snapflow.Application.Cards.AddComment; + +internal sealed class AddCardCommentCommandHandler( + IAppDbContext context) : ICommandHandler // <--- ZMIANA 1: Zwracamy DTO zamiast int +{ + public async Task> Handle(AddCardCommentCommand request, CancellationToken cancellationToken = default) + { + var card = await context.Cards + .FirstOrDefaultAsync(c => c.Id == request.CardId && !c.IsDeleted, cancellationToken); + + if (card is null) + { + return Result.Failure(CardErrors.NotFound(request.CardId)); + } + + var user = await context.Users + .FirstOrDefaultAsync(u => u.Id == request.UserId, cancellationToken); + + if (user is null) + { + return Result.Failure(new Error("User.NotFound", "User not found", ErrorType.NotFound)); + } + + var comment = CardComment.Create(request.CardId, request.UserId, request.Content); + + context.CardComments.Add(comment); + await context.SaveChangesAsync(cancellationToken); + + var dto = new GetBoardByIdResponse.CardCommentDto( + comment.Id, + user.Id, + user.UserName ?? "Unknown", + comment.Content, + comment.CreatedAt + ); + + return dto; + } +} \ No newline at end of file diff --git a/server/src/Application/Cards/AppComment/CardCommentDto.cs b/server/src/Application/Cards/AppComment/CardCommentDto.cs new file mode 100644 index 0000000..da0eec8 --- /dev/null +++ b/server/src/Application/Cards/AppComment/CardCommentDto.cs @@ -0,0 +1,8 @@ +namespace Snapflow.Application.Cards.AddComment; + +public sealed record CardCommentDto( + int Id, + int UserId, + string UserName, + string Content, + DateTimeOffset CreatedAt); \ No newline at end of file diff --git a/server/src/Application/Cards/GetByBoardId/GetCardsByBoardIdResponse.cs b/server/src/Application/Cards/GetByBoardId/GetCardsByBoardIdResponse.cs index a96a3b6..efe9042 100644 --- a/server/src/Application/Cards/GetByBoardId/GetCardsByBoardIdResponse.cs +++ b/server/src/Application/Cards/GetByBoardId/GetCardsByBoardIdResponse.cs @@ -8,6 +8,6 @@ public sealed record CardDto( int SwimlaneId, int BoardId, string Title, - string Description, + string? Description, string Rank); } \ No newline at end of file diff --git a/server/src/Application/Cards/GetById/GetCardByIdResponse.cs b/server/src/Application/Cards/GetById/GetCardByIdResponse.cs index 1d97e80..bbcbba8 100644 --- a/server/src/Application/Cards/GetById/GetCardByIdResponse.cs +++ b/server/src/Application/Cards/GetById/GetCardByIdResponse.cs @@ -9,7 +9,7 @@ public sealed record GetCardByIdResponse( int SwimlaneId, int BoardId, string Title, - string Description, + string? Description, string Rank, DateTimeOffset CreatedAt, UserDto CreatedBy, diff --git a/server/src/Application/Cards/GetByListId/GetCardsByListIdResponse.cs b/server/src/Application/Cards/GetByListId/GetCardsByListIdResponse.cs index b7bd651..8f9658a 100644 --- a/server/src/Application/Cards/GetByListId/GetCardsByListIdResponse.cs +++ b/server/src/Application/Cards/GetByListId/GetCardsByListIdResponse.cs @@ -8,6 +8,6 @@ public sealed record CardDto( int SwimlaneId, int BoardId, string Title, - string Description, + string? Description, string Rank); } \ No newline at end of file diff --git a/server/src/Application/Cards/GetBySwimlaneId/GetCardsBySwimlaneIdResponse.cs b/server/src/Application/Cards/GetBySwimlaneId/GetCardsBySwimlaneIdResponse.cs index 050fd9a..2cac8db 100644 --- a/server/src/Application/Cards/GetBySwimlaneId/GetCardsBySwimlaneIdResponse.cs +++ b/server/src/Application/Cards/GetBySwimlaneId/GetCardsBySwimlaneIdResponse.cs @@ -8,6 +8,6 @@ public sealed record CardDto( int SwimlaneId, int BoardId, string Title, - string Description, + string? Description, string Rank); } \ No newline at end of file diff --git a/server/src/Application/Cards/Update/UpdateCardCommand.cs b/server/src/Application/Cards/Update/UpdateCardCommand.cs index 1c3183c..ab4844b 100644 --- a/server/src/Application/Cards/Update/UpdateCardCommand.cs +++ b/server/src/Application/Cards/Update/UpdateCardCommand.cs @@ -2,4 +2,4 @@ namespace Snapflow.Application.Cards.Update; -public sealed record UpdateCardCommand(int Id, string Title, string Description) : ICommand; \ No newline at end of file +public sealed record UpdateCardCommand(int Id, string Title, string? Description) : ICommand; \ No newline at end of file diff --git a/server/src/Domain/Boards/Board.cs b/server/src/Domain/Boards/Board.cs index 03beda6..8eb3e50 100644 --- a/server/src/Domain/Boards/Board.cs +++ b/server/src/Domain/Boards/Board.cs @@ -13,7 +13,7 @@ public class Board : Entity public Board() { } public string Title { get; private set; } = null!; - public string Description { get; private set; } = ""; + public string? Description { get; private set; } = ""; public DateTimeOffset CreatedAt { get; private set; } public int CreatedById { get; private set; } @@ -34,7 +34,7 @@ public Board() { } public virtual ICollection Cards { get; private set; } = []; public virtual ICollection Tags { get; private set; } = []; - public static Board Create(string title, string description, int createdById, DateTimeOffset createdAt, string? connectionId = null) + public static Board Create(string title, string? description, int createdById, DateTimeOffset createdAt, string? connectionId = null) { var board = new Board { @@ -51,7 +51,7 @@ public static Board Create(string title, string description, int createdById, Da return board; } - public void Update(string title, string description, int updatedById, DateTimeOffset updatedAt, string? connectionId = null) + public void Update(string title, string? description, int updatedById, DateTimeOffset updatedAt, string? connectionId = null) { Title = title; Description = description; diff --git a/server/src/Domain/Boards/BoardEvents.cs b/server/src/Domain/Boards/BoardEvents.cs index 9cb8d5e..a47ee34 100644 --- a/server/src/Domain/Boards/BoardEvents.cs +++ b/server/src/Domain/Boards/BoardEvents.cs @@ -11,7 +11,7 @@ public sealed record BoardCreatedDomainEvent( public sealed record BoardUpdatedDomainEvent( int Id, string Title, - string Description, + string? Description, string? ConnectionId) : IDomainEvent; public sealed record BoardDeletedDomainEvent( diff --git a/server/src/Domain/Cards/Card.cs b/server/src/Domain/Cards/Card.cs index 3cbfdb5..9ed3cec 100644 --- a/server/src/Domain/Cards/Card.cs +++ b/server/src/Domain/Cards/Card.cs @@ -20,9 +20,8 @@ public Card() { } public virtual List List { get; private set; } = null!; public string Title { get; private set; } = null!; - public string Description { get; private set; } = ""; + public string? Description { get; private set; } = ""; public string Rank { get; set; } = null!; - public DateTimeOffset CreatedAt { get; private set; } public int CreatedById { get; private set; } public virtual IUser CreatedBy { get; private set; } = null!; @@ -38,8 +37,10 @@ public Card() { } public bool DeletedByCascade { get; private set; } public virtual ICollection Tags { get; private set; } = []; - - public static Card Create(int boardId, int swimlaneId, int listId, string title, string description, string rank, int createdById, DateTimeOffset createdAt, string? connectionId = null) + + private readonly List _comments = []; + public virtual IReadOnlyCollection Comments => _comments.AsReadOnly(); + public static Card Create(int boardId, int swimlaneId, int listId, string title, string? description, string rank, int createdById, DateTimeOffset createdAt, string? connectionId = null) { var card = new Card { @@ -58,7 +59,7 @@ public static Card Create(int boardId, int swimlaneId, int listId, string title, return card; } - public void Update(string title, string description, int updatedById, DateTimeOffset updatedAt, string? connectionId = null) + public void Update(string title, string? description, int updatedById, DateTimeOffset updatedAt, string? connectionId = null) { Title = title; Description = description; @@ -88,4 +89,8 @@ public void SoftDelete(int deletedById, DateTimeOffset deletedAt, string? connec Raise(c => new CardDeletedDomainEvent(Id, BoardId, connectionId)); } + public void AddComment(int userId, string content) + { + _comments.Add(CardComment.Create(Id, userId, content)); + } } \ No newline at end of file diff --git a/server/src/Domain/Cards/CardComment.cs b/server/src/Domain/Cards/CardComment.cs new file mode 100644 index 0000000..f699c95 --- /dev/null +++ b/server/src/Domain/Cards/CardComment.cs @@ -0,0 +1,24 @@ +using Snapflow.Domain.Users; +using Snapflow.Common; + +namespace Snapflow.Domain.Cards; + +public class CardComment : Entity +{ + public int CardId { get; private set; } + public int UserId { get; private set; } + public string Content { get; private set; } = null!; + public DateTimeOffset CreatedAt { get; private set; } + public virtual IUser User { get; private set; } = null!; + + public static CardComment Create(int cardId, int userId, string content) + { + return new CardComment + { + CardId = cardId, + UserId = userId, + Content = content, + CreatedAt = DateTimeOffset.UtcNow + }; + } +} \ No newline at end of file diff --git a/server/src/Domain/Cards/CardEvents.cs b/server/src/Domain/Cards/CardEvents.cs index d5434d7..36ea785 100644 --- a/server/src/Domain/Cards/CardEvents.cs +++ b/server/src/Domain/Cards/CardEvents.cs @@ -8,7 +8,7 @@ public sealed record CardCreatedDomainEvent( int SwimlaneId, int ListId, string Title, - string Description, + string? Description, string Rank, string? ConnectionId) : IDomainEvent; @@ -16,7 +16,7 @@ public sealed record CardUpdatedDomainEvent( int Id, int BoardId, string Title, - string Description, + string? Description, string? ConnectionId) : IDomainEvent; public sealed record CardMovedDomainEvent( diff --git a/server/src/Infrastructure/Infrastructure.csproj b/server/src/Infrastructure/Infrastructure.csproj index 7504989..b55c3c0 100644 --- a/server/src/Infrastructure/Infrastructure.csproj +++ b/server/src/Infrastructure/Infrastructure.csproj @@ -10,6 +10,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/server/src/Infrastructure/Persistence/AppDbContext.cs b/server/src/Infrastructure/Persistence/AppDbContext.cs index 56dcbd6..9c26774 100644 --- a/server/src/Infrastructure/Persistence/AppDbContext.cs +++ b/server/src/Infrastructure/Persistence/AppDbContext.cs @@ -14,6 +14,7 @@ using Microsoft.EntityFrameworkCore.Metadata; using Snapflow.Domain.Roles; + namespace Snapflow.Infrastructure.Persistence; public sealed class AppDbContext( @@ -28,7 +29,7 @@ public sealed class AppDbContext( public DbSet Members { get; private set; } public DbSet Tags { get; private set; } public DbSet Cards { get; private set; } - + public DbSet CardComments => Set(); public DbSet DataProtectionKeys { get; private set; } protected override void OnModelCreating(ModelBuilder builder) @@ -44,5 +45,14 @@ protected override void OnModelCreating(ModelBuilder builder) { builder.Entity(entityType.ClrType).Ignore(nameof(IEntity.DomainEvents)); } + builder.Entity(entity => + { + entity.HasOne(x => (AppUser)x.User) + .WithMany() + .HasForeignKey(x => x.UserId) + .OnDelete(DeleteBehavior.NoAction); + + entity.ToTable("CardComments"); + }); } } diff --git a/server/src/Infrastructure/Persistence/Configurations/CardConfiguration.cs b/server/src/Infrastructure/Persistence/Configurations/CardConfiguration.cs index 1811120..a05e952 100644 --- a/server/src/Infrastructure/Persistence/Configurations/CardConfiguration.cs +++ b/server/src/Infrastructure/Persistence/Configurations/CardConfiguration.cs @@ -37,5 +37,8 @@ public void Configure(EntityTypeBuilder builder) builder.HasIndex(c => c.ListId) .HasFilter("is_deleted = false"); + builder.Property(card => card.Description) + .HasMaxLength(2000) + .IsRequired(false); } } diff --git a/server/src/Infrastructure/Persistence/Migrations/20260505113154_AddCardDescription.Designer.cs b/server/src/Infrastructure/Persistence/Migrations/20260505113154_AddCardDescription.Designer.cs new file mode 100644 index 0000000..2f9e8c0 --- /dev/null +++ b/server/src/Infrastructure/Persistence/Migrations/20260505113154_AddCardDescription.Designer.cs @@ -0,0 +1,1124 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Snapflow.Infrastructure.Persistence; + +#nullable disable + +namespace Snapflow.Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260505113154_AddCardDescription")] + partial class AddCardDescription + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("public") + .HasAnnotation("ProductVersion", "10.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("CardTag", b => + { + b.Property("CardsId") + .HasColumnType("integer") + .HasColumnName("cards_id"); + + b.Property("TagsId") + .HasColumnType("integer") + .HasColumnName("tags_id"); + + b.HasKey("CardsId", "TagsId") + .HasName("pk_card_tag"); + + b.HasIndex("TagsId") + .HasDatabaseName("ix_card_tag_tags_id"); + + b.ToTable("card_tag", "public"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("FriendlyName") + .HasColumnType("text") + .HasColumnName("friendly_name"); + + b.Property("Xml") + .HasColumnType("text") + .HasColumnName("xml"); + + b.HasKey("Id") + .HasName("pk_data_protection_keys"); + + b.ToTable("data_protection_keys", "public"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text") + .HasColumnName("claim_type"); + + b.Property("ClaimValue") + .HasColumnType("text") + .HasColumnName("claim_value"); + + b.Property("RoleId") + .HasColumnType("integer") + .HasColumnName("role_id"); + + b.HasKey("Id") + .HasName("pk_role_claims"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_role_claims_role_id"); + + b.ToTable("role_claims", "public"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text") + .HasColumnName("claim_type"); + + b.Property("ClaimValue") + .HasColumnType("text") + .HasColumnName("claim_value"); + + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_user_claims"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_user_claims_user_id"); + + b.ToTable("user_claims", "public"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text") + .HasColumnName("login_provider"); + + b.Property("ProviderKey") + .HasColumnType("text") + .HasColumnName("provider_key"); + + b.Property("ProviderDisplayName") + .HasColumnType("text") + .HasColumnName("provider_display_name"); + + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.HasKey("LoginProvider", "ProviderKey") + .HasName("pk_user_logins"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_user_logins_user_id"); + + b.ToTable("user_logins", "public"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.Property("RoleId") + .HasColumnType("integer") + .HasColumnName("role_id"); + + b.HasKey("UserId", "RoleId") + .HasName("pk_user_roles"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_user_roles_role_id"); + + b.ToTable("user_roles", "public"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.Property("LoginProvider") + .HasColumnType("text") + .HasColumnName("login_provider"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Value") + .HasColumnType("text") + .HasColumnName("value"); + + b.HasKey("UserId", "LoginProvider", "Name") + .HasName("pk_user_tokens"); + + b.ToTable("user_tokens", "public"); + }); + + modelBuilder.Entity("Snapflow.Domain.Boards.Board", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("integer") + .HasColumnName("created_by_id"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedById") + .HasColumnType("integer") + .HasColumnName("deleted_by_id"); + + b.Property("Description") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasDefaultValue("") + .HasColumnName("description"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("title"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UpdatedById") + .HasColumnType("integer") + .HasColumnName("updated_by_id"); + + b.HasKey("Id") + .HasName("pk_boards"); + + b.HasIndex("CreatedById") + .HasDatabaseName("ix_boards_created_by_id"); + + b.HasIndex("DeletedById") + .HasDatabaseName("ix_boards_deleted_by_id"); + + b.HasIndex("Title") + .HasDatabaseName("ix_boards_title") + .HasAnnotation("Npgsql:TsVectorConfig", "english"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Title"), "GIN"); + + b.HasIndex("UpdatedById") + .HasDatabaseName("ix_boards_updated_by_id"); + + b.ToTable("boards", "public"); + }); + + modelBuilder.Entity("Snapflow.Domain.Cards.Card", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BoardId") + .HasColumnType("integer") + .HasColumnName("board_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("integer") + .HasColumnName("created_by_id"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedByCascade") + .HasColumnType("boolean") + .HasColumnName("deleted_by_cascade"); + + b.Property("DeletedById") + .HasColumnType("integer") + .HasColumnName("deleted_by_id"); + + b.Property("Description") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("description"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("ListId") + .HasColumnType("integer") + .HasColumnName("list_id"); + + b.Property("Rank") + .IsRequired() + .HasMaxLength(12) + .HasColumnType("character varying(12)") + .HasColumnName("rank"); + + b.Property("SwimlaneId") + .HasColumnType("integer") + .HasColumnName("swimlane_id"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text") + .HasColumnName("title"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UpdatedById") + .HasColumnType("integer") + .HasColumnName("updated_by_id"); + + b.HasKey("Id") + .HasName("pk_cards"); + + b.HasIndex("BoardId") + .HasDatabaseName("ix_cards_board_id") + .HasFilter("is_deleted = false"); + + b.HasIndex("CreatedById") + .HasDatabaseName("ix_cards_created_by_id"); + + b.HasIndex("DeletedById") + .HasDatabaseName("ix_cards_deleted_by_id"); + + b.HasIndex("ListId") + .HasDatabaseName("ix_cards_list_id") + .HasFilter("is_deleted = false"); + + b.HasIndex("SwimlaneId") + .HasDatabaseName("ix_cards_swimlane_id"); + + b.HasIndex("UpdatedById") + .HasDatabaseName("ix_cards_updated_by_id"); + + b.HasIndex("ListId", "Rank") + .IsUnique() + .HasDatabaseName("ix_cards_list_id_rank") + .HasFilter("is_deleted = false"); + + b.ToTable("cards", "public"); + }); + + modelBuilder.Entity("Snapflow.Domain.Lists.List", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BoardId") + .HasColumnType("integer") + .HasColumnName("board_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("integer") + .HasColumnName("created_by_id"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedByCascade") + .HasColumnType("boolean") + .HasColumnName("deleted_by_cascade"); + + b.Property("DeletedById") + .HasColumnType("integer") + .HasColumnName("deleted_by_id"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("Rank") + .IsRequired() + .HasMaxLength(12) + .HasColumnType("character varying(12)") + .HasColumnName("rank"); + + b.Property("SwimlaneId") + .HasColumnType("integer") + .HasColumnName("swimlane_id"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("title"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UpdatedById") + .HasColumnType("integer") + .HasColumnName("updated_by_id"); + + b.Property("Width") + .HasColumnType("integer") + .HasColumnName("width"); + + b.HasKey("Id") + .HasName("pk_lists"); + + b.HasIndex("BoardId") + .HasDatabaseName("ix_lists_board_id") + .HasFilter("is_deleted = false"); + + b.HasIndex("CreatedById") + .HasDatabaseName("ix_lists_created_by_id"); + + b.HasIndex("DeletedById") + .HasDatabaseName("ix_lists_deleted_by_id"); + + b.HasIndex("SwimlaneId") + .HasDatabaseName("ix_lists_swimlane_id") + .HasFilter("is_deleted = false"); + + b.HasIndex("UpdatedById") + .HasDatabaseName("ix_lists_updated_by_id"); + + b.HasIndex("SwimlaneId", "Rank") + .IsUnique() + .HasDatabaseName("ix_lists_swimlane_id_rank") + .HasFilter("is_deleted = false"); + + b.ToTable("lists", "public"); + }); + + modelBuilder.Entity("Snapflow.Domain.Members.Member", b => + { + b.Property("BoardId") + .HasColumnType("integer") + .HasColumnName("board_id"); + + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.Property("Role") + .HasColumnType("integer") + .HasColumnName("role"); + + b.HasKey("BoardId", "UserId") + .HasName("pk_board_members"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_board_members_user_id"); + + b.HasIndex("BoardId", "Role") + .IsUnique() + .HasDatabaseName("ix_board_members_board_id_role") + .HasFilter("\"role\" = 0"); + + b.ToTable("board_members", "public"); + }); + + modelBuilder.Entity("Snapflow.Domain.Swimlanes.Swimlane", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BoardId") + .HasColumnType("integer") + .HasColumnName("board_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("integer") + .HasColumnName("created_by_id"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedByCascade") + .HasColumnType("boolean") + .HasColumnName("deleted_by_cascade"); + + b.Property("DeletedById") + .HasColumnType("integer") + .HasColumnName("deleted_by_id"); + + b.Property("Height") + .HasColumnType("integer") + .HasColumnName("height"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("Rank") + .IsRequired() + .HasMaxLength(12) + .HasColumnType("character varying(12)") + .HasColumnName("rank"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("title"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UpdatedById") + .HasColumnType("integer") + .HasColumnName("updated_by_id"); + + b.HasKey("Id") + .HasName("pk_swimlanes"); + + b.HasIndex("BoardId") + .HasDatabaseName("ix_swimlanes_board_id") + .HasFilter("is_deleted = false"); + + b.HasIndex("CreatedById") + .HasDatabaseName("ix_swimlanes_created_by_id"); + + b.HasIndex("DeletedById") + .HasDatabaseName("ix_swimlanes_deleted_by_id"); + + b.HasIndex("UpdatedById") + .HasDatabaseName("ix_swimlanes_updated_by_id"); + + b.HasIndex("BoardId", "Rank") + .IsUnique() + .HasDatabaseName("ix_swimlanes_board_id_rank") + .HasFilter("is_deleted = false"); + + b.ToTable("swimlanes", "public"); + }); + + modelBuilder.Entity("Snapflow.Domain.Tags.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BoardId") + .HasColumnType("integer") + .HasColumnName("board_id"); + + b.Property("Color") + .HasColumnType("integer") + .HasColumnName("color"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("integer") + .HasColumnName("created_by_id"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedById") + .HasColumnType("integer") + .HasColumnName("deleted_by_id"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("title"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UpdatedById") + .HasColumnType("integer") + .HasColumnName("updated_by_id"); + + b.HasKey("Id") + .HasName("pk_tags"); + + b.HasIndex("BoardId") + .HasDatabaseName("ix_tags_board_id"); + + b.HasIndex("CreatedById") + .HasDatabaseName("ix_tags_created_by_id"); + + b.HasIndex("DeletedById") + .HasDatabaseName("ix_tags_deleted_by_id"); + + b.HasIndex("UpdatedById") + .HasDatabaseName("ix_tags_updated_by_id"); + + b.ToTable("tags", "public"); + }); + + modelBuilder.Entity("Snapflow.Infrastructure.Auth.Entities.AppRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text") + .HasColumnName("concurrency_stamp"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("name"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("normalized_name"); + + b.HasKey("Id") + .HasName("pk_roles"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("roles", "public"); + }); + + modelBuilder.Entity("Snapflow.Infrastructure.Auth.Entities.AppUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccessFailedCount") + .HasColumnType("integer") + .HasColumnName("access_failed_count"); + + b.Property("AvatarContentType") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("avatar_content_type"); + + b.Property("AvatarData") + .HasColumnType("bytea") + .HasColumnName("avatar_data"); + + b.Property("AvatarType") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("avatar_type"); + + b.Property("AvatarUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("avatar_url"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text") + .HasColumnName("concurrency_stamp"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("email"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean") + .HasColumnName("email_confirmed"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean") + .HasColumnName("lockout_enabled"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone") + .HasColumnName("lockout_end"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("normalized_email"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("normalized_user_name"); + + b.Property("PasswordHash") + .HasColumnType("text") + .HasColumnName("password_hash"); + + b.Property("PhoneNumber") + .HasColumnType("text") + .HasColumnName("phone_number"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean") + .HasColumnName("phone_number_confirmed"); + + b.Property("SecurityStamp") + .HasColumnType("text") + .HasColumnName("security_stamp"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean") + .HasColumnName("two_factor_enabled"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("user_name"); + + b.HasKey("Id") + .HasName("pk_users"); + + b.HasIndex("Email") + .IsUnique() + .HasDatabaseName("ix_users_email"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("users", "public"); + }); + + modelBuilder.Entity("CardTag", b => + { + b.HasOne("Snapflow.Domain.Cards.Card", null) + .WithMany() + .HasForeignKey("CardsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_card_tag_cards_cards_id"); + + b.HasOne("Snapflow.Domain.Tags.Tag", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_card_tag_tags_tags_id"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_role_claims_roles_role_id"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_claims_users_user_id"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_logins_users_user_id"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_roles_roles_role_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_roles_users_user_id"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_tokens_users_user_id"); + }); + + modelBuilder.Entity("Snapflow.Domain.Boards.Board", b => + { + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_boards_users_created_by_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "DeletedBy") + .WithMany() + .HasForeignKey("DeletedById") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_boards_users_deleted_by_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "UpdatedBy") + .WithMany() + .HasForeignKey("UpdatedById") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_boards_users_updated_by_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("UpdatedBy"); + }); + + modelBuilder.Entity("Snapflow.Domain.Cards.Card", b => + { + b.HasOne("Snapflow.Domain.Boards.Board", "Board") + .WithMany("Cards") + .HasForeignKey("BoardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_cards_boards_board_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_cards_users_created_by_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "DeletedBy") + .WithMany() + .HasForeignKey("DeletedById") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_cards_users_deleted_by_id"); + + b.HasOne("Snapflow.Domain.Lists.List", "List") + .WithMany("Cards") + .HasForeignKey("ListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_cards_lists_list_id"); + + b.HasOne("Snapflow.Domain.Swimlanes.Swimlane", "Swimlane") + .WithMany("Cards") + .HasForeignKey("SwimlaneId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_cards_swimlanes_swimlane_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "UpdatedBy") + .WithMany() + .HasForeignKey("UpdatedById") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_cards_users_updated_by_id"); + + b.Navigation("Board"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("List"); + + b.Navigation("Swimlane"); + + b.Navigation("UpdatedBy"); + }); + + modelBuilder.Entity("Snapflow.Domain.Lists.List", b => + { + b.HasOne("Snapflow.Domain.Boards.Board", "Board") + .WithMany("Lists") + .HasForeignKey("BoardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_lists_boards_board_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_lists_users_created_by_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "DeletedBy") + .WithMany() + .HasForeignKey("DeletedById") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_lists_users_deleted_by_id"); + + b.HasOne("Snapflow.Domain.Swimlanes.Swimlane", "Swimlane") + .WithMany("Lists") + .HasForeignKey("SwimlaneId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_lists_swimlanes_swimlane_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "UpdatedBy") + .WithMany() + .HasForeignKey("UpdatedById") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_lists_users_updated_by_id"); + + b.Navigation("Board"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("Swimlane"); + + b.Navigation("UpdatedBy"); + }); + + modelBuilder.Entity("Snapflow.Domain.Members.Member", b => + { + b.HasOne("Snapflow.Domain.Boards.Board", "Board") + .WithMany("Members") + .HasForeignKey("BoardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_board_members_boards_board_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_board_members_users_user_id"); + + b.Navigation("Board"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Snapflow.Domain.Swimlanes.Swimlane", b => + { + b.HasOne("Snapflow.Domain.Boards.Board", "Board") + .WithMany("Swimlanes") + .HasForeignKey("BoardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_swimlanes_boards_board_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_swimlanes_users_created_by_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "DeletedBy") + .WithMany() + .HasForeignKey("DeletedById") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_swimlanes_users_deleted_by_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "UpdatedBy") + .WithMany() + .HasForeignKey("UpdatedById") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_swimlanes_users_updated_by_id"); + + b.Navigation("Board"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("UpdatedBy"); + }); + + modelBuilder.Entity("Snapflow.Domain.Tags.Tag", b => + { + b.HasOne("Snapflow.Domain.Boards.Board", "Board") + .WithMany("Tags") + .HasForeignKey("BoardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tags_boards_board_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_tags_users_created_by_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "DeletedBy") + .WithMany() + .HasForeignKey("DeletedById") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_tags_users_deleted_by_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "UpdatedBy") + .WithMany() + .HasForeignKey("UpdatedById") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_tags_users_updated_by_id"); + + b.Navigation("Board"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("UpdatedBy"); + }); + + modelBuilder.Entity("Snapflow.Domain.Boards.Board", b => + { + b.Navigation("Cards"); + + b.Navigation("Lists"); + + b.Navigation("Members"); + + b.Navigation("Swimlanes"); + + b.Navigation("Tags"); + }); + + modelBuilder.Entity("Snapflow.Domain.Lists.List", b => + { + b.Navigation("Cards"); + }); + + modelBuilder.Entity("Snapflow.Domain.Swimlanes.Swimlane", b => + { + b.Navigation("Cards"); + + b.Navigation("Lists"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/server/src/Infrastructure/Persistence/Migrations/20260505113154_AddCardDescription.cs b/server/src/Infrastructure/Persistence/Migrations/20260505113154_AddCardDescription.cs new file mode 100644 index 0000000..79f161c --- /dev/null +++ b/server/src/Infrastructure/Persistence/Migrations/20260505113154_AddCardDescription.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Snapflow.Infrastructure.Persistence.Migrations +{ + /// + public partial class AddCardDescription : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "description", + schema: "public", + table: "cards", + type: "character varying(2000)", + maxLength: 2000, + nullable: true, + oldClrType: typeof(string), + oldType: "text"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "description", + schema: "public", + table: "cards", + type: "text", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "character varying(2000)", + oldMaxLength: 2000, + oldNullable: true); + } + } +} diff --git a/server/src/Infrastructure/Persistence/Migrations/20260505141557_AddCardComments.Designer.cs b/server/src/Infrastructure/Persistence/Migrations/20260505141557_AddCardComments.Designer.cs new file mode 100644 index 0000000..cf8aebc --- /dev/null +++ b/server/src/Infrastructure/Persistence/Migrations/20260505141557_AddCardComments.Designer.cs @@ -0,0 +1,1186 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Snapflow.Infrastructure.Persistence; + +#nullable disable + +namespace Snapflow.Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260505141557_AddCardComments")] + partial class AddCardComments + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("public") + .HasAnnotation("ProductVersion", "10.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("CardTag", b => + { + b.Property("CardsId") + .HasColumnType("integer") + .HasColumnName("cards_id"); + + b.Property("TagsId") + .HasColumnType("integer") + .HasColumnName("tags_id"); + + b.HasKey("CardsId", "TagsId") + .HasName("pk_card_tag"); + + b.HasIndex("TagsId") + .HasDatabaseName("ix_card_tag_tags_id"); + + b.ToTable("card_tag", "public"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("FriendlyName") + .HasColumnType("text") + .HasColumnName("friendly_name"); + + b.Property("Xml") + .HasColumnType("text") + .HasColumnName("xml"); + + b.HasKey("Id") + .HasName("pk_data_protection_keys"); + + b.ToTable("data_protection_keys", "public"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text") + .HasColumnName("claim_type"); + + b.Property("ClaimValue") + .HasColumnType("text") + .HasColumnName("claim_value"); + + b.Property("RoleId") + .HasColumnType("integer") + .HasColumnName("role_id"); + + b.HasKey("Id") + .HasName("pk_role_claims"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_role_claims_role_id"); + + b.ToTable("role_claims", "public"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text") + .HasColumnName("claim_type"); + + b.Property("ClaimValue") + .HasColumnType("text") + .HasColumnName("claim_value"); + + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_user_claims"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_user_claims_user_id"); + + b.ToTable("user_claims", "public"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text") + .HasColumnName("login_provider"); + + b.Property("ProviderKey") + .HasColumnType("text") + .HasColumnName("provider_key"); + + b.Property("ProviderDisplayName") + .HasColumnType("text") + .HasColumnName("provider_display_name"); + + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.HasKey("LoginProvider", "ProviderKey") + .HasName("pk_user_logins"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_user_logins_user_id"); + + b.ToTable("user_logins", "public"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.Property("RoleId") + .HasColumnType("integer") + .HasColumnName("role_id"); + + b.HasKey("UserId", "RoleId") + .HasName("pk_user_roles"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_user_roles_role_id"); + + b.ToTable("user_roles", "public"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.Property("LoginProvider") + .HasColumnType("text") + .HasColumnName("login_provider"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Value") + .HasColumnType("text") + .HasColumnName("value"); + + b.HasKey("UserId", "LoginProvider", "Name") + .HasName("pk_user_tokens"); + + b.ToTable("user_tokens", "public"); + }); + + modelBuilder.Entity("Snapflow.Domain.Boards.Board", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("integer") + .HasColumnName("created_by_id"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedById") + .HasColumnType("integer") + .HasColumnName("deleted_by_id"); + + b.Property("Description") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasDefaultValue("") + .HasColumnName("description"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("title"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UpdatedById") + .HasColumnType("integer") + .HasColumnName("updated_by_id"); + + b.HasKey("Id") + .HasName("pk_boards"); + + b.HasIndex("CreatedById") + .HasDatabaseName("ix_boards_created_by_id"); + + b.HasIndex("DeletedById") + .HasDatabaseName("ix_boards_deleted_by_id"); + + b.HasIndex("Title") + .HasDatabaseName("ix_boards_title") + .HasAnnotation("Npgsql:TsVectorConfig", "english"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Title"), "GIN"); + + b.HasIndex("UpdatedById") + .HasDatabaseName("ix_boards_updated_by_id"); + + b.ToTable("boards", "public"); + }); + + modelBuilder.Entity("Snapflow.Domain.Cards.Card", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BoardId") + .HasColumnType("integer") + .HasColumnName("board_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("integer") + .HasColumnName("created_by_id"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedByCascade") + .HasColumnType("boolean") + .HasColumnName("deleted_by_cascade"); + + b.Property("DeletedById") + .HasColumnType("integer") + .HasColumnName("deleted_by_id"); + + b.Property("Description") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("description"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("ListId") + .HasColumnType("integer") + .HasColumnName("list_id"); + + b.Property("Rank") + .IsRequired() + .HasMaxLength(12) + .HasColumnType("character varying(12)") + .HasColumnName("rank"); + + b.Property("SwimlaneId") + .HasColumnType("integer") + .HasColumnName("swimlane_id"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text") + .HasColumnName("title"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UpdatedById") + .HasColumnType("integer") + .HasColumnName("updated_by_id"); + + b.HasKey("Id") + .HasName("pk_cards"); + + b.HasIndex("BoardId") + .HasDatabaseName("ix_cards_board_id") + .HasFilter("is_deleted = false"); + + b.HasIndex("CreatedById") + .HasDatabaseName("ix_cards_created_by_id"); + + b.HasIndex("DeletedById") + .HasDatabaseName("ix_cards_deleted_by_id"); + + b.HasIndex("ListId") + .HasDatabaseName("ix_cards_list_id") + .HasFilter("is_deleted = false"); + + b.HasIndex("SwimlaneId") + .HasDatabaseName("ix_cards_swimlane_id"); + + b.HasIndex("UpdatedById") + .HasDatabaseName("ix_cards_updated_by_id"); + + b.HasIndex("ListId", "Rank") + .IsUnique() + .HasDatabaseName("ix_cards_list_id_rank") + .HasFilter("is_deleted = false"); + + b.ToTable("cards", "public"); + }); + + modelBuilder.Entity("Snapflow.Domain.Cards.CardComment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CardId") + .HasColumnType("integer") + .HasColumnName("card_id"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text") + .HasColumnName("content"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_card_comments"); + + b.HasIndex("CardId") + .HasDatabaseName("ix_card_comments_card_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_card_comments_user_id"); + + b.ToTable("CardComments", "public"); + }); + + modelBuilder.Entity("Snapflow.Domain.Lists.List", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BoardId") + .HasColumnType("integer") + .HasColumnName("board_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("integer") + .HasColumnName("created_by_id"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedByCascade") + .HasColumnType("boolean") + .HasColumnName("deleted_by_cascade"); + + b.Property("DeletedById") + .HasColumnType("integer") + .HasColumnName("deleted_by_id"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("Rank") + .IsRequired() + .HasMaxLength(12) + .HasColumnType("character varying(12)") + .HasColumnName("rank"); + + b.Property("SwimlaneId") + .HasColumnType("integer") + .HasColumnName("swimlane_id"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("title"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UpdatedById") + .HasColumnType("integer") + .HasColumnName("updated_by_id"); + + b.Property("Width") + .HasColumnType("integer") + .HasColumnName("width"); + + b.HasKey("Id") + .HasName("pk_lists"); + + b.HasIndex("BoardId") + .HasDatabaseName("ix_lists_board_id") + .HasFilter("is_deleted = false"); + + b.HasIndex("CreatedById") + .HasDatabaseName("ix_lists_created_by_id"); + + b.HasIndex("DeletedById") + .HasDatabaseName("ix_lists_deleted_by_id"); + + b.HasIndex("SwimlaneId") + .HasDatabaseName("ix_lists_swimlane_id") + .HasFilter("is_deleted = false"); + + b.HasIndex("UpdatedById") + .HasDatabaseName("ix_lists_updated_by_id"); + + b.HasIndex("SwimlaneId", "Rank") + .IsUnique() + .HasDatabaseName("ix_lists_swimlane_id_rank") + .HasFilter("is_deleted = false"); + + b.ToTable("lists", "public"); + }); + + modelBuilder.Entity("Snapflow.Domain.Members.Member", b => + { + b.Property("BoardId") + .HasColumnType("integer") + .HasColumnName("board_id"); + + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.Property("Role") + .HasColumnType("integer") + .HasColumnName("role"); + + b.HasKey("BoardId", "UserId") + .HasName("pk_board_members"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_board_members_user_id"); + + b.HasIndex("BoardId", "Role") + .IsUnique() + .HasDatabaseName("ix_board_members_board_id_role") + .HasFilter("\"role\" = 0"); + + b.ToTable("board_members", "public"); + }); + + modelBuilder.Entity("Snapflow.Domain.Swimlanes.Swimlane", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BoardId") + .HasColumnType("integer") + .HasColumnName("board_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("integer") + .HasColumnName("created_by_id"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedByCascade") + .HasColumnType("boolean") + .HasColumnName("deleted_by_cascade"); + + b.Property("DeletedById") + .HasColumnType("integer") + .HasColumnName("deleted_by_id"); + + b.Property("Height") + .HasColumnType("integer") + .HasColumnName("height"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("Rank") + .IsRequired() + .HasMaxLength(12) + .HasColumnType("character varying(12)") + .HasColumnName("rank"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("title"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UpdatedById") + .HasColumnType("integer") + .HasColumnName("updated_by_id"); + + b.HasKey("Id") + .HasName("pk_swimlanes"); + + b.HasIndex("BoardId") + .HasDatabaseName("ix_swimlanes_board_id") + .HasFilter("is_deleted = false"); + + b.HasIndex("CreatedById") + .HasDatabaseName("ix_swimlanes_created_by_id"); + + b.HasIndex("DeletedById") + .HasDatabaseName("ix_swimlanes_deleted_by_id"); + + b.HasIndex("UpdatedById") + .HasDatabaseName("ix_swimlanes_updated_by_id"); + + b.HasIndex("BoardId", "Rank") + .IsUnique() + .HasDatabaseName("ix_swimlanes_board_id_rank") + .HasFilter("is_deleted = false"); + + b.ToTable("swimlanes", "public"); + }); + + modelBuilder.Entity("Snapflow.Domain.Tags.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BoardId") + .HasColumnType("integer") + .HasColumnName("board_id"); + + b.Property("Color") + .HasColumnType("integer") + .HasColumnName("color"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("integer") + .HasColumnName("created_by_id"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedById") + .HasColumnType("integer") + .HasColumnName("deleted_by_id"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("title"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UpdatedById") + .HasColumnType("integer") + .HasColumnName("updated_by_id"); + + b.HasKey("Id") + .HasName("pk_tags"); + + b.HasIndex("BoardId") + .HasDatabaseName("ix_tags_board_id"); + + b.HasIndex("CreatedById") + .HasDatabaseName("ix_tags_created_by_id"); + + b.HasIndex("DeletedById") + .HasDatabaseName("ix_tags_deleted_by_id"); + + b.HasIndex("UpdatedById") + .HasDatabaseName("ix_tags_updated_by_id"); + + b.ToTable("tags", "public"); + }); + + modelBuilder.Entity("Snapflow.Infrastructure.Auth.Entities.AppRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text") + .HasColumnName("concurrency_stamp"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("name"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("normalized_name"); + + b.HasKey("Id") + .HasName("pk_roles"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("roles", "public"); + }); + + modelBuilder.Entity("Snapflow.Infrastructure.Auth.Entities.AppUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccessFailedCount") + .HasColumnType("integer") + .HasColumnName("access_failed_count"); + + b.Property("AvatarContentType") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("avatar_content_type"); + + b.Property("AvatarData") + .HasColumnType("bytea") + .HasColumnName("avatar_data"); + + b.Property("AvatarType") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("avatar_type"); + + b.Property("AvatarUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("avatar_url"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text") + .HasColumnName("concurrency_stamp"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("email"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean") + .HasColumnName("email_confirmed"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean") + .HasColumnName("lockout_enabled"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone") + .HasColumnName("lockout_end"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("normalized_email"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("normalized_user_name"); + + b.Property("PasswordHash") + .HasColumnType("text") + .HasColumnName("password_hash"); + + b.Property("PhoneNumber") + .HasColumnType("text") + .HasColumnName("phone_number"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean") + .HasColumnName("phone_number_confirmed"); + + b.Property("SecurityStamp") + .HasColumnType("text") + .HasColumnName("security_stamp"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean") + .HasColumnName("two_factor_enabled"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("user_name"); + + b.HasKey("Id") + .HasName("pk_users"); + + b.HasIndex("Email") + .IsUnique() + .HasDatabaseName("ix_users_email"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("users", "public"); + }); + + modelBuilder.Entity("CardTag", b => + { + b.HasOne("Snapflow.Domain.Cards.Card", null) + .WithMany() + .HasForeignKey("CardsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_card_tag_cards_cards_id"); + + b.HasOne("Snapflow.Domain.Tags.Tag", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_card_tag_tags_tags_id"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_role_claims_roles_role_id"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_claims_users_user_id"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_logins_users_user_id"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_roles_roles_role_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_roles_users_user_id"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_tokens_users_user_id"); + }); + + modelBuilder.Entity("Snapflow.Domain.Boards.Board", b => + { + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_boards_users_created_by_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "DeletedBy") + .WithMany() + .HasForeignKey("DeletedById") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_boards_users_deleted_by_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "UpdatedBy") + .WithMany() + .HasForeignKey("UpdatedById") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_boards_users_updated_by_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("UpdatedBy"); + }); + + modelBuilder.Entity("Snapflow.Domain.Cards.Card", b => + { + b.HasOne("Snapflow.Domain.Boards.Board", "Board") + .WithMany("Cards") + .HasForeignKey("BoardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_cards_boards_board_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_cards_users_created_by_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "DeletedBy") + .WithMany() + .HasForeignKey("DeletedById") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_cards_users_deleted_by_id"); + + b.HasOne("Snapflow.Domain.Lists.List", "List") + .WithMany("Cards") + .HasForeignKey("ListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_cards_lists_list_id"); + + b.HasOne("Snapflow.Domain.Swimlanes.Swimlane", "Swimlane") + .WithMany("Cards") + .HasForeignKey("SwimlaneId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_cards_swimlanes_swimlane_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "UpdatedBy") + .WithMany() + .HasForeignKey("UpdatedById") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_cards_users_updated_by_id"); + + b.Navigation("Board"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("List"); + + b.Navigation("Swimlane"); + + b.Navigation("UpdatedBy"); + }); + + modelBuilder.Entity("Snapflow.Domain.Cards.CardComment", b => + { + b.HasOne("Snapflow.Domain.Cards.Card", null) + .WithMany("Comments") + .HasForeignKey("CardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_card_comments_cards_card_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired() + .HasConstraintName("fk_card_comments_users_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Snapflow.Domain.Lists.List", b => + { + b.HasOne("Snapflow.Domain.Boards.Board", "Board") + .WithMany("Lists") + .HasForeignKey("BoardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_lists_boards_board_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_lists_users_created_by_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "DeletedBy") + .WithMany() + .HasForeignKey("DeletedById") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_lists_users_deleted_by_id"); + + b.HasOne("Snapflow.Domain.Swimlanes.Swimlane", "Swimlane") + .WithMany("Lists") + .HasForeignKey("SwimlaneId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_lists_swimlanes_swimlane_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "UpdatedBy") + .WithMany() + .HasForeignKey("UpdatedById") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_lists_users_updated_by_id"); + + b.Navigation("Board"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("Swimlane"); + + b.Navigation("UpdatedBy"); + }); + + modelBuilder.Entity("Snapflow.Domain.Members.Member", b => + { + b.HasOne("Snapflow.Domain.Boards.Board", "Board") + .WithMany("Members") + .HasForeignKey("BoardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_board_members_boards_board_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_board_members_users_user_id"); + + b.Navigation("Board"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Snapflow.Domain.Swimlanes.Swimlane", b => + { + b.HasOne("Snapflow.Domain.Boards.Board", "Board") + .WithMany("Swimlanes") + .HasForeignKey("BoardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_swimlanes_boards_board_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_swimlanes_users_created_by_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "DeletedBy") + .WithMany() + .HasForeignKey("DeletedById") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_swimlanes_users_deleted_by_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "UpdatedBy") + .WithMany() + .HasForeignKey("UpdatedById") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_swimlanes_users_updated_by_id"); + + b.Navigation("Board"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("UpdatedBy"); + }); + + modelBuilder.Entity("Snapflow.Domain.Tags.Tag", b => + { + b.HasOne("Snapflow.Domain.Boards.Board", "Board") + .WithMany("Tags") + .HasForeignKey("BoardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tags_boards_board_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_tags_users_created_by_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "DeletedBy") + .WithMany() + .HasForeignKey("DeletedById") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_tags_users_deleted_by_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "UpdatedBy") + .WithMany() + .HasForeignKey("UpdatedById") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_tags_users_updated_by_id"); + + b.Navigation("Board"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("UpdatedBy"); + }); + + modelBuilder.Entity("Snapflow.Domain.Boards.Board", b => + { + b.Navigation("Cards"); + + b.Navigation("Lists"); + + b.Navigation("Members"); + + b.Navigation("Swimlanes"); + + b.Navigation("Tags"); + }); + + modelBuilder.Entity("Snapflow.Domain.Cards.Card", b => + { + b.Navigation("Comments"); + }); + + modelBuilder.Entity("Snapflow.Domain.Lists.List", b => + { + b.Navigation("Cards"); + }); + + modelBuilder.Entity("Snapflow.Domain.Swimlanes.Swimlane", b => + { + b.Navigation("Cards"); + + b.Navigation("Lists"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/server/src/Infrastructure/Persistence/Migrations/20260505141557_AddCardComments.cs b/server/src/Infrastructure/Persistence/Migrations/20260505141557_AddCardComments.cs new file mode 100644 index 0000000..2a9c69c --- /dev/null +++ b/server/src/Infrastructure/Persistence/Migrations/20260505141557_AddCardComments.cs @@ -0,0 +1,66 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Snapflow.Infrastructure.Persistence.Migrations +{ + /// + public partial class AddCardComments : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "CardComments", + schema: "public", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + card_id = table.Column(type: "integer", nullable: false), + user_id = table.Column(type: "integer", nullable: false), + content = table.Column(type: "text", nullable: false), + created_at = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_card_comments", x => x.id); + table.ForeignKey( + name: "fk_card_comments_cards_card_id", + column: x => x.card_id, + principalSchema: "public", + principalTable: "cards", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_card_comments_users_user_id", + column: x => x.user_id, + principalSchema: "public", + principalTable: "users", + principalColumn: "id"); + }); + + migrationBuilder.CreateIndex( + name: "ix_card_comments_card_id", + schema: "public", + table: "CardComments", + column: "card_id"); + + migrationBuilder.CreateIndex( + name: "ix_card_comments_user_id", + schema: "public", + table: "CardComments", + column: "user_id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "CardComments", + schema: "public"); + } + } +} diff --git a/server/src/Infrastructure/Persistence/Migrations/20260505193022_AddCardCommentsRelation.Designer.cs b/server/src/Infrastructure/Persistence/Migrations/20260505193022_AddCardCommentsRelation.Designer.cs new file mode 100644 index 0000000..07cb54c --- /dev/null +++ b/server/src/Infrastructure/Persistence/Migrations/20260505193022_AddCardCommentsRelation.Designer.cs @@ -0,0 +1,1186 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Snapflow.Infrastructure.Persistence; + +#nullable disable + +namespace Snapflow.Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260505193022_AddCardCommentsRelation")] + partial class AddCardCommentsRelation + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("public") + .HasAnnotation("ProductVersion", "10.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("CardTag", b => + { + b.Property("CardsId") + .HasColumnType("integer") + .HasColumnName("cards_id"); + + b.Property("TagsId") + .HasColumnType("integer") + .HasColumnName("tags_id"); + + b.HasKey("CardsId", "TagsId") + .HasName("pk_card_tag"); + + b.HasIndex("TagsId") + .HasDatabaseName("ix_card_tag_tags_id"); + + b.ToTable("card_tag", "public"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("FriendlyName") + .HasColumnType("text") + .HasColumnName("friendly_name"); + + b.Property("Xml") + .HasColumnType("text") + .HasColumnName("xml"); + + b.HasKey("Id") + .HasName("pk_data_protection_keys"); + + b.ToTable("data_protection_keys", "public"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text") + .HasColumnName("claim_type"); + + b.Property("ClaimValue") + .HasColumnType("text") + .HasColumnName("claim_value"); + + b.Property("RoleId") + .HasColumnType("integer") + .HasColumnName("role_id"); + + b.HasKey("Id") + .HasName("pk_role_claims"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_role_claims_role_id"); + + b.ToTable("role_claims", "public"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text") + .HasColumnName("claim_type"); + + b.Property("ClaimValue") + .HasColumnType("text") + .HasColumnName("claim_value"); + + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_user_claims"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_user_claims_user_id"); + + b.ToTable("user_claims", "public"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text") + .HasColumnName("login_provider"); + + b.Property("ProviderKey") + .HasColumnType("text") + .HasColumnName("provider_key"); + + b.Property("ProviderDisplayName") + .HasColumnType("text") + .HasColumnName("provider_display_name"); + + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.HasKey("LoginProvider", "ProviderKey") + .HasName("pk_user_logins"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_user_logins_user_id"); + + b.ToTable("user_logins", "public"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.Property("RoleId") + .HasColumnType("integer") + .HasColumnName("role_id"); + + b.HasKey("UserId", "RoleId") + .HasName("pk_user_roles"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_user_roles_role_id"); + + b.ToTable("user_roles", "public"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.Property("LoginProvider") + .HasColumnType("text") + .HasColumnName("login_provider"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Value") + .HasColumnType("text") + .HasColumnName("value"); + + b.HasKey("UserId", "LoginProvider", "Name") + .HasName("pk_user_tokens"); + + b.ToTable("user_tokens", "public"); + }); + + modelBuilder.Entity("Snapflow.Domain.Boards.Board", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("integer") + .HasColumnName("created_by_id"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedById") + .HasColumnType("integer") + .HasColumnName("deleted_by_id"); + + b.Property("Description") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasDefaultValue("") + .HasColumnName("description"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("title"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UpdatedById") + .HasColumnType("integer") + .HasColumnName("updated_by_id"); + + b.HasKey("Id") + .HasName("pk_boards"); + + b.HasIndex("CreatedById") + .HasDatabaseName("ix_boards_created_by_id"); + + b.HasIndex("DeletedById") + .HasDatabaseName("ix_boards_deleted_by_id"); + + b.HasIndex("Title") + .HasDatabaseName("ix_boards_title") + .HasAnnotation("Npgsql:TsVectorConfig", "english"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Title"), "GIN"); + + b.HasIndex("UpdatedById") + .HasDatabaseName("ix_boards_updated_by_id"); + + b.ToTable("boards", "public"); + }); + + modelBuilder.Entity("Snapflow.Domain.Cards.Card", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BoardId") + .HasColumnType("integer") + .HasColumnName("board_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("integer") + .HasColumnName("created_by_id"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedByCascade") + .HasColumnType("boolean") + .HasColumnName("deleted_by_cascade"); + + b.Property("DeletedById") + .HasColumnType("integer") + .HasColumnName("deleted_by_id"); + + b.Property("Description") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("description"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("ListId") + .HasColumnType("integer") + .HasColumnName("list_id"); + + b.Property("Rank") + .IsRequired() + .HasMaxLength(12) + .HasColumnType("character varying(12)") + .HasColumnName("rank"); + + b.Property("SwimlaneId") + .HasColumnType("integer") + .HasColumnName("swimlane_id"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text") + .HasColumnName("title"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UpdatedById") + .HasColumnType("integer") + .HasColumnName("updated_by_id"); + + b.HasKey("Id") + .HasName("pk_cards"); + + b.HasIndex("BoardId") + .HasDatabaseName("ix_cards_board_id") + .HasFilter("is_deleted = false"); + + b.HasIndex("CreatedById") + .HasDatabaseName("ix_cards_created_by_id"); + + b.HasIndex("DeletedById") + .HasDatabaseName("ix_cards_deleted_by_id"); + + b.HasIndex("ListId") + .HasDatabaseName("ix_cards_list_id") + .HasFilter("is_deleted = false"); + + b.HasIndex("SwimlaneId") + .HasDatabaseName("ix_cards_swimlane_id"); + + b.HasIndex("UpdatedById") + .HasDatabaseName("ix_cards_updated_by_id"); + + b.HasIndex("ListId", "Rank") + .IsUnique() + .HasDatabaseName("ix_cards_list_id_rank") + .HasFilter("is_deleted = false"); + + b.ToTable("cards", "public"); + }); + + modelBuilder.Entity("Snapflow.Domain.Cards.CardComment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CardId") + .HasColumnType("integer") + .HasColumnName("card_id"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text") + .HasColumnName("content"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_card_comments"); + + b.HasIndex("CardId") + .HasDatabaseName("ix_card_comments_card_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_card_comments_user_id"); + + b.ToTable("CardComments", "public"); + }); + + modelBuilder.Entity("Snapflow.Domain.Lists.List", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BoardId") + .HasColumnType("integer") + .HasColumnName("board_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("integer") + .HasColumnName("created_by_id"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedByCascade") + .HasColumnType("boolean") + .HasColumnName("deleted_by_cascade"); + + b.Property("DeletedById") + .HasColumnType("integer") + .HasColumnName("deleted_by_id"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("Rank") + .IsRequired() + .HasMaxLength(12) + .HasColumnType("character varying(12)") + .HasColumnName("rank"); + + b.Property("SwimlaneId") + .HasColumnType("integer") + .HasColumnName("swimlane_id"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("title"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UpdatedById") + .HasColumnType("integer") + .HasColumnName("updated_by_id"); + + b.Property("Width") + .HasColumnType("integer") + .HasColumnName("width"); + + b.HasKey("Id") + .HasName("pk_lists"); + + b.HasIndex("BoardId") + .HasDatabaseName("ix_lists_board_id") + .HasFilter("is_deleted = false"); + + b.HasIndex("CreatedById") + .HasDatabaseName("ix_lists_created_by_id"); + + b.HasIndex("DeletedById") + .HasDatabaseName("ix_lists_deleted_by_id"); + + b.HasIndex("SwimlaneId") + .HasDatabaseName("ix_lists_swimlane_id") + .HasFilter("is_deleted = false"); + + b.HasIndex("UpdatedById") + .HasDatabaseName("ix_lists_updated_by_id"); + + b.HasIndex("SwimlaneId", "Rank") + .IsUnique() + .HasDatabaseName("ix_lists_swimlane_id_rank") + .HasFilter("is_deleted = false"); + + b.ToTable("lists", "public"); + }); + + modelBuilder.Entity("Snapflow.Domain.Members.Member", b => + { + b.Property("BoardId") + .HasColumnType("integer") + .HasColumnName("board_id"); + + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.Property("Role") + .HasColumnType("integer") + .HasColumnName("role"); + + b.HasKey("BoardId", "UserId") + .HasName("pk_board_members"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_board_members_user_id"); + + b.HasIndex("BoardId", "Role") + .IsUnique() + .HasDatabaseName("ix_board_members_board_id_role") + .HasFilter("\"role\" = 0"); + + b.ToTable("board_members", "public"); + }); + + modelBuilder.Entity("Snapflow.Domain.Swimlanes.Swimlane", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BoardId") + .HasColumnType("integer") + .HasColumnName("board_id"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("integer") + .HasColumnName("created_by_id"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedByCascade") + .HasColumnType("boolean") + .HasColumnName("deleted_by_cascade"); + + b.Property("DeletedById") + .HasColumnType("integer") + .HasColumnName("deleted_by_id"); + + b.Property("Height") + .HasColumnType("integer") + .HasColumnName("height"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("Rank") + .IsRequired() + .HasMaxLength(12) + .HasColumnType("character varying(12)") + .HasColumnName("rank"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("title"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UpdatedById") + .HasColumnType("integer") + .HasColumnName("updated_by_id"); + + b.HasKey("Id") + .HasName("pk_swimlanes"); + + b.HasIndex("BoardId") + .HasDatabaseName("ix_swimlanes_board_id") + .HasFilter("is_deleted = false"); + + b.HasIndex("CreatedById") + .HasDatabaseName("ix_swimlanes_created_by_id"); + + b.HasIndex("DeletedById") + .HasDatabaseName("ix_swimlanes_deleted_by_id"); + + b.HasIndex("UpdatedById") + .HasDatabaseName("ix_swimlanes_updated_by_id"); + + b.HasIndex("BoardId", "Rank") + .IsUnique() + .HasDatabaseName("ix_swimlanes_board_id_rank") + .HasFilter("is_deleted = false"); + + b.ToTable("swimlanes", "public"); + }); + + modelBuilder.Entity("Snapflow.Domain.Tags.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BoardId") + .HasColumnType("integer") + .HasColumnName("board_id"); + + b.Property("Color") + .HasColumnType("integer") + .HasColumnName("color"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("integer") + .HasColumnName("created_by_id"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedById") + .HasColumnType("integer") + .HasColumnName("deleted_by_id"); + + b.Property("IsDeleted") + .HasColumnType("boolean") + .HasColumnName("is_deleted"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("title"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_at"); + + b.Property("UpdatedById") + .HasColumnType("integer") + .HasColumnName("updated_by_id"); + + b.HasKey("Id") + .HasName("pk_tags"); + + b.HasIndex("BoardId") + .HasDatabaseName("ix_tags_board_id"); + + b.HasIndex("CreatedById") + .HasDatabaseName("ix_tags_created_by_id"); + + b.HasIndex("DeletedById") + .HasDatabaseName("ix_tags_deleted_by_id"); + + b.HasIndex("UpdatedById") + .HasDatabaseName("ix_tags_updated_by_id"); + + b.ToTable("tags", "public"); + }); + + modelBuilder.Entity("Snapflow.Infrastructure.Auth.Entities.AppRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text") + .HasColumnName("concurrency_stamp"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("name"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("normalized_name"); + + b.HasKey("Id") + .HasName("pk_roles"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("roles", "public"); + }); + + modelBuilder.Entity("Snapflow.Infrastructure.Auth.Entities.AppUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccessFailedCount") + .HasColumnType("integer") + .HasColumnName("access_failed_count"); + + b.Property("AvatarContentType") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("avatar_content_type"); + + b.Property("AvatarData") + .HasColumnType("bytea") + .HasColumnName("avatar_data"); + + b.Property("AvatarType") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("avatar_type"); + + b.Property("AvatarUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("avatar_url"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text") + .HasColumnName("concurrency_stamp"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("email"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean") + .HasColumnName("email_confirmed"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("is_deleted"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean") + .HasColumnName("lockout_enabled"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone") + .HasColumnName("lockout_end"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("normalized_email"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("normalized_user_name"); + + b.Property("PasswordHash") + .HasColumnType("text") + .HasColumnName("password_hash"); + + b.Property("PhoneNumber") + .HasColumnType("text") + .HasColumnName("phone_number"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean") + .HasColumnName("phone_number_confirmed"); + + b.Property("SecurityStamp") + .HasColumnType("text") + .HasColumnName("security_stamp"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean") + .HasColumnName("two_factor_enabled"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("user_name"); + + b.HasKey("Id") + .HasName("pk_users"); + + b.HasIndex("Email") + .IsUnique() + .HasDatabaseName("ix_users_email"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("users", "public"); + }); + + modelBuilder.Entity("CardTag", b => + { + b.HasOne("Snapflow.Domain.Cards.Card", null) + .WithMany() + .HasForeignKey("CardsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_card_tag_cards_cards_id"); + + b.HasOne("Snapflow.Domain.Tags.Tag", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_card_tag_tags_tags_id"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_role_claims_roles_role_id"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_claims_users_user_id"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_logins_users_user_id"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_roles_roles_role_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_roles_users_user_id"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_tokens_users_user_id"); + }); + + modelBuilder.Entity("Snapflow.Domain.Boards.Board", b => + { + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_boards_users_created_by_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "DeletedBy") + .WithMany() + .HasForeignKey("DeletedById") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_boards_users_deleted_by_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "UpdatedBy") + .WithMany() + .HasForeignKey("UpdatedById") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_boards_users_updated_by_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("UpdatedBy"); + }); + + modelBuilder.Entity("Snapflow.Domain.Cards.Card", b => + { + b.HasOne("Snapflow.Domain.Boards.Board", "Board") + .WithMany("Cards") + .HasForeignKey("BoardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_cards_boards_board_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_cards_users_created_by_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "DeletedBy") + .WithMany() + .HasForeignKey("DeletedById") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_cards_users_deleted_by_id"); + + b.HasOne("Snapflow.Domain.Lists.List", "List") + .WithMany("Cards") + .HasForeignKey("ListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_cards_lists_list_id"); + + b.HasOne("Snapflow.Domain.Swimlanes.Swimlane", "Swimlane") + .WithMany("Cards") + .HasForeignKey("SwimlaneId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_cards_swimlanes_swimlane_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "UpdatedBy") + .WithMany() + .HasForeignKey("UpdatedById") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_cards_users_updated_by_id"); + + b.Navigation("Board"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("List"); + + b.Navigation("Swimlane"); + + b.Navigation("UpdatedBy"); + }); + + modelBuilder.Entity("Snapflow.Domain.Cards.CardComment", b => + { + b.HasOne("Snapflow.Domain.Cards.Card", null) + .WithMany("Comments") + .HasForeignKey("CardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_card_comments_cards_card_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired() + .HasConstraintName("fk_card_comments_users_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Snapflow.Domain.Lists.List", b => + { + b.HasOne("Snapflow.Domain.Boards.Board", "Board") + .WithMany("Lists") + .HasForeignKey("BoardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_lists_boards_board_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_lists_users_created_by_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "DeletedBy") + .WithMany() + .HasForeignKey("DeletedById") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_lists_users_deleted_by_id"); + + b.HasOne("Snapflow.Domain.Swimlanes.Swimlane", "Swimlane") + .WithMany("Lists") + .HasForeignKey("SwimlaneId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_lists_swimlanes_swimlane_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "UpdatedBy") + .WithMany() + .HasForeignKey("UpdatedById") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_lists_users_updated_by_id"); + + b.Navigation("Board"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("Swimlane"); + + b.Navigation("UpdatedBy"); + }); + + modelBuilder.Entity("Snapflow.Domain.Members.Member", b => + { + b.HasOne("Snapflow.Domain.Boards.Board", "Board") + .WithMany("Members") + .HasForeignKey("BoardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_board_members_boards_board_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_board_members_users_user_id"); + + b.Navigation("Board"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Snapflow.Domain.Swimlanes.Swimlane", b => + { + b.HasOne("Snapflow.Domain.Boards.Board", "Board") + .WithMany("Swimlanes") + .HasForeignKey("BoardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_swimlanes_boards_board_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_swimlanes_users_created_by_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "DeletedBy") + .WithMany() + .HasForeignKey("DeletedById") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_swimlanes_users_deleted_by_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "UpdatedBy") + .WithMany() + .HasForeignKey("UpdatedById") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_swimlanes_users_updated_by_id"); + + b.Navigation("Board"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("UpdatedBy"); + }); + + modelBuilder.Entity("Snapflow.Domain.Tags.Tag", b => + { + b.HasOne("Snapflow.Domain.Boards.Board", "Board") + .WithMany("Tags") + .HasForeignKey("BoardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tags_boards_board_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_tags_users_created_by_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "DeletedBy") + .WithMany() + .HasForeignKey("DeletedById") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_tags_users_deleted_by_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "UpdatedBy") + .WithMany() + .HasForeignKey("UpdatedById") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_tags_users_updated_by_id"); + + b.Navigation("Board"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("UpdatedBy"); + }); + + modelBuilder.Entity("Snapflow.Domain.Boards.Board", b => + { + b.Navigation("Cards"); + + b.Navigation("Lists"); + + b.Navigation("Members"); + + b.Navigation("Swimlanes"); + + b.Navigation("Tags"); + }); + + modelBuilder.Entity("Snapflow.Domain.Cards.Card", b => + { + b.Navigation("Comments"); + }); + + modelBuilder.Entity("Snapflow.Domain.Lists.List", b => + { + b.Navigation("Cards"); + }); + + modelBuilder.Entity("Snapflow.Domain.Swimlanes.Swimlane", b => + { + b.Navigation("Cards"); + + b.Navigation("Lists"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/server/src/Infrastructure/Persistence/Migrations/20260505193022_AddCardCommentsRelation.cs b/server/src/Infrastructure/Persistence/Migrations/20260505193022_AddCardCommentsRelation.cs new file mode 100644 index 0000000..3dfc4f3 --- /dev/null +++ b/server/src/Infrastructure/Persistence/Migrations/20260505193022_AddCardCommentsRelation.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Snapflow.Infrastructure.Persistence.Migrations +{ + /// + public partial class AddCardCommentsRelation : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/server/src/Infrastructure/Persistence/Migrations/AppDbContextModelSnapshot.cs b/server/src/Infrastructure/Persistence/Migrations/AppDbContextModelSnapshot.cs index c4a67ac..220a163 100644 --- a/server/src/Infrastructure/Persistence/Migrations/AppDbContextModelSnapshot.cs +++ b/server/src/Infrastructure/Persistence/Migrations/AppDbContextModelSnapshot.cs @@ -301,8 +301,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnName("deleted_by_id"); b.Property("Description") - .IsRequired() - .HasColumnType("text") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") .HasColumnName("description"); b.Property("IsDeleted") @@ -367,6 +367,44 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("cards", "public"); }); + modelBuilder.Entity("Snapflow.Domain.Cards.CardComment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CardId") + .HasColumnType("integer") + .HasColumnName("card_id"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text") + .HasColumnName("content"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_card_comments"); + + b.HasIndex("CardId") + .HasDatabaseName("ix_card_comments_card_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_card_comments_user_id"); + + b.ToTable("CardComments", "public"); + }); + modelBuilder.Entity("Snapflow.Domain.Lists.List", b => { b.Property("Id") @@ -950,6 +988,25 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("UpdatedBy"); }); + modelBuilder.Entity("Snapflow.Domain.Cards.CardComment", b => + { + b.HasOne("Snapflow.Domain.Cards.Card", null) + .WithMany("Comments") + .HasForeignKey("CardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_card_comments_cards_card_id"); + + b.HasOne("Snapflow.Infrastructure.Auth.Entities.AppUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired() + .HasConstraintName("fk_card_comments_users_user_id"); + + b.Navigation("User"); + }); + modelBuilder.Entity("Snapflow.Domain.Lists.List", b => { b.HasOne("Snapflow.Domain.Boards.Board", "Board") @@ -1104,6 +1161,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Tags"); }); + modelBuilder.Entity("Snapflow.Domain.Cards.Card", b => + { + b.Navigation("Comments"); + }); + modelBuilder.Entity("Snapflow.Domain.Lists.List", b => { b.Navigation("Cards"); diff --git a/server/src/Presentation/Endpoints/Cards/Update.cs b/server/src/Presentation/Endpoints/Cards/Update.cs index 7bee44f..404a97a 100644 --- a/server/src/Presentation/Endpoints/Cards/Update.cs +++ b/server/src/Presentation/Endpoints/Cards/Update.cs @@ -8,7 +8,7 @@ namespace Snapflow.Presentation.Endpoints.Cards; internal sealed class Update : IEndpoint { - public sealed record UpdateCardRequest(string Title, string Description); + public sealed record UpdateCardRequest(string Title, string? Description); public void MapEndpoint(IEndpointRouteBuilder app) { diff --git a/server/src/Presentation/Hubs/Board/Hub.cs b/server/src/Presentation/Hubs/Board/Hub.cs index 8f0ff1a..60b0064 100644 --- a/server/src/Presentation/Hubs/Board/Hub.cs +++ b/server/src/Presentation/Hubs/Board/Hub.cs @@ -1,6 +1,15 @@ -using Microsoft.AspNetCore.Authorization; +using System.Globalization; +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.SignalR; +using Snapflow.Application.Abstractions.Messaging; +using Snapflow.Application.Boards.GetById; +using Snapflow.Application.Cards.AddComment; +using Snapflow.Common; using Snapflow.Domain.Boards; +using Snapflow.Presentation.Extensions; +using Microsoft.Extensions.DependencyInjection; namespace Snapflow.Presentation.Hubs.Board; @@ -17,13 +26,30 @@ public override async Task OnConnectedAsync() Context.Abort(); return; } - if (!httpContext.Request.RouteValues.TryGetValue("boardId", out var boardIdObj) || - !int.TryParse(boardIdObj?.ToString(), out var boardId)) + + int boardId = 0; + + if (httpContext.Request.RouteValues.TryGetValue("boardId", out var boardIdObj) && int.TryParse(boardIdObj?.ToString(), out var parsedRouteId)) + { + boardId = parsedRouteId; + } + else + { + var pathSegments = httpContext.Request.Path.Value?.Split('/'); + var idSegment = pathSegments?.FirstOrDefault(s => int.TryParse(s, out _)); + if (idSegment != null) + { + boardId = int.Parse(idSegment, CultureInfo.InvariantCulture); + } + } + + if (boardId == 0) { - logger.LogWarning("Connection {ConnectionId} aborted: invalid or missing boardId in route.", Context.ConnectionId); + logger.LogWarning("Connection {ConnectionId} aborted: could not find boardId in URL {Url}.", Context.ConnectionId, httpContext.Request.Path.Value); Context.Abort(); return; } + Context.SetBoardId(boardId); var userIdString = Context.UserIdentifier; await Groups.AddToGroupAsync(Context.ConnectionId, $"{boardId}", Context.ConnectionAborted); @@ -37,4 +63,34 @@ public override async Task OnConnectedAsync() if (logger.IsEnabled(LogLevel.Information)) logger.LogInformation("Connection {ConnectionId} connected to board {BoardId}.", Context.ConnectionId, boardId); } -} + + public async Task AddComment( + int cardId, + string content, + [FromServices] ICommandHandler handler) + { + // 1. Pobieranie ID użytkownika + if (!int.TryParse(Context.UserIdentifier, out int userId)) return; + + var command = new AddCardCommentCommand(cardId, userId, content); + + // 2. Używamy konkretnego handlera do obsłużenia komendy (wywołujemy .Handle zamiast .Send) + var result = await handler.Handle(command, default); + + // 3. Sprawdzamy wynik + if (result.IsSuccess && result.Value is not null) + { + var httpContext = Context.GetHttpContext(); + + if (httpContext?.Request.RouteValues.TryGetValue("boardId", out var boardIdObj) == true && boardIdObj is not null) + { + await Clients.Group(boardIdObj.ToString()!).CommentAdded(cardId, result.Value); + } + } + else + { + logger.LogWarning("Failed to add comment to card {CardId}. Error: {Error}", cardId, result.Error); + } + } +} +public sealed record AddCardCommentRequest(int CardId, string Content); \ No newline at end of file diff --git a/server/src/Presentation/Hubs/Board/IBoardHubClient.cs b/server/src/Presentation/Hubs/Board/IBoardHubClient.cs index 1cf8db1..26a9dc7 100644 --- a/server/src/Presentation/Hubs/Board/IBoardHubClient.cs +++ b/server/src/Presentation/Hubs/Board/IBoardHubClient.cs @@ -1,10 +1,11 @@ -using Snapflow.Domain.Members; - +using Snapflow.Application.Boards.GetById; +using Snapflow.Application.Cards.AddComment; +using Snapflow.Domain.Members; namespace Snapflow.Presentation.Hubs.Board; public interface IBoardHubClient { - public sealed record BoardUpdatedPayload(string Title, string Description); + public sealed record BoardUpdatedPayload(string Title, string? Description); Task BoardUpdated(BoardUpdatedPayload payload, CancellationToken cancellationToken = default); @@ -46,11 +47,11 @@ public sealed record ListDeletedPayload(int Id); Task CardUnlocked(); - public sealed record CardCreatedPayload(int Id, int ListId, string Title, string Description, string Rank); + public sealed record CardCreatedPayload(int Id, int ListId, string Title, string? Description, string Rank); Task CardCreated(CardCreatedPayload payload, CancellationToken cancellationToken = default); - - public sealed record CardUpdatedPayload(int Id, string Title, string Description); + Task CommentAdded(int cardId, GetBoardByIdResponse.CardCommentDto comment); + public sealed record CardUpdatedPayload(int Id, string Title, string? Description); Task CardUpdated(CardUpdatedPayload payload, CancellationToken cancellationToken = default); diff --git a/server/src/Presentation/Presentation.csproj b/server/src/Presentation/Presentation.csproj index cdb0333..d8c6d9e 100644 --- a/server/src/Presentation/Presentation.csproj +++ b/server/src/Presentation/Presentation.csproj @@ -7,6 +7,7 @@ + diff --git a/web/src/lib/features/boards/api/boards.api.ts b/web/src/lib/features/boards/api/boards.api.ts index 193b02a..00c738a 100644 --- a/web/src/lib/features/boards/api/boards.api.ts +++ b/web/src/lib/features/boards/api/boards.api.ts @@ -136,4 +136,13 @@ export class BoardsService { }) ); } + addComment(cardId: number, content: string): Promise> { + return this.handleResponse( + this.apiClient.fetch(`cards/${cardId}/comments`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ content }) + }) + ); + } } diff --git a/web/src/lib/features/boards/components/BoardCard.svelte b/web/src/lib/features/boards/components/BoardCard.svelte index 3ed5fe7..e9beffc 100644 --- a/web/src/lib/features/boards/components/BoardCard.svelte +++ b/web/src/lib/features/boards/components/BoardCard.svelte @@ -18,31 +18,34 @@
-
-

{title}

+ + +
+

+ {title} +

+ {#if editHref && yourRole?.toLowerCase() === 'owner'} -
+
+ +
- {#if card.description} -

- {card.description} -

- {/if} - -
-
+ +
+ +
{card.createdBy.userName.charAt(0).toUpperCase()}
+ + + {#if card.comments && card.comments.length > 0} +
+ + {card.comments.length} +
+ {/if}
-
- - {new Date(card.createdAt).toLocaleDateString()} + + +
+ + + {new Date(card.createdAt).toLocaleDateString(undefined, { month: 'short', day: 'numeric' })} +
-
+
\ No newline at end of file diff --git a/web/src/lib/features/boards/components/CardModal.svelte b/web/src/lib/features/boards/components/CardModal.svelte index 9359f0d..d5c7218 100644 --- a/web/src/lib/features/boards/components/CardModal.svelte +++ b/web/src/lib/features/boards/components/CardModal.svelte @@ -4,6 +4,7 @@ import type { GetBoardByIdResponse } from '$lib/features/boards/types/boards.api'; import type { Response } from '$lib/core/types/app'; import { createForm } from '$lib/ui/utils'; + let { open = $bindable(false), card = $bindable(undefined), @@ -16,7 +17,8 @@ mobileDrawerSide = 'bottom', triggerElement = undefined, onConfirm, - onDelete + onDelete, + onAddComment }: { open: boolean; card?: GetBoardByIdResponse.CardDto; @@ -30,10 +32,27 @@ triggerElement?: HTMLElement | null; onConfirm: (title: string, description: string) => Promise>; onDelete?: (id: number) => Promise; + onAddComment?: (content: string) => Promise; } = $props(); let isDeleting = $state(false); + let newComment = $state(''); + let isSendingComment = $state(false); + async function handleAddComment() { + if (!card || !newComment.trim() || isSendingComment || !onAddComment) return; + + isSendingComment = true; + try { + await onAddComment(newComment); + + newComment = ''; + } catch (error) { + console.error("Failed to add comment:", error); + } finally { + isSendingComment = false; + } + } const form = createForm({ initialValues: { title: '', @@ -103,7 +122,7 @@ {desktopAnimation} {mobileAnimation} {triggerElement} - contentClass="sm:rounded-lg md:w-full" + contentClass="sm:rounded-lg md:w-full hide-scrollbar" >
+ + {#if card} +
+

Comments

+ + +
+ {#if card.comments && card.comments.length > 0} + {#each card.comments as comment} +
+
+ {comment.userName} + {new Date(comment.createdAt).toLocaleString()} +
+

{comment.content}

+
+ {/each} + {:else} +

No comments yet. Be the first!

+ {/if} +
+ + +
+