add checklists
This commit is contained in:
parent
799e089deb
commit
d295a00e2f
8 changed files with 556 additions and 856 deletions
|
|
@ -1,514 +0,0 @@
|
|||
# Kanban API Documentation
|
||||
|
||||
This document describes all the API endpoints for the Kanban application. All endpoints require authentication unless otherwise noted.
|
||||
|
||||
## Base URL
|
||||
```
|
||||
http://localhost:5000/api
|
||||
```
|
||||
|
||||
## Authentication
|
||||
|
||||
All endpoints (except register and login) require a JWT token in the Authorization header:
|
||||
```
|
||||
Authorization: Bearer <your_jwt_token>
|
||||
```
|
||||
|
||||
## Boards
|
||||
|
||||
### Get All Boards
|
||||
```
|
||||
GET /api/boards
|
||||
```
|
||||
Get all boards for the current authenticated user.
|
||||
|
||||
**Response:** Array of board objects
|
||||
|
||||
### Get Single Board
|
||||
```
|
||||
GET /api/boards/<board_id>
|
||||
```
|
||||
Get a board with all its details including lists, cards, and labels.
|
||||
|
||||
**Response:** Board object with nested lists, cards, and labels
|
||||
|
||||
### Create Board
|
||||
```
|
||||
POST /api/boards
|
||||
```
|
||||
Create a new board.
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"name": "My Project Board",
|
||||
"description": "Project management board",
|
||||
"url": "https://example.com/board/123",
|
||||
"short_link": "abc123",
|
||||
"short_url": "https://example.com/b/abc123",
|
||||
"prefs": {},
|
||||
"label_names": {},
|
||||
"limits": {}
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** Created board object
|
||||
|
||||
### Update Board
|
||||
```
|
||||
PUT /api/boards/<board_id>
|
||||
```
|
||||
Update a board.
|
||||
|
||||
**Request Body:** Partial board object (only include fields to update)
|
||||
|
||||
**Response:** Updated board object
|
||||
|
||||
### Delete Board
|
||||
```
|
||||
DELETE /api/boards/<board_id>
|
||||
```
|
||||
Delete a board and all its associated data.
|
||||
|
||||
**Response:** Success message
|
||||
|
||||
## Lists
|
||||
|
||||
### Create List
|
||||
```
|
||||
POST /api/boards/<board_id>/lists
|
||||
```
|
||||
Create a new list in a board.
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"name": "To Do",
|
||||
"pos": 0
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** Created list object
|
||||
|
||||
### Update List
|
||||
```
|
||||
PUT /api/lists/<list_id>
|
||||
```
|
||||
Update a list.
|
||||
|
||||
**Request Body:** Partial list object
|
||||
|
||||
**Response:** Updated list object
|
||||
|
||||
### Delete List
|
||||
```
|
||||
DELETE /api/lists/<list_id>
|
||||
```
|
||||
Delete a list and all its cards.
|
||||
|
||||
**Response:** Success message
|
||||
|
||||
## Cards
|
||||
|
||||
### Create Card
|
||||
```
|
||||
POST /api/lists/<list_id>/cards
|
||||
```
|
||||
Create a new card in a list.
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"name": "Task Name",
|
||||
"description": "Task description",
|
||||
"pos": 0,
|
||||
"due": "2026-12-31T23:59:59Z",
|
||||
"due_complete": false,
|
||||
"badges": {},
|
||||
"cover": {},
|
||||
"desc_data": {}
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** Created card object
|
||||
|
||||
### Get Single Card
|
||||
```
|
||||
GET /api/cards/<card_id>
|
||||
```
|
||||
Get a card with full details including labels, checklists, and comments.
|
||||
|
||||
**Response:** Card object with nested labels, checklists, items, and comments
|
||||
|
||||
### Update Card
|
||||
```
|
||||
PUT /api/cards/<card_id>
|
||||
```
|
||||
Update a card. Can also move card to different list by providing `list_id`.
|
||||
|
||||
**Request Body:** Partial card object
|
||||
|
||||
**Response:** Updated card object
|
||||
|
||||
### Delete Card
|
||||
```
|
||||
DELETE /api/cards/<card_id>
|
||||
```
|
||||
Delete a card and all its associated data.
|
||||
|
||||
**Response:** Success message
|
||||
|
||||
## Labels
|
||||
|
||||
### Get Board Labels
|
||||
```
|
||||
GET /api/boards/<board_id>/labels
|
||||
```
|
||||
Get all labels for a board.
|
||||
|
||||
**Response:** Array of label objects
|
||||
|
||||
### Create Label
|
||||
```
|
||||
POST /api/boards/<board_id>/labels
|
||||
```
|
||||
Create a new label in a board.
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"name": "Urgent",
|
||||
"color": "red"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** Created label object
|
||||
|
||||
### Add Label to Card
|
||||
```
|
||||
POST /api/cards/<card_id>/labels
|
||||
```
|
||||
Add a label to a card.
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"label_id": 1
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** Created card-label association object
|
||||
|
||||
### Remove Label from Card
|
||||
```
|
||||
DELETE /api/cards/<card_id>/labels/<label_id>
|
||||
```
|
||||
Remove a label from a card.
|
||||
|
||||
**Response:** Success message
|
||||
|
||||
## Checklists
|
||||
|
||||
### Create Checklist
|
||||
```
|
||||
POST /api/cards/<card_id>/checklists
|
||||
```
|
||||
Create a new checklist in a card.
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"name": "Tasks",
|
||||
"pos": 0
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** Created checklist object
|
||||
|
||||
### Create Check Item
|
||||
```
|
||||
POST /api/checklists/<checklist_id>/items
|
||||
```
|
||||
Create a new check item in a checklist.
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"name": "Complete task",
|
||||
"pos": 0,
|
||||
"state": "incomplete",
|
||||
"due": "2026-12-31T23:59:59Z"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** Created check item object
|
||||
|
||||
### Update Check Item
|
||||
```
|
||||
PUT /api/check-items/<item_id>
|
||||
```
|
||||
Update a check item (typically used to toggle state).
|
||||
|
||||
**Request Body:** Partial check item object
|
||||
|
||||
**Response:** Updated check item object
|
||||
|
||||
### Delete Checklist
|
||||
```
|
||||
DELETE /api/checklists/<checklist_id>
|
||||
```
|
||||
Delete a checklist and all its items.
|
||||
|
||||
**Response:** Success message
|
||||
|
||||
### Delete Check Item
|
||||
```
|
||||
DELETE /api/check-items/<item_id>
|
||||
```
|
||||
Delete a check item.
|
||||
|
||||
**Response:** Success message
|
||||
|
||||
## Comments
|
||||
|
||||
### Get Card Comments
|
||||
```
|
||||
GET /api/cards/<card_id>/comments
|
||||
```
|
||||
Get all comments for a card, ordered by creation date (newest first).
|
||||
|
||||
**Response:** Array of comment objects with user information
|
||||
|
||||
### Create Comment
|
||||
```
|
||||
POST /api/cards/<card_id>/comments
|
||||
```
|
||||
Create a new comment on a card.
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"text": "This is a comment"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** Created comment object
|
||||
|
||||
### Update Comment
|
||||
```
|
||||
PUT /api/comments/<comment_id>
|
||||
```
|
||||
Update a comment (only by the comment author).
|
||||
|
||||
**Request Body:** Partial comment object
|
||||
|
||||
**Response:** Updated comment object
|
||||
|
||||
### Delete Comment
|
||||
```
|
||||
DELETE /api/comments/<comment_id>
|
||||
```
|
||||
Delete a comment (only by the comment author).
|
||||
|
||||
**Response:** Success message
|
||||
|
||||
## Data Models
|
||||
|
||||
### Board
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"name": "My Board",
|
||||
"description": "Board description",
|
||||
"closed": false,
|
||||
"url": "https://example.com",
|
||||
"short_link": "abc123",
|
||||
"short_url": "https://example.com/b/abc123",
|
||||
"user_id": 1,
|
||||
"date_last_activity": "2026-01-01T00:00:00Z",
|
||||
"date_last_view": "2026-01-01T00:00:00Z",
|
||||
"created_at": "2026-01-01T00:00:00Z",
|
||||
"updated_at": "2026-01-01T00:00:00Z",
|
||||
"prefs": {},
|
||||
"label_names": {},
|
||||
"limits": {}
|
||||
}
|
||||
```
|
||||
|
||||
### List
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"name": "To Do",
|
||||
"closed": false,
|
||||
"pos": 0,
|
||||
"board_id": 1,
|
||||
"created_at": "2026-01-01T00:00:00Z",
|
||||
"updated_at": "2026-01-01T00:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### Card
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Task Name",
|
||||
"description": "Task description",
|
||||
"closed": false,
|
||||
"due": "2026-12-31T23:59:59Z",
|
||||
"due_complete": false,
|
||||
"pos": 0,
|
||||
"id_short": 1,
|
||||
"board_id": 1,
|
||||
"list_id": 1,
|
||||
"date_last_activity": "2026-01-01T00:00:00Z",
|
||||
"created_at": "2026-01-01T00:00:00Z",
|
||||
"updated_at": "2026-01-01T00:00:00Z",
|
||||
"badges": {},
|
||||
"cover": {},
|
||||
"desc_data": {},
|
||||
"labels": [],
|
||||
"checklists": [],
|
||||
"comments": []
|
||||
}
|
||||
```
|
||||
|
||||
### Label
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Urgent",
|
||||
"color": "red",
|
||||
"uses": 0,
|
||||
"board_id": 1,
|
||||
"created_at": "2026-01-01T00:00:00Z",
|
||||
"updated_at": "2026-01-01T00:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### Checklist
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Tasks",
|
||||
"pos": 0,
|
||||
"board_id": 1,
|
||||
"card_id": 1,
|
||||
"created_at": "2026-01-01T00:00:00Z",
|
||||
"updated_at": "2026-01-01T00:00:00Z",
|
||||
"items": []
|
||||
}
|
||||
```
|
||||
|
||||
### Check Item
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Complete task",
|
||||
"pos": 0,
|
||||
"state": "incomplete",
|
||||
"due": "2026-12-31T23:59:59Z",
|
||||
"checklist_id": 1,
|
||||
"user_id": 1,
|
||||
"created_at": "2026-01-01T00:00:00Z",
|
||||
"updated_at": "2026-01-01T00:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### Comment
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"text": "Comment text",
|
||||
"card_id": 1,
|
||||
"user_id": 1,
|
||||
"created_at": "2026-01-01T00:00:00Z",
|
||||
"updated_at": "2026-01-01T00:00:00Z",
|
||||
"user": {}
|
||||
}
|
||||
```
|
||||
|
||||
## Common Response Codes
|
||||
|
||||
- `200 OK` - Request successful
|
||||
- `201 Created` - Resource created successfully
|
||||
- `400 Bad Request` - Invalid request data
|
||||
- `401 Unauthorized` - Authentication required or invalid
|
||||
- `403 Forbidden` - Access denied (not your resource)
|
||||
- `404 Not Found` - Resource not found
|
||||
- `500 Internal Server Error` - Server error
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Creating a complete board with lists and cards
|
||||
|
||||
```bash
|
||||
# 1. Login to get token
|
||||
curl -X POST http://localhost:5000/api/auth/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"email": "user@example.com", "password": "password"}'
|
||||
|
||||
# 2. Create a board
|
||||
curl -X POST http://localhost:5000/api/boards \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer <token>" \
|
||||
-d '{"name": "My Project"}'
|
||||
|
||||
# 3. Create a list
|
||||
curl -X POST http://localhost:5000/api/boards/1/lists \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer <token>" \
|
||||
-d '{"name": "To Do", "pos": 0}'
|
||||
|
||||
# 4. Create a card
|
||||
curl -X POST http://localhost:5000/api/lists/1/cards \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer <token>" \
|
||||
-d '{"name": "Task 1", "pos": 0}'
|
||||
```
|
||||
|
||||
### Adding labels to a card
|
||||
|
||||
```bash
|
||||
# 1. Create a label
|
||||
curl -X POST http://localhost:5000/api/boards/1/labels \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer <token>" \
|
||||
-d '{"name": "Urgent", "color": "red"}'
|
||||
|
||||
# 2. Add label to card
|
||||
curl -X POST http://localhost:5000/api/cards/1/labels \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer <token>" \
|
||||
-d '{"label_id": 1}'
|
||||
```
|
||||
|
||||
### Creating a checklist with items
|
||||
|
||||
```bash
|
||||
# 1. Create checklist
|
||||
curl -X POST http://localhost:5000/api/cards/1/checklists \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer <token>" \
|
||||
-d '{"name": "Subtasks", "pos": 0}'
|
||||
|
||||
# 2. Add check items
|
||||
curl -X POST http://localhost:5000/api/checklists/1/items \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer <token>" \
|
||||
-d '{"name": "Task 1", "pos": 0}'
|
||||
|
||||
# 3. Mark item as complete
|
||||
curl -X PUT http://localhost:5000/api/check-items/1 \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer <token>" \
|
||||
-d '{"state": "complete"}'
|
||||
```
|
||||
|
||||
### Adding comments
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:5000/api/cards/1/comments \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer <token>" \
|
||||
-d '{"text": "This is a comment"}'
|
||||
|
|
@ -1,300 +0,0 @@
|
|||
# Kanban Backend Implementation Summary
|
||||
|
||||
## Overview
|
||||
This document summarizes the Kanban backend implementation for the project. The backend provides a complete API for managing Kanban boards, lists, cards, labels, checklists, and comments.
|
||||
|
||||
## Database Schema
|
||||
|
||||
### Tables Created
|
||||
|
||||
1. **boards** - Main board entities
|
||||
- id, name, description, closed, url, short_link, short_url
|
||||
- user_id (foreign key to users)
|
||||
- date_last_activity, date_last_view, created_at, updated_at
|
||||
- JSONB fields: prefs, label_names, limits
|
||||
- Relationships: lists, cards, labels
|
||||
|
||||
2. **lists** - Board columns/lists
|
||||
- id, name, closed, pos
|
||||
- board_id (foreign key to boards)
|
||||
- created_at, updated_at
|
||||
- Relationships: cards
|
||||
|
||||
3. **cards** - Task cards within lists
|
||||
- id, name, description, closed, due, due_complete, pos, id_short
|
||||
- board_id (foreign key to boards), list_id (foreign key to lists)
|
||||
- date_last_activity, created_at, updated_at
|
||||
- JSONB fields: badges, cover, desc_data
|
||||
- Relationships: checklists, labels, comments
|
||||
|
||||
4. **labels** - Color-coded tags
|
||||
- id, name, color, uses
|
||||
- board_id (foreign key to boards)
|
||||
- created_at, updated_at
|
||||
- Relationships: card_labels
|
||||
|
||||
5. **card_labels** - Many-to-many relationship between cards and labels
|
||||
- id, card_id, label_id
|
||||
- Unique constraint on (card_id, label_id)
|
||||
|
||||
6. **checklists** - Checklists within cards
|
||||
- id, name, pos
|
||||
- board_id (foreign key to boards), card_id (foreign key to cards)
|
||||
- created_at, updated_at
|
||||
- Relationships: check_items
|
||||
|
||||
7. **check_items** - Items within checklists
|
||||
- id, name, pos, state, due
|
||||
- checklist_id (foreign key to checklists), user_id (foreign key to users)
|
||||
- created_at, updated_at
|
||||
|
||||
8. **comments** - Comments on cards
|
||||
- id, text
|
||||
- card_id (foreign key to cards), user_id (foreign key to users)
|
||||
- created_at, updated_at
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Boards (`/api/boards`)
|
||||
- `GET /api/boards` - Get all boards for current user
|
||||
- `GET /api/boards/<id>` - Get board with full details (lists, cards, labels)
|
||||
- `POST /api/boards` - Create new board
|
||||
- `PUT /api/boards/<id>` - Update board
|
||||
- `DELETE /api/boards/<id>` - Delete board
|
||||
|
||||
### Lists (`/api/lists`, `/api/boards/<id>/lists`)
|
||||
- `POST /api/boards/<board_id>/lists` - Create list in board
|
||||
- `PUT /api/lists/<id>` - Update list
|
||||
- `DELETE /api/lists/<id>` - Delete list
|
||||
|
||||
### Cards (`/api/cards`, `/api/lists/<id>/cards`)
|
||||
- `POST /api/lists/<list_id>/cards` - Create card in list
|
||||
- `GET /api/cards/<id>` - Get card with full details
|
||||
- `PUT /api/cards/<id>` - Update card (includes moving between lists)
|
||||
- `DELETE /api/cards/<id>` - Delete card
|
||||
|
||||
### Labels (`/api/boards/<id>/labels`, `/api/cards/<id>/labels`)
|
||||
- `GET /api/boards/<board_id>/labels` - Get all labels for board
|
||||
- `POST /api/boards/<board_id>/labels` - Create label
|
||||
- `POST /api/cards/<card_id>/labels` - Add label to card
|
||||
- `DELETE /api/cards/<card_id>/labels/<label_id>` - Remove label from card
|
||||
|
||||
### Checklists (`/api/checklists`, `/api/cards/<id>/checklists`)
|
||||
- `POST /api/cards/<card_id>/checklists` - Create checklist
|
||||
- `DELETE /api/checklists/<id>` - Delete checklist
|
||||
|
||||
### Check Items (`/api/check-items`, `/api/checklists/<id>/items`)
|
||||
- `POST /api/checklists/<checklist_id>/items` - Create check item
|
||||
- `PUT /api/check-items/<id>` - Update check item
|
||||
- `DELETE /api/check-items/<id>` - Delete check item
|
||||
|
||||
### Comments (`/api/comments`, `/api/cards/<id>/comments`)
|
||||
- `GET /api/cards/<card_id>/comments` - Get all comments for card
|
||||
- `POST /api/cards/<card_id>/comments` - Create comment
|
||||
- `PUT /api/comments/<id>` - Update comment
|
||||
- `DELETE /api/comments/<id>` - Delete comment
|
||||
|
||||
## Key Features
|
||||
|
||||
### Authentication
|
||||
- All endpoints (except register/login) require JWT authentication
|
||||
- User can only access their own boards and related resources
|
||||
- Comments can only be edited/deleted by their author
|
||||
|
||||
### Data Integrity
|
||||
- Cascading deletes ensure cleanup of related data
|
||||
- Foreign key constraints maintain referential integrity
|
||||
- Unique constraints prevent duplicate card-label associations
|
||||
|
||||
### Position Tracking
|
||||
- All orderable entities (lists, cards, checklists, check items) have `pos` field
|
||||
- Enables flexible sorting and drag-and-drop functionality
|
||||
|
||||
### Activity Tracking
|
||||
- Boards track `date_last_activity` and `date_last_view`
|
||||
- Cards track `date_last_activity`
|
||||
- Useful for showing recent activity
|
||||
|
||||
### JSONB Fields
|
||||
- Flexible storage for complex data (prefs, limits, badges, cover, desc_data)
|
||||
- Allows extensibility without schema changes
|
||||
- Supports Trello-like feature parity
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
backend/app/
|
||||
├── models/
|
||||
│ ├── board.py # Board model
|
||||
│ ├── list_model.py # List model (named to avoid Python conflict)
|
||||
│ ├── card.py # Card model
|
||||
│ ├── label.py # Label model
|
||||
│ ├── card_label.py # Card-Label junction table
|
||||
│ ├── checklist.py # Checklist model
|
||||
│ ├── check_item.py # CheckItem model
|
||||
│ ├── comment.py # Comment model
|
||||
│ └── user.py # Updated with boards relationship
|
||||
├── routes/
|
||||
│ └── kanban.py # All Kanban API routes
|
||||
└── __init__.py # Updated to import models and register blueprint
|
||||
|
||||
docs/
|
||||
├── kanban_api.md # Complete API documentation
|
||||
└── kanban_implementation_summary.md # This file
|
||||
```
|
||||
|
||||
## Migration
|
||||
|
||||
Migration file: `backend/migrations/versions/1c0b9dfbd933_add_kanban_models_board_list_card_label_.py`
|
||||
|
||||
To apply migrations:
|
||||
```bash
|
||||
cd backend && . venv/bin/activate && flask db upgrade
|
||||
```
|
||||
|
||||
## Usage Flow
|
||||
|
||||
### Typical User Workflow
|
||||
|
||||
1. **Register/Login**
|
||||
- User registers account or logs in
|
||||
- Receives JWT token for authentication
|
||||
|
||||
2. **Create Board**
|
||||
- User creates a new board
|
||||
- Board is associated with their user ID
|
||||
|
||||
3. **Add Lists**
|
||||
- User adds lists (columns) to the board
|
||||
- Examples: "To Do", "In Progress", "Done"
|
||||
|
||||
4. **Add Cards**
|
||||
- User creates cards within lists
|
||||
- Cards can have descriptions, due dates, etc.
|
||||
|
||||
5. **Enhance Cards**
|
||||
- Add labels for categorization
|
||||
- Add checklists for subtasks
|
||||
- Add comments for collaboration
|
||||
|
||||
6. **Manage Work**
|
||||
- Move cards between lists (drag-and-drop)
|
||||
- Update card details
|
||||
- Mark checklist items as complete
|
||||
- Delete completed items
|
||||
|
||||
## Design Decisions
|
||||
|
||||
### Single User App
|
||||
- No organization or membership models (as specified)
|
||||
- Each board belongs to exactly one user
|
||||
- Simplifies permissions model
|
||||
|
||||
### Trello-Inspired Schema
|
||||
- Uses similar field names and structure as Trello
|
||||
- Makes it familiar to users
|
||||
- Supports importing from Trello JSON exports
|
||||
|
||||
### Position-Based Ordering
|
||||
- Uses float `pos` field for ordering
|
||||
- Allows inserting items between others
|
||||
- Supports infinite granularity for drag-and-drop
|
||||
|
||||
### Cascading Deletes
|
||||
- Deleting a board deletes all its lists, cards, labels, etc.
|
||||
- Deleting a list deletes all its cards
|
||||
- Ensures no orphaned data
|
||||
|
||||
### JSONB for Flexible Data
|
||||
- Stores complex nested data without normalized tables
|
||||
- Simplifies schema for optional features
|
||||
- Maintains flexibility for future enhancements
|
||||
|
||||
## Next Steps
|
||||
|
||||
### Backend
|
||||
- [ ] Add validation schemas (Pydantic)
|
||||
- [ ] Add comprehensive tests
|
||||
- [ ] Add rate limiting
|
||||
- [ ] Add file upload support for attachments
|
||||
- [ ] Add activity logging/history
|
||||
- [ ] Add search functionality
|
||||
- [ ] Add filtering and sorting options
|
||||
|
||||
### Frontend
|
||||
- [ ] Create board list view
|
||||
- [ ] Create board detail view with drag-and-drop
|
||||
- [ ] Implement card creation/editing
|
||||
- [ ] Implement label management
|
||||
- [ ] Implement checklist functionality
|
||||
- [ ] Implement comments
|
||||
- [ ] Add real-time updates (WebSocket)
|
||||
|
||||
## Testing
|
||||
|
||||
Test the API using the examples in `docs/kanban_api.md` or use tools like:
|
||||
- Postman
|
||||
- Insomnia
|
||||
- curl (command line)
|
||||
|
||||
Example:
|
||||
```bash
|
||||
# Start the backend server
|
||||
make dev-services # Start postgres & redis
|
||||
make dev-backend # Start Flask server
|
||||
|
||||
# Test endpoints
|
||||
curl http://localhost:5000/api/health
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- JWT authentication required for all operations
|
||||
- User isolation: users can only access their own resources
|
||||
- SQL injection prevention through SQLAlchemy ORM
|
||||
- Input validation on all endpoints
|
||||
- CORS configured for frontend integration
|
||||
|
||||
## Performance Notes
|
||||
|
||||
- Indexed fields: user_id, board_id, list_id, card_id, etc.
|
||||
- Lazy loading relationships to avoid N+1 queries
|
||||
- Efficient queries using SQLAlchemy's query builder
|
||||
- JSONB fields use PostgreSQL's optimized JSON storage
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Migration Issues
|
||||
```bash
|
||||
# Reset migrations (WARNING: deletes data)
|
||||
cd backend && . venv/bin/activate && flask db downgrade base
|
||||
rm -rf migrations/versions/*.py
|
||||
flask db migrate -m "Initial migration"
|
||||
flask db upgrade
|
||||
```
|
||||
|
||||
### Database Connection Issues
|
||||
```bash
|
||||
# Restart services
|
||||
make dev-stop-services
|
||||
make dev-services
|
||||
```
|
||||
|
||||
### Import Errors
|
||||
```bash
|
||||
# Ensure venv is activated
|
||||
cd backend && . venv/bin/activate
|
||||
pip install -r requirements/dev.txt
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
The Kanban backend is now fully implemented with:
|
||||
- ✅ Complete database schema
|
||||
- ✅ RESTful API endpoints
|
||||
- ✅ Authentication and authorization
|
||||
- ✅ Data integrity and validation
|
||||
- ✅ Comprehensive documentation
|
||||
- ✅ Database migration
|
||||
|
||||
The backend is ready for frontend integration and testing.
|
||||
216
frontend/src/components/CardChecklists.tsx
Normal file
216
frontend/src/components/CardChecklists.tsx
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
import CheckSquareIcon from './icons/CheckSquareIcon';
|
||||
import Trash2Icon from './icons/Trash2Icon';
|
||||
import Edit2Icon from './icons/Edit2Icon';
|
||||
import PlusIcon from './icons/PlusIcon';
|
||||
import { useModal } from '../context/modals/useModal';
|
||||
import { CreateChecklistModal } from './CreateChecklistModal';
|
||||
import { DeleteChecklistModal } from './DeleteChecklistModal';
|
||||
import { EditCheckItemModal } from './EditCheckItemModal';
|
||||
import { CheckItem } from '../types/kanban';
|
||||
|
||||
interface CardChecklistsProps {
|
||||
checklists: any[];
|
||||
cardId: number;
|
||||
addChecklist: (name: string, pos: number) => Promise<boolean>;
|
||||
removeChecklist: (id: number) => Promise<boolean>;
|
||||
addCheckItem: (
|
||||
checklistId: number,
|
||||
name: string,
|
||||
pos: number,
|
||||
state?: 'incomplete' | 'complete'
|
||||
) => Promise<boolean>;
|
||||
toggleCheckItem: (item: CheckItem, currentState: 'incomplete' | 'complete') => Promise<boolean>;
|
||||
editCheckItem: (
|
||||
itemId: number,
|
||||
name: string,
|
||||
pos: number,
|
||||
state: 'incomplete' | 'complete'
|
||||
) => Promise<boolean>;
|
||||
removeCheckItem: (itemId: number) => Promise<boolean>;
|
||||
}
|
||||
|
||||
export function CardChecklists({
|
||||
checklists,
|
||||
addChecklist,
|
||||
removeChecklist,
|
||||
addCheckItem,
|
||||
toggleCheckItem,
|
||||
editCheckItem,
|
||||
removeCheckItem,
|
||||
}: CardChecklistsProps) {
|
||||
const { openModal } = useModal();
|
||||
|
||||
const handleAddChecklist = () => {
|
||||
openModal((props) => (
|
||||
<CreateChecklistModal
|
||||
onSave={async (name) => {
|
||||
const checklistCount = checklists?.length || 0;
|
||||
return await addChecklist(name, checklistCount);
|
||||
}}
|
||||
onClose={props.onClose}
|
||||
/>
|
||||
));
|
||||
};
|
||||
|
||||
const handleDeleteChecklist = (checklist: any) => {
|
||||
openModal((props) => (
|
||||
<DeleteChecklistModal
|
||||
checklistName={checklist.name}
|
||||
onDelete={async () => {
|
||||
const success = await removeChecklist(checklist.id);
|
||||
if (success) {
|
||||
props.onClose();
|
||||
}
|
||||
}}
|
||||
onClose={props.onClose}
|
||||
/>
|
||||
));
|
||||
};
|
||||
|
||||
const handleAddCheckItem = (checklist: any) => {
|
||||
const itemName = prompt('Enter item name:');
|
||||
if (itemName && itemName.trim()) {
|
||||
const itemCount = checklist.items?.length || 0;
|
||||
addCheckItem(checklist.id, itemName.trim(), itemCount);
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleCheckItem = async (item: any) => {
|
||||
await toggleCheckItem(item, item.state);
|
||||
};
|
||||
|
||||
const handleEditCheckItem = (item: any) => {
|
||||
openModal((props) => (
|
||||
<EditCheckItemModal
|
||||
itemName={item.name}
|
||||
onSave={async (name) => {
|
||||
return await editCheckItem(item.id, name, item.pos, item.state);
|
||||
}}
|
||||
onClose={props.onClose}
|
||||
/>
|
||||
));
|
||||
};
|
||||
|
||||
const handleDeleteCheckItem = (item: any) => {
|
||||
if (confirm('Are you sure you want to delete this item?')) {
|
||||
removeCheckItem(item.id);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className="bg-gray-800 rounded-lg p-6">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-xl font-bold text-white flex items-center gap-2">
|
||||
<span className="w-5 h-5">
|
||||
<CheckSquareIcon />
|
||||
</span>
|
||||
Checklists
|
||||
</h2>
|
||||
<button
|
||||
onClick={handleAddChecklist}
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium py-1.5 px-3 rounded-lg transition-colors flex items-center gap-1"
|
||||
>
|
||||
<span className="w-4 h-4">
|
||||
<PlusIcon />
|
||||
</span>
|
||||
Add Checklist
|
||||
</button>
|
||||
</div>
|
||||
{checklists && checklists.length > 0 ? (
|
||||
<div className="space-y-4">
|
||||
{checklists.map((checklist: any) => {
|
||||
const completedCount =
|
||||
checklist.items?.filter((item: any) => item.state === 'complete').length || 0;
|
||||
const totalCount = checklist.items?.length || 0;
|
||||
const progress = totalCount > 0 ? (completedCount / totalCount) * 100 : 0;
|
||||
|
||||
return (
|
||||
<div key={checklist.id} className="border border-gray-700 rounded-lg p-4">
|
||||
<div className="flex justify-between items-start mb-3">
|
||||
<div className="flex-1">
|
||||
<h3 className="font-semibold text-white mb-1">{checklist.name}</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1 h-2 bg-gray-700 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-green-500 transition-all duration-300"
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xs text-gray-400 whitespace-nowrap">
|
||||
{completedCount}/{totalCount}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleDeleteChecklist(checklist)}
|
||||
className="text-gray-400 hover:text-red-400 transition-colors ml-2"
|
||||
title="Delete checklist"
|
||||
>
|
||||
<span className="w-4 h-4">
|
||||
<Trash2Icon />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{checklist.items && checklist.items.length > 0 ? (
|
||||
checklist.items.map((item: any) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="flex items-center gap-3 p-2 bg-gray-700 rounded group hover:bg-gray-600 transition-colors"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={item.state === 'complete'}
|
||||
onChange={() => handleToggleCheckItem(item)}
|
||||
className="w-5 h-5 rounded cursor-pointer"
|
||||
/>
|
||||
<span
|
||||
onClick={() => handleToggleCheckItem(item)}
|
||||
className={`flex-1 text-sm cursor-pointer ${item.state === 'complete' ? 'text-gray-400 line-through' : 'text-white'}`}
|
||||
>
|
||||
{item.name}
|
||||
</span>
|
||||
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<button
|
||||
onClick={() => handleEditCheckItem(item)}
|
||||
className="text-gray-400 hover:text-white transition-colors p-1"
|
||||
title="Edit item"
|
||||
>
|
||||
<span className="w-3.5 h-3.5">
|
||||
<Edit2Icon />
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDeleteCheckItem(item)}
|
||||
className="text-gray-400 hover:text-red-400 transition-colors p-1"
|
||||
title="Delete item"
|
||||
>
|
||||
<span className="w-3.5 h-3.5">
|
||||
<Trash2Icon />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<p className="text-gray-400 text-sm">No items yet</p>
|
||||
)}
|
||||
<button
|
||||
onClick={() => handleAddCheckItem(checklist)}
|
||||
className="text-blue-400 hover:text-blue-300 text-sm font-medium mt-2 flex items-center gap-1"
|
||||
>
|
||||
<span className="w-4 h-4">
|
||||
<PlusIcon />
|
||||
</span>
|
||||
Add an item
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-gray-400 text-sm">No checklists yet. Add one to get started!</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
55
frontend/src/components/CreateChecklistModal.tsx
Normal file
55
frontend/src/components/CreateChecklistModal.tsx
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import { useState } from 'react';
|
||||
|
||||
interface CreateChecklistModalProps {
|
||||
onSave: (name: string) => Promise<boolean>;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function CreateChecklistModal({ onSave, onClose }: CreateChecklistModalProps) {
|
||||
const [name, setName] = useState('');
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!name.trim()) return;
|
||||
|
||||
const success = await onSave(name);
|
||||
if (success) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-gray-800 rounded-lg p-6 max-w-md w-full">
|
||||
<h3 className="text-xl font-bold text-white mb-4">Add Checklist</h3>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-gray-300 text-sm font-medium mb-2">Title</label>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="w-full bg-gray-700 text-white rounded-lg p-3 border border-gray-600 focus:border-blue-500 focus:outline-none"
|
||||
placeholder="Checklist title..."
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2 pt-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="flex-1 bg-gray-700 hover:bg-gray-600 text-white font-medium py-2 px-4 rounded-lg transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!name.trim()}
|
||||
className="flex-1 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-600 disabled:cursor-not-allowed text-white font-medium py-2 px-4 rounded-lg transition-colors"
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
45
frontend/src/components/DeleteChecklistModal.tsx
Normal file
45
frontend/src/components/DeleteChecklistModal.tsx
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import Trash2Icon from './icons/Trash2Icon';
|
||||
|
||||
interface DeleteChecklistModalProps {
|
||||
checklistName: string;
|
||||
onDelete: () => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function DeleteChecklistModal({
|
||||
checklistName,
|
||||
onDelete,
|
||||
onClose,
|
||||
}: DeleteChecklistModalProps) {
|
||||
return (
|
||||
<div className="bg-gray-800 rounded-lg p-6 max-w-md w-full">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<span className="w-10 h-10 bg-red-600 rounded-full flex items-center justify-center">
|
||||
<span className="w-5 h-5">
|
||||
<Trash2Icon />
|
||||
</span>
|
||||
</span>
|
||||
<h3 className="text-xl font-bold text-white">Delete Checklist</h3>
|
||||
</div>
|
||||
<p className="text-gray-300 mb-6">
|
||||
Are you sure you want to delete
|
||||
<span className="text-white font-semibold">"{checklistName}"</span>? This action
|
||||
cannot be undone.
|
||||
</p>
|
||||
<div className="flex justify-end gap-3">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 bg-gray-700 hover:bg-gray-600 text-white rounded-lg transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={onDelete}
|
||||
className="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg transition-colors"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
60
frontend/src/components/EditCheckItemModal.tsx
Normal file
60
frontend/src/components/EditCheckItemModal.tsx
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
|
||||
interface EditCheckItemModalProps {
|
||||
itemName: string;
|
||||
onSave: (name: string) => Promise<boolean>;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function EditCheckItemModal({ itemName, onSave, onClose }: EditCheckItemModalProps) {
|
||||
const [name, setName] = useState(itemName);
|
||||
|
||||
useEffect(() => {
|
||||
setName(itemName);
|
||||
}, [itemName]);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!name.trim()) return;
|
||||
|
||||
const success = await onSave(name);
|
||||
if (success) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-gray-800 rounded-lg p-6 max-w-md w-full">
|
||||
<h3 className="text-xl font-bold text-white mb-4">Edit Item</h3>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-gray-300 text-sm font-medium mb-2">Item Name</label>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="w-full bg-gray-700 text-white rounded-lg p-3 border border-gray-600 focus:border-blue-500 focus:outline-none"
|
||||
placeholder="Item name..."
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2 pt-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="flex-1 bg-gray-700 hover:bg-gray-600 text-white font-medium py-2 px-4 rounded-lg transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!name.trim()}
|
||||
className="flex-1 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-600 disabled:cursor-not-allowed text-white font-medium py-2 px-4 rounded-lg transition-colors"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
167
frontend/src/hooks/useChecklistMutations.ts
Normal file
167
frontend/src/hooks/useChecklistMutations.ts
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
import { useApi } from './useApi';
|
||||
import { useLoader } from '../context/loaders/useLoader';
|
||||
import { useToast } from '../context/toasts/useToast';
|
||||
import { CheckItem } from '../types/kanban';
|
||||
|
||||
export function useChecklistMutations(cardId: number, onUpdate: () => void) {
|
||||
const { createChecklist, deleteChecklist, createCheckItem, updateCheckItem, deleteCheckItem } =
|
||||
useApi();
|
||||
const { withLoader } = useLoader();
|
||||
const { addNotification } = useToast();
|
||||
|
||||
const addChecklist = async (name: string, pos: number) => {
|
||||
try {
|
||||
await withLoader(() => createChecklist(cardId, { name, pos }), 'Creating checklist...');
|
||||
onUpdate();
|
||||
addNotification({
|
||||
type: 'success',
|
||||
title: 'Checklist Created',
|
||||
message: 'Checklist has been created successfully.',
|
||||
duration: 3000,
|
||||
});
|
||||
return true;
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Failed to create checklist';
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: 'Error',
|
||||
message: errorMessage,
|
||||
duration: 5000,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const removeChecklist = async (checklistId: number) => {
|
||||
try {
|
||||
await withLoader(() => deleteChecklist(checklistId), 'Deleting checklist...');
|
||||
onUpdate();
|
||||
addNotification({
|
||||
type: 'success',
|
||||
title: 'Checklist Deleted',
|
||||
message: 'Checklist has been deleted successfully.',
|
||||
duration: 3000,
|
||||
});
|
||||
return true;
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Failed to delete checklist';
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: 'Error',
|
||||
message: errorMessage,
|
||||
duration: 5000,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const addCheckItem = async (
|
||||
checklistId: number,
|
||||
name: string,
|
||||
pos: number,
|
||||
state: 'incomplete' | 'complete' = 'incomplete'
|
||||
) => {
|
||||
try {
|
||||
await withLoader(() => createCheckItem(checklistId, { name, pos, state }), 'Adding item...');
|
||||
onUpdate();
|
||||
addNotification({
|
||||
type: 'success',
|
||||
title: 'Item Added',
|
||||
message: 'Check item has been added successfully.',
|
||||
duration: 3000,
|
||||
});
|
||||
return true;
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Failed to add item';
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: 'Error',
|
||||
message: errorMessage,
|
||||
duration: 5000,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const toggleCheckItem = async (item: CheckItem, currentState: 'incomplete' | 'complete') => {
|
||||
console.log('item', item);
|
||||
try {
|
||||
const newState = currentState === 'incomplete' ? 'complete' : 'incomplete';
|
||||
await withLoader(
|
||||
() => updateCheckItem(item.id, { name: item.name, pos: 0, state: newState }),
|
||||
'Updating item...'
|
||||
);
|
||||
onUpdate();
|
||||
return true;
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Failed to update item';
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: 'Error',
|
||||
message: errorMessage,
|
||||
duration: 5000,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const editCheckItem = async (
|
||||
itemId: number,
|
||||
name: string,
|
||||
pos: number,
|
||||
state: 'incomplete' | 'complete'
|
||||
) => {
|
||||
try {
|
||||
await withLoader(() => updateCheckItem(itemId, { name, pos, state }), 'Updating item...');
|
||||
onUpdate();
|
||||
addNotification({
|
||||
type: 'success',
|
||||
title: 'Item Updated',
|
||||
message: 'Check item has been updated successfully.',
|
||||
duration: 3000,
|
||||
});
|
||||
return true;
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Failed to update item';
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: 'Error',
|
||||
message: errorMessage,
|
||||
duration: 5000,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const removeCheckItem = async (itemId: number) => {
|
||||
try {
|
||||
await withLoader(() => deleteCheckItem(itemId), 'Deleting item...');
|
||||
onUpdate();
|
||||
addNotification({
|
||||
type: 'success',
|
||||
title: 'Item Deleted',
|
||||
message: 'Check item has been deleted successfully.',
|
||||
duration: 3000,
|
||||
});
|
||||
return true;
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Failed to delete item';
|
||||
addNotification({
|
||||
type: 'error',
|
||||
title: 'Error',
|
||||
message: errorMessage,
|
||||
duration: 5000,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
addChecklist,
|
||||
removeChecklist,
|
||||
addCheckItem,
|
||||
toggleCheckItem,
|
||||
editCheckItem,
|
||||
removeCheckItem,
|
||||
};
|
||||
}
|
||||
|
|
@ -1,12 +1,13 @@
|
|||
import { useParams, Link, useNavigate } from 'react-router-dom';
|
||||
import { useCard } from '../hooks/useCard';
|
||||
import { useCardDetailMutations } from '../hooks/useCardDetailMutations';
|
||||
import { useChecklistMutations } from '../hooks/useChecklistMutations';
|
||||
import { useModal } from '../context/modals/useModal';
|
||||
import { CardSidebar } from '../components/CardSidebar';
|
||||
import { CardComments } from '../components/CardComments';
|
||||
import { CardChecklists } from '../components/CardChecklists';
|
||||
import { EditCardModal } from '../components/EditCardModal';
|
||||
import { DeleteCardModal } from '../components/DeleteCardModal';
|
||||
import CheckSquareIcon from '../components/icons/CheckSquareIcon';
|
||||
import TagIcon from '../components/icons/TagIcon';
|
||||
import Trash2Icon from '../components/icons/Trash2Icon';
|
||||
import ArrowLeftIcon from '../components/icons/ArrowLeftIcon';
|
||||
|
|
@ -25,6 +26,7 @@ export function CardDetail() {
|
|||
} = useCardDetailMutations(parseInt(cardId || '0'), card, fetchCard);
|
||||
|
||||
const { openModal } = useModal();
|
||||
const checklistMutations = useChecklistMutations(parseInt(cardId || '0'), fetchCard);
|
||||
|
||||
const handleEditCard = () => {
|
||||
if (!card) return;
|
||||
|
|
@ -153,47 +155,16 @@ export function CardDetail() {
|
|||
)}
|
||||
|
||||
{/* Checklists Section */}
|
||||
{card.checklists && card.checklists.length > 0 && (
|
||||
<div className="bg-gray-800 rounded-lg p-6">
|
||||
<h2 className="text-xl font-bold text-white flex items-center gap-2 mb-4">
|
||||
<span className="w-5 h-5">
|
||||
<CheckSquareIcon />
|
||||
</span>
|
||||
Checklists
|
||||
</h2>
|
||||
<div className="space-y-4">
|
||||
{card.checklists.map((checklist: any) => (
|
||||
<div key={checklist.id} className="border border-gray-700 rounded-lg p-4">
|
||||
<h3 className="font-semibold text-white mb-3">{checklist.name}</h3>
|
||||
<div className="space-y-2">
|
||||
{checklist.items && checklist.items.length > 0 ? (
|
||||
checklist.items.map((item: any) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="flex items-center gap-3 p-2 bg-gray-700 rounded"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={item.state === 'complete'}
|
||||
readOnly
|
||||
className="w-5 h-5 rounded"
|
||||
<CardChecklists
|
||||
checklists={card.checklists || []}
|
||||
cardId={parseInt(cardId || '0')}
|
||||
addChecklist={checklistMutations.addChecklist}
|
||||
removeChecklist={checklistMutations.removeChecklist}
|
||||
addCheckItem={checklistMutations.addCheckItem}
|
||||
toggleCheckItem={checklistMutations.toggleCheckItem}
|
||||
editCheckItem={checklistMutations.editCheckItem}
|
||||
removeCheckItem={checklistMutations.removeCheckItem}
|
||||
/>
|
||||
<span
|
||||
className={`text-sm ${item.state === 'complete' ? 'text-gray-400 line-through' : 'text-white'}`}
|
||||
>
|
||||
{item.name}
|
||||
</span>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<p className="text-gray-400 text-sm">No items yet</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CardComments
|
||||
card={card}
|
||||
|
|
|
|||
Loading…
Reference in a new issue