feat: task templates (vibe-kanban) (#197)
* I've successfully implemented task templates for vibe-kanban with the following features: - Created a new `task_templates` table with fields for: - `id` (UUID primary key) - `project_id` (nullable for global templates) - `title` (default task title) - `description` (default task description) - `template_name` (display name for the template) - Timestamps for tracking creation/updates - Created `TaskTemplate` model with full CRUD operations - Added REST API endpoints: - `GET /api/templates` - List all templates - `GET /api/templates/global` - List only global templates - `GET /api/projects/:project_id/templates` - List templates for a project (includes global) - `GET /api/templates/:id` - Get specific template - `POST /api/templates` - Create template - `PUT /api/templates/:id` - Update template - `DELETE /api/templates/:id` - Delete template 1. **Task Creation Dialog**: - Added template selector dropdown when creating new tasks - Templates are fetched based on project context - Selecting a template pre-fills title and description fields - User can edit pre-filled values before creating the task 2. **Global Settings**: - Added "Task Templates" section to manage global templates - Full CRUD interface with table view - Create/Edit dialog for template management 3. **Project Settings**: - Modified project form to use tabs when editing - Added "Task Templates" tab for project-specific templates - Same management interface as global settings - **Scope Management**: Templates can be global (available to all projects) or project-specific - **User Experience**: Template selection is optional and doesn't interfere with normal task creation - **Data Validation**: Unique template names within same scope (global or per-project) - **UI Polish**: Clean interface with loading states, error handling, and confirmation dialogs The implementation allows users to create reusable task templates that streamline the task creation process by pre-filling common values while still allowing full editing before submission. * improve styling * address review comments * fix unqiue contraint on tempaltes * distinguish between local and global templates in UI * keyboard shortcuts for task creation * add dropdown on project page to select templates * update types * add default global task templates * Add task templates from kanban (#219) * Create project templates from kanban * Fixes * remove duplicate --------- Co-authored-by: Louis Knight-Webb <louis@bloop.ai>
This commit is contained in:
committed by
GitHub
parent
4f694f1fc6
commit
471d28defd
56
backend/.sqlx/query-290ce5c152be8d36e58ff42570f9157beb07ab9e77a03ec6fc30b4f56f9b8f6b.json
generated
Normal file
56
backend/.sqlx/query-290ce5c152be8d36e58ff42570f9157beb07ab9e77a03ec6fc30b4f56f9b8f6b.json
generated
Normal file
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "UPDATE task_templates \n SET title = $2, description = $3, template_name = $4, updated_at = datetime('now', 'subsec')\n WHERE id = $1 \n RETURNING id as \"id!: Uuid\", project_id as \"project_id?: Uuid\", title, description, template_name, created_at as \"created_at!: DateTime<Utc>\", updated_at as \"updated_at!: DateTime<Utc>\"",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id!: Uuid",
|
||||
"ordinal": 0,
|
||||
"type_info": "Blob"
|
||||
},
|
||||
{
|
||||
"name": "project_id?: Uuid",
|
||||
"ordinal": 1,
|
||||
"type_info": "Blob"
|
||||
},
|
||||
{
|
||||
"name": "title",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"ordinal": 3,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "template_name",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "created_at!: DateTime<Utc>",
|
||||
"ordinal": 5,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "updated_at!: DateTime<Utc>",
|
||||
"ordinal": 6,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 4
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "290ce5c152be8d36e58ff42570f9157beb07ab9e77a03ec6fc30b4f56f9b8f6b"
|
||||
}
|
||||
56
backend/.sqlx/query-36e4ba7bbd81b402d5a20b6005755eafbb174c8dda442081823406ac32809a94.json
generated
Normal file
56
backend/.sqlx/query-36e4ba7bbd81b402d5a20b6005755eafbb174c8dda442081823406ac32809a94.json
generated
Normal file
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT id as \"id!: Uuid\", project_id as \"project_id?: Uuid\", title, description, template_name, created_at as \"created_at!: DateTime<Utc>\", updated_at as \"updated_at!: DateTime<Utc>\"\n FROM task_templates \n WHERE id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id!: Uuid",
|
||||
"ordinal": 0,
|
||||
"type_info": "Blob"
|
||||
},
|
||||
{
|
||||
"name": "project_id?: Uuid",
|
||||
"ordinal": 1,
|
||||
"type_info": "Blob"
|
||||
},
|
||||
{
|
||||
"name": "title",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"ordinal": 3,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "template_name",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "created_at!: DateTime<Utc>",
|
||||
"ordinal": 5,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "updated_at!: DateTime<Utc>",
|
||||
"ordinal": 6,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "36e4ba7bbd81b402d5a20b6005755eafbb174c8dda442081823406ac32809a94"
|
||||
}
|
||||
56
backend/.sqlx/query-3d6bd16fbce59efe30b7f67ea342e0e4ea6d1432389c02468ad79f1f742d4031.json
generated
Normal file
56
backend/.sqlx/query-3d6bd16fbce59efe30b7f67ea342e0e4ea6d1432389c02468ad79f1f742d4031.json
generated
Normal file
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "INSERT INTO task_templates (id, project_id, title, description, template_name) \n VALUES ($1, $2, $3, $4, $5) \n RETURNING id as \"id!: Uuid\", project_id as \"project_id?: Uuid\", title, description, template_name, created_at as \"created_at!: DateTime<Utc>\", updated_at as \"updated_at!: DateTime<Utc>\"",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id!: Uuid",
|
||||
"ordinal": 0,
|
||||
"type_info": "Blob"
|
||||
},
|
||||
{
|
||||
"name": "project_id?: Uuid",
|
||||
"ordinal": 1,
|
||||
"type_info": "Blob"
|
||||
},
|
||||
{
|
||||
"name": "title",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"ordinal": 3,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "template_name",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "created_at!: DateTime<Utc>",
|
||||
"ordinal": 5,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "updated_at!: DateTime<Utc>",
|
||||
"ordinal": 6,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 5
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "3d6bd16fbce59efe30b7f67ea342e0e4ea6d1432389c02468ad79f1f742d4031"
|
||||
}
|
||||
56
backend/.sqlx/query-461cc1b0bb6fd909afc9dd2246e8526b3771cfbb0b22ae4b5d17b51af587b9e2.json
generated
Normal file
56
backend/.sqlx/query-461cc1b0bb6fd909afc9dd2246e8526b3771cfbb0b22ae4b5d17b51af587b9e2.json
generated
Normal file
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT id as \"id!: Uuid\", project_id as \"project_id?: Uuid\", title, description, template_name, created_at as \"created_at!: DateTime<Utc>\", updated_at as \"updated_at!: DateTime<Utc>\"\n FROM task_templates \n WHERE project_id IS NULL\n ORDER BY template_name ASC",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id!: Uuid",
|
||||
"ordinal": 0,
|
||||
"type_info": "Blob"
|
||||
},
|
||||
{
|
||||
"name": "project_id?: Uuid",
|
||||
"ordinal": 1,
|
||||
"type_info": "Blob"
|
||||
},
|
||||
{
|
||||
"name": "title",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"ordinal": 3,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "template_name",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "created_at!: DateTime<Utc>",
|
||||
"ordinal": 5,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "updated_at!: DateTime<Utc>",
|
||||
"ordinal": 6,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "461cc1b0bb6fd909afc9dd2246e8526b3771cfbb0b22ae4b5d17b51af587b9e2"
|
||||
}
|
||||
12
backend/.sqlx/query-8f01ebd64bdcde6a090479f14810d73ba23020e76fd70854ac57f2da251702c3.json
generated
Normal file
12
backend/.sqlx/query-8f01ebd64bdcde6a090479f14810d73ba23020e76fd70854ac57f2da251702c3.json
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "DELETE FROM task_templates WHERE id = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "8f01ebd64bdcde6a090479f14810d73ba23020e76fd70854ac57f2da251702c3"
|
||||
}
|
||||
56
backend/.sqlx/query-96036c4f9e0f48bdc5a4a4588f0c5f288ac7aaa5425cac40fc33f337e1a351f2.json
generated
Normal file
56
backend/.sqlx/query-96036c4f9e0f48bdc5a4a4588f0c5f288ac7aaa5425cac40fc33f337e1a351f2.json
generated
Normal file
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT id as \"id!: Uuid\", project_id as \"project_id?: Uuid\", title, description, template_name, created_at as \"created_at!: DateTime<Utc>\", updated_at as \"updated_at!: DateTime<Utc>\"\n FROM task_templates \n ORDER BY project_id IS NULL DESC, template_name ASC",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id!: Uuid",
|
||||
"ordinal": 0,
|
||||
"type_info": "Blob"
|
||||
},
|
||||
{
|
||||
"name": "project_id?: Uuid",
|
||||
"ordinal": 1,
|
||||
"type_info": "Blob"
|
||||
},
|
||||
{
|
||||
"name": "title",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"ordinal": 3,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "template_name",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "created_at!: DateTime<Utc>",
|
||||
"ordinal": 5,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "updated_at!: DateTime<Utc>",
|
||||
"ordinal": 6,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "96036c4f9e0f48bdc5a4a4588f0c5f288ac7aaa5425cac40fc33f337e1a351f2"
|
||||
}
|
||||
56
backend/.sqlx/query-fdb06a7d9050f98d73e743b6522c7443d603931c082bd38f13b8f1f127b88711.json
generated
Normal file
56
backend/.sqlx/query-fdb06a7d9050f98d73e743b6522c7443d603931c082bd38f13b8f1f127b88711.json
generated
Normal file
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT id as \"id!: Uuid\", project_id as \"project_id?: Uuid\", title, description, template_name, created_at as \"created_at!: DateTime<Utc>\", updated_at as \"updated_at!: DateTime<Utc>\"\n FROM task_templates \n WHERE project_id = $1 OR project_id IS NULL\n ORDER BY project_id IS NULL, template_name ASC",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id!: Uuid",
|
||||
"ordinal": 0,
|
||||
"type_info": "Blob"
|
||||
},
|
||||
{
|
||||
"name": "project_id?: Uuid",
|
||||
"ordinal": 1,
|
||||
"type_info": "Blob"
|
||||
},
|
||||
{
|
||||
"name": "title",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"ordinal": 3,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "template_name",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "created_at!: DateTime<Utc>",
|
||||
"ordinal": 5,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "updated_at!: DateTime<Utc>",
|
||||
"ordinal": 6,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "fdb06a7d9050f98d73e743b6522c7443d603931c082bd38f13b8f1f127b88711"
|
||||
}
|
||||
25
backend/migrations/20250715154859_add_task_templates.sql
Normal file
25
backend/migrations/20250715154859_add_task_templates.sql
Normal file
@@ -0,0 +1,25 @@
|
||||
-- Add task templates tables
|
||||
CREATE TABLE task_templates (
|
||||
id BLOB PRIMARY KEY,
|
||||
project_id BLOB, -- NULL for global templates
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
template_name TEXT NOT NULL, -- Display name for the template
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now', 'subsec')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now', 'subsec')),
|
||||
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Add index for faster queries
|
||||
CREATE INDEX idx_task_templates_project_id ON task_templates(project_id);
|
||||
|
||||
-- Add unique constraints to prevent duplicate template names within same scope
|
||||
-- For project-specific templates: unique within each project
|
||||
CREATE UNIQUE INDEX idx_task_templates_unique_name_project
|
||||
ON task_templates(project_id, template_name)
|
||||
WHERE project_id IS NOT NULL;
|
||||
|
||||
-- For global templates: unique across all global templates
|
||||
CREATE UNIQUE INDEX idx_task_templates_unique_name_global
|
||||
ON task_templates(template_name)
|
||||
WHERE project_id IS NULL;
|
||||
174
backend/migrations/20250716143725_add_default_templates.sql
Normal file
174
backend/migrations/20250716143725_add_default_templates.sql
Normal file
@@ -0,0 +1,174 @@
|
||||
-- Add default global templates
|
||||
|
||||
-- 1. Bug Analysis template
|
||||
INSERT INTO task_templates (
|
||||
id,
|
||||
project_id,
|
||||
title,
|
||||
description,
|
||||
template_name,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES (
|
||||
randomblob(16),
|
||||
NULL, -- Global template
|
||||
'Analyze codebase for potential bugs and issues',
|
||||
'Perform a comprehensive analysis of the project codebase to identify potential bugs, code smells, and areas of improvement.
|
||||
|
||||
## Analysis Checklist:
|
||||
|
||||
### 1. Static Code Analysis
|
||||
- [ ] Run linting tools to identify syntax and style issues
|
||||
- [ ] Check for unused variables, imports, and dead code
|
||||
- [ ] Identify potential type errors or mismatches
|
||||
- [ ] Look for deprecated API usage
|
||||
|
||||
### 2. Common Bug Patterns
|
||||
- [ ] Check for null/undefined reference errors
|
||||
- [ ] Identify potential race conditions
|
||||
- [ ] Look for improper error handling
|
||||
- [ ] Check for resource leaks (memory, file handles, connections)
|
||||
- [ ] Identify potential security vulnerabilities (XSS, SQL injection, etc.)
|
||||
|
||||
### 3. Code Quality Issues
|
||||
- [ ] Identify overly complex functions (high cyclomatic complexity)
|
||||
- [ ] Look for code duplication
|
||||
- [ ] Check for missing or inadequate input validation
|
||||
- [ ] Identify hardcoded values that should be configurable
|
||||
|
||||
### 4. Testing Gaps
|
||||
- [ ] Identify untested code paths
|
||||
- [ ] Check for missing edge case tests
|
||||
- [ ] Look for inadequate error scenario testing
|
||||
|
||||
### 5. Performance Concerns
|
||||
- [ ] Identify potential performance bottlenecks
|
||||
- [ ] Check for inefficient algorithms or data structures
|
||||
- [ ] Look for unnecessary database queries or API calls
|
||||
|
||||
## Deliverables:
|
||||
1. Prioritized list of identified issues
|
||||
2. Recommendations for fixes
|
||||
3. Estimated effort for addressing each issue',
|
||||
'Bug Analysis',
|
||||
datetime('now', 'subsec'),
|
||||
datetime('now', 'subsec')
|
||||
);
|
||||
|
||||
-- 2. Unit Test template
|
||||
INSERT INTO task_templates (
|
||||
id,
|
||||
project_id,
|
||||
title,
|
||||
description,
|
||||
template_name,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES (
|
||||
randomblob(16),
|
||||
NULL, -- Global template
|
||||
'Add unit tests for [component/function]',
|
||||
'Write unit tests to improve code coverage and ensure reliability.
|
||||
|
||||
## Unit Testing Checklist
|
||||
|
||||
### 1. Identify What to Test
|
||||
- [ ] Run coverage report to find untested functions
|
||||
- [ ] List the specific functions/methods to test
|
||||
- [ ] Note current coverage percentage
|
||||
|
||||
### 2. Write Tests
|
||||
- [ ] Test the happy path (expected behavior)
|
||||
- [ ] Test edge cases (empty inputs, boundaries)
|
||||
- [ ] Test error cases (invalid inputs, exceptions)
|
||||
- [ ] Mock external dependencies
|
||||
- [ ] Use descriptive test names
|
||||
|
||||
### 3. Test Quality
|
||||
- [ ] Each test focuses on one behavior
|
||||
- [ ] Tests can run independently
|
||||
- [ ] No hardcoded values that might change
|
||||
- [ ] Clear assertions that verify the behavior
|
||||
|
||||
## Examples to Cover:
|
||||
- Normal inputs → Expected outputs
|
||||
- Empty/null inputs → Proper handling
|
||||
- Invalid inputs → Error cases
|
||||
- Boundary values → Edge case behavior
|
||||
|
||||
## Goal
|
||||
Achieve at least 80% coverage for the target component
|
||||
|
||||
## Deliverables
|
||||
1. New test file(s) with comprehensive unit tests
|
||||
2. Updated coverage report
|
||||
3. All tests passing',
|
||||
'Add Unit Tests',
|
||||
datetime('now', 'subsec'),
|
||||
datetime('now', 'subsec')
|
||||
);
|
||||
|
||||
-- 3. Code Refactoring template
|
||||
INSERT INTO task_templates (
|
||||
id,
|
||||
project_id,
|
||||
title,
|
||||
description,
|
||||
template_name,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES (
|
||||
randomblob(16),
|
||||
NULL, -- Global template
|
||||
'Refactor [component/module] for better maintainability',
|
||||
'Improve code structure and maintainability without changing functionality.
|
||||
|
||||
## Refactoring Checklist
|
||||
|
||||
### 1. Identify Refactoring Targets
|
||||
- [ ] Run code analysis tools (linters, complexity analyzers)
|
||||
- [ ] Identify code smells (long methods, duplicate code, large classes)
|
||||
- [ ] Check for outdated patterns or deprecated approaches
|
||||
- [ ] Review areas with frequent bugs or changes
|
||||
|
||||
### 2. Plan the Refactoring
|
||||
- [ ] Define clear goals (what to improve and why)
|
||||
- [ ] Ensure tests exist for current functionality
|
||||
- [ ] Create a backup branch
|
||||
- [ ] Break down into small, safe steps
|
||||
|
||||
### 3. Common Refactoring Actions
|
||||
- [ ] Extract methods from long functions
|
||||
- [ ] Remove duplicate code (DRY principle)
|
||||
- [ ] Rename variables/functions for clarity
|
||||
- [ ] Simplify complex conditionals
|
||||
- [ ] Extract constants from magic numbers/strings
|
||||
- [ ] Group related functionality into modules
|
||||
- [ ] Remove dead code
|
||||
|
||||
### 4. Maintain Functionality
|
||||
- [ ] Run tests after each change
|
||||
- [ ] Keep changes small and incremental
|
||||
- [ ] Commit frequently with clear messages
|
||||
- [ ] Verify no behavior has changed
|
||||
|
||||
### 5. Code Quality Improvements
|
||||
- [ ] Apply consistent formatting
|
||||
- [ ] Update to modern syntax/features
|
||||
- [ ] Improve error handling
|
||||
- [ ] Add type annotations (if applicable)
|
||||
|
||||
## Success Criteria
|
||||
- All tests still pass
|
||||
- Code is more readable and maintainable
|
||||
- No new bugs introduced
|
||||
- Performance not degraded
|
||||
|
||||
## Deliverables
|
||||
1. Refactored code with improved structure
|
||||
2. All tests passing
|
||||
3. Brief summary of changes made',
|
||||
'Code Refactoring',
|
||||
datetime('now', 'subsec'),
|
||||
datetime('now', 'subsec')
|
||||
);
|
||||
@@ -98,6 +98,9 @@ fn generate_types_content() -> String {
|
||||
vibe_kanban::models::task::Task::decl(),
|
||||
vibe_kanban::models::task::TaskWithAttemptStatus::decl(),
|
||||
vibe_kanban::models::task::UpdateTask::decl(),
|
||||
vibe_kanban::models::task_template::TaskTemplate::decl(),
|
||||
vibe_kanban::models::task_template::CreateTaskTemplate::decl(),
|
||||
vibe_kanban::models::task_template::UpdateTaskTemplate::decl(),
|
||||
vibe_kanban::models::task_attempt::TaskAttemptStatus::decl(),
|
||||
vibe_kanban::models::task_attempt::TaskAttempt::decl(),
|
||||
vibe_kanban::models::task_attempt::CreateTaskAttempt::decl(),
|
||||
|
||||
@@ -29,7 +29,9 @@ mod utils;
|
||||
use app_state::AppState;
|
||||
use execution_monitor::execution_monitor;
|
||||
use models::{ApiResponse, Config};
|
||||
use routes::{auth, config, filesystem, health, projects, stream, task_attempts, tasks};
|
||||
use routes::{
|
||||
auth, config, filesystem, health, projects, stream, task_attempts, task_templates, tasks,
|
||||
};
|
||||
use services::PrMonitorService;
|
||||
|
||||
async fn echo_handler(
|
||||
@@ -199,6 +201,7 @@ fn main() -> anyhow::Result<()> {
|
||||
.merge(tasks::tasks_router())
|
||||
.merge(task_attempts::task_attempts_router())
|
||||
.merge(stream::stream_router())
|
||||
.merge(task_templates::templates_router())
|
||||
.merge(filesystem::filesystem_router())
|
||||
.merge(config::config_router())
|
||||
.merge(auth::auth_router())
|
||||
|
||||
@@ -8,3 +8,21 @@ pub struct ApiResponse<T> {
|
||||
pub data: Option<T>,
|
||||
pub message: Option<String>,
|
||||
}
|
||||
|
||||
impl<T> ApiResponse<T> {
|
||||
pub fn success(data: T) -> Self {
|
||||
Self {
|
||||
success: true,
|
||||
data: Some(data),
|
||||
message: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn error(message: &str) -> Self {
|
||||
Self {
|
||||
success: false,
|
||||
data: None,
|
||||
message: Some(message.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ pub mod project;
|
||||
pub mod task;
|
||||
pub mod task_attempt;
|
||||
pub mod task_attempt_activity;
|
||||
pub mod task_template;
|
||||
|
||||
pub use api_response::ApiResponse;
|
||||
pub use config::Config;
|
||||
|
||||
145
backend/src/models/task_template.rs
Normal file
145
backend/src/models/task_template.rs
Normal file
@@ -0,0 +1,145 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, SqlitePool};
|
||||
use ts_rs::TS;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone, FromRow, Serialize, Deserialize, TS)]
|
||||
#[ts(export)]
|
||||
pub struct TaskTemplate {
|
||||
pub id: Uuid,
|
||||
pub project_id: Option<Uuid>, // None for global templates
|
||||
pub title: String,
|
||||
pub description: Option<String>,
|
||||
pub template_name: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, TS)]
|
||||
#[ts(export)]
|
||||
pub struct CreateTaskTemplate {
|
||||
pub project_id: Option<Uuid>,
|
||||
pub title: String,
|
||||
pub description: Option<String>,
|
||||
pub template_name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, TS)]
|
||||
#[ts(export)]
|
||||
pub struct UpdateTaskTemplate {
|
||||
pub title: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub template_name: Option<String>,
|
||||
}
|
||||
|
||||
impl TaskTemplate {
|
||||
pub async fn find_all(pool: &SqlitePool) -> Result<Vec<Self>, sqlx::Error> {
|
||||
sqlx::query_as!(
|
||||
TaskTemplate,
|
||||
r#"SELECT id as "id!: Uuid", project_id as "project_id?: Uuid", title, description, template_name, created_at as "created_at!: DateTime<Utc>", updated_at as "updated_at!: DateTime<Utc>"
|
||||
FROM task_templates
|
||||
ORDER BY project_id IS NULL DESC, template_name ASC"#
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn find_by_project_id(
|
||||
pool: &SqlitePool,
|
||||
project_id: Option<Uuid>,
|
||||
) -> Result<Vec<Self>, sqlx::Error> {
|
||||
if let Some(pid) = project_id {
|
||||
// Return only project-specific templates
|
||||
sqlx::query_as::<_, TaskTemplate>(
|
||||
r#"SELECT id, project_id, title, description, template_name, created_at, updated_at
|
||||
FROM task_templates
|
||||
WHERE project_id = ?
|
||||
ORDER BY template_name ASC"#,
|
||||
)
|
||||
.bind(pid)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
} else {
|
||||
// Return only global templates
|
||||
sqlx::query_as!(
|
||||
TaskTemplate,
|
||||
r#"SELECT id as "id!: Uuid", project_id as "project_id?: Uuid", title, description, template_name, created_at as "created_at!: DateTime<Utc>", updated_at as "updated_at!: DateTime<Utc>"
|
||||
FROM task_templates
|
||||
WHERE project_id IS NULL
|
||||
ORDER BY template_name ASC"#
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn find_by_id(pool: &SqlitePool, id: Uuid) -> Result<Option<Self>, sqlx::Error> {
|
||||
sqlx::query_as!(
|
||||
TaskTemplate,
|
||||
r#"SELECT id as "id!: Uuid", project_id as "project_id?: Uuid", title, description, template_name, created_at as "created_at!: DateTime<Utc>", updated_at as "updated_at!: DateTime<Utc>"
|
||||
FROM task_templates
|
||||
WHERE id = $1"#,
|
||||
id
|
||||
)
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn create(pool: &SqlitePool, data: &CreateTaskTemplate) -> Result<Self, sqlx::Error> {
|
||||
let id = Uuid::new_v4();
|
||||
sqlx::query_as!(
|
||||
TaskTemplate,
|
||||
r#"INSERT INTO task_templates (id, project_id, title, description, template_name)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING id as "id!: Uuid", project_id as "project_id?: Uuid", title, description, template_name, created_at as "created_at!: DateTime<Utc>", updated_at as "updated_at!: DateTime<Utc>""#,
|
||||
id,
|
||||
data.project_id,
|
||||
data.title,
|
||||
data.description,
|
||||
data.template_name
|
||||
)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn update(
|
||||
pool: &SqlitePool,
|
||||
id: Uuid,
|
||||
data: &UpdateTaskTemplate,
|
||||
) -> Result<Self, sqlx::Error> {
|
||||
// Get existing template first
|
||||
let existing = Self::find_by_id(pool, id)
|
||||
.await?
|
||||
.ok_or(sqlx::Error::RowNotFound)?;
|
||||
|
||||
// Use let bindings to create longer-lived values
|
||||
let title = data.title.as_ref().unwrap_or(&existing.title);
|
||||
let description = data.description.as_ref().or(existing.description.as_ref());
|
||||
let template_name = data
|
||||
.template_name
|
||||
.as_ref()
|
||||
.unwrap_or(&existing.template_name);
|
||||
|
||||
sqlx::query_as!(
|
||||
TaskTemplate,
|
||||
r#"UPDATE task_templates
|
||||
SET title = $2, description = $3, template_name = $4, updated_at = datetime('now', 'subsec')
|
||||
WHERE id = $1
|
||||
RETURNING id as "id!: Uuid", project_id as "project_id?: Uuid", title, description, template_name, created_at as "created_at!: DateTime<Utc>", updated_at as "updated_at!: DateTime<Utc>""#,
|
||||
id,
|
||||
title,
|
||||
description,
|
||||
template_name
|
||||
)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn delete(pool: &SqlitePool, id: Uuid) -> Result<u64, sqlx::Error> {
|
||||
let result = sqlx::query!("DELETE FROM task_templates WHERE id = $1", id)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
Ok(result.rows_affected())
|
||||
}
|
||||
}
|
||||
@@ -5,4 +5,5 @@ pub mod health;
|
||||
pub mod projects;
|
||||
pub mod stream;
|
||||
pub mod task_attempts;
|
||||
pub mod task_templates;
|
||||
pub mod tasks;
|
||||
|
||||
178
backend/src/routes/task_templates.rs
Normal file
178
backend/src/routes/task_templates.rs
Normal file
@@ -0,0 +1,178 @@
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
response::IntoResponse,
|
||||
routing::get,
|
||||
Json, Router,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
app_state::AppState,
|
||||
models::{
|
||||
api_response::ApiResponse,
|
||||
task_template::{CreateTaskTemplate, TaskTemplate, UpdateTaskTemplate},
|
||||
},
|
||||
};
|
||||
|
||||
pub async fn list_templates(
|
||||
State(state): State<AppState>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, Json<ApiResponse<()>>)> {
|
||||
match TaskTemplate::find_all(&state.db_pool).await {
|
||||
Ok(templates) => Ok(Json(ApiResponse::success(templates))),
|
||||
Err(e) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(ApiResponse::error(&format!(
|
||||
"Failed to fetch templates: {}",
|
||||
e
|
||||
))),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn list_project_templates(
|
||||
State(state): State<AppState>,
|
||||
Path(project_id): Path<Uuid>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, Json<ApiResponse<()>>)> {
|
||||
match TaskTemplate::find_by_project_id(&state.db_pool, Some(project_id)).await {
|
||||
Ok(templates) => Ok(Json(ApiResponse::success(templates))),
|
||||
Err(e) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(ApiResponse::error(&format!(
|
||||
"Failed to fetch templates: {}",
|
||||
e
|
||||
))),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn list_global_templates(
|
||||
State(state): State<AppState>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, Json<ApiResponse<()>>)> {
|
||||
match TaskTemplate::find_by_project_id(&state.db_pool, None).await {
|
||||
Ok(templates) => Ok(Json(ApiResponse::success(templates))),
|
||||
Err(e) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(ApiResponse::error(&format!(
|
||||
"Failed to fetch global templates: {}",
|
||||
e
|
||||
))),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_template(
|
||||
State(state): State<AppState>,
|
||||
Path(template_id): Path<Uuid>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, Json<ApiResponse<()>>)> {
|
||||
match TaskTemplate::find_by_id(&state.db_pool, template_id).await {
|
||||
Ok(Some(template)) => Ok(Json(ApiResponse::success(template))),
|
||||
Ok(None) => Err((
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(ApiResponse::error("Template not found")),
|
||||
)),
|
||||
Err(e) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(ApiResponse::error(&format!(
|
||||
"Failed to fetch template: {}",
|
||||
e
|
||||
))),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_template(
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<CreateTaskTemplate>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, Json<ApiResponse<()>>)> {
|
||||
match TaskTemplate::create(&state.db_pool, &payload).await {
|
||||
Ok(template) => Ok((StatusCode::CREATED, Json(ApiResponse::success(template)))),
|
||||
Err(e) => {
|
||||
if e.to_string().contains("UNIQUE constraint failed") {
|
||||
Err((
|
||||
StatusCode::CONFLICT,
|
||||
Json(ApiResponse::error(
|
||||
"A template with this name already exists in this scope",
|
||||
)),
|
||||
))
|
||||
} else {
|
||||
Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(ApiResponse::error(&format!(
|
||||
"Failed to create template: {}",
|
||||
e
|
||||
))),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_template(
|
||||
State(state): State<AppState>,
|
||||
Path(template_id): Path<Uuid>,
|
||||
Json(payload): Json<UpdateTaskTemplate>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, Json<ApiResponse<()>>)> {
|
||||
match TaskTemplate::update(&state.db_pool, template_id, &payload).await {
|
||||
Ok(template) => Ok(Json(ApiResponse::success(template))),
|
||||
Err(e) => {
|
||||
if matches!(e, sqlx::Error::RowNotFound) {
|
||||
Err((
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(ApiResponse::error("Template not found")),
|
||||
))
|
||||
} else if e.to_string().contains("UNIQUE constraint failed") {
|
||||
Err((
|
||||
StatusCode::CONFLICT,
|
||||
Json(ApiResponse::error(
|
||||
"A template with this name already exists in this scope",
|
||||
)),
|
||||
))
|
||||
} else {
|
||||
Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(ApiResponse::error(&format!(
|
||||
"Failed to update template: {}",
|
||||
e
|
||||
))),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete_template(
|
||||
State(state): State<AppState>,
|
||||
Path(template_id): Path<Uuid>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, Json<ApiResponse<()>>)> {
|
||||
match TaskTemplate::delete(&state.db_pool, template_id).await {
|
||||
Ok(0) => Err((
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(ApiResponse::error("Template not found")),
|
||||
)),
|
||||
Ok(_) => Ok(Json(ApiResponse::success(()))),
|
||||
Err(e) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(ApiResponse::error(&format!(
|
||||
"Failed to delete template: {}",
|
||||
e
|
||||
))),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn templates_router() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/templates", get(list_templates).post(create_template))
|
||||
.route("/templates/global", get(list_global_templates))
|
||||
.route(
|
||||
"/templates/:id",
|
||||
get(get_template)
|
||||
.put(update_template)
|
||||
.delete(delete_template),
|
||||
)
|
||||
.route(
|
||||
"/projects/:project_id/templates",
|
||||
get(list_project_templates),
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user