@nextsparkjs/theme-productivity 0.1.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -0
- package/about.md +123 -0
- package/components/CardDetailModal.tsx +318 -0
- package/components/KanbanBoard.tsx +612 -0
- package/components/KanbanCard.tsx +218 -0
- package/components/KanbanColumn.tsx +264 -0
- package/components/SortableList.tsx +46 -0
- package/components/index.ts +4 -0
- package/config/app.config.ts +172 -0
- package/config/billing.config.ts +187 -0
- package/config/dashboard.config.ts +357 -0
- package/config/dev.config.ts +55 -0
- package/config/features.config.ts +256 -0
- package/config/flows.config.ts +484 -0
- package/config/permissions.config.ts +167 -0
- package/config/theme.config.ts +106 -0
- package/entities/boards/boards.config.ts +61 -0
- package/entities/boards/boards.fields.ts +154 -0
- package/entities/boards/boards.service.ts +256 -0
- package/entities/boards/boards.types.ts +57 -0
- package/entities/boards/messages/en.json +80 -0
- package/entities/boards/messages/es.json +80 -0
- package/entities/boards/migrations/001_boards_table.sql +83 -0
- package/entities/cards/cards.config.ts +61 -0
- package/entities/cards/cards.fields.ts +242 -0
- package/entities/cards/cards.service.ts +336 -0
- package/entities/cards/cards.types.ts +79 -0
- package/entities/cards/messages/en.json +114 -0
- package/entities/cards/messages/es.json +114 -0
- package/entities/cards/migrations/020_cards_table.sql +92 -0
- package/entities/lists/lists.config.ts +61 -0
- package/entities/lists/lists.fields.ts +105 -0
- package/entities/lists/lists.service.ts +252 -0
- package/entities/lists/lists.types.ts +55 -0
- package/entities/lists/messages/en.json +60 -0
- package/entities/lists/messages/es.json +60 -0
- package/entities/lists/migrations/010_lists_table.sql +79 -0
- package/lib/selectors.ts +206 -0
- package/messages/en.json +79 -0
- package/messages/es.json +79 -0
- package/migrations/999_theme_sample_data.sql +922 -0
- package/migrations/999a_initial_sample_data.sql +377 -0
- package/migrations/999b_abundant_sample_data.sql +346 -0
- package/package.json +17 -0
- package/permissions-matrix.md +122 -0
- package/styles/components.css +460 -0
- package/styles/globals.css +560 -0
- package/templates/dashboard/(main)/boards/[id]/[cardId]/page.tsx +238 -0
- package/templates/dashboard/(main)/boards/[id]/edit/page.tsx +390 -0
- package/templates/dashboard/(main)/boards/[id]/page.tsx +236 -0
- package/templates/dashboard/(main)/boards/create/page.tsx +236 -0
- package/templates/dashboard/(main)/boards/page.tsx +335 -0
- package/templates/dashboard/(main)/layout.tsx +32 -0
- package/templates/dashboard/(main)/page.tsx +592 -0
- package/templates/shared/ProductivityMobileNav.tsx +410 -0
- package/templates/shared/ProductivitySidebar.tsx +538 -0
- package/templates/shared/ProductivityTopBar.tsx +317 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
-- ============================================================================
|
|
2
|
+
-- Cards Table Migration
|
|
3
|
+
-- Productivity theme: Task cards within lists
|
|
4
|
+
-- ============================================================================
|
|
5
|
+
|
|
6
|
+
CREATE TABLE IF NOT EXISTS "cards" (
|
|
7
|
+
"id" TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
8
|
+
"title" VARCHAR(500) NOT NULL,
|
|
9
|
+
"description" TEXT,
|
|
10
|
+
"position" INTEGER DEFAULT 0,
|
|
11
|
+
"priority" VARCHAR(20) DEFAULT 'medium' CHECK ("priority" IN ('low', 'medium', 'high', 'urgent')),
|
|
12
|
+
"dueDate" DATE,
|
|
13
|
+
"labels" JSONB DEFAULT '[]'::jsonb,
|
|
14
|
+
"assigneeId" TEXT REFERENCES "users"("id") ON DELETE SET NULL,
|
|
15
|
+
"listId" TEXT NOT NULL REFERENCES "lists"("id") ON DELETE CASCADE,
|
|
16
|
+
"boardId" TEXT NOT NULL REFERENCES "boards"("id") ON DELETE CASCADE,
|
|
17
|
+
"userId" TEXT NOT NULL REFERENCES "users"("id") ON DELETE CASCADE,
|
|
18
|
+
"teamId" TEXT NOT NULL REFERENCES "teams"("id") ON DELETE CASCADE,
|
|
19
|
+
"createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
20
|
+
"updatedAt" TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
-- Indexes
|
|
24
|
+
CREATE INDEX IF NOT EXISTS "cards_listId_idx" ON "cards" ("listId");
|
|
25
|
+
CREATE INDEX IF NOT EXISTS "cards_boardId_idx" ON "cards" ("boardId");
|
|
26
|
+
CREATE INDEX IF NOT EXISTS "cards_teamId_idx" ON "cards" ("teamId");
|
|
27
|
+
CREATE INDEX IF NOT EXISTS "cards_assigneeId_idx" ON "cards" ("assigneeId");
|
|
28
|
+
CREATE INDEX IF NOT EXISTS "cards_position_idx" ON "cards" ("listId", "position");
|
|
29
|
+
CREATE INDEX IF NOT EXISTS "cards_dueDate_idx" ON "cards" ("dueDate") WHERE "dueDate" IS NOT NULL;
|
|
30
|
+
CREATE INDEX IF NOT EXISTS "cards_labels_idx" ON "cards" USING GIN ("labels");
|
|
31
|
+
|
|
32
|
+
-- Enable RLS
|
|
33
|
+
ALTER TABLE "cards" ENABLE ROW LEVEL SECURITY;
|
|
34
|
+
|
|
35
|
+
-- Drop existing policies
|
|
36
|
+
DROP POLICY IF EXISTS "cards_select_policy" ON "cards";
|
|
37
|
+
DROP POLICY IF EXISTS "cards_insert_policy" ON "cards";
|
|
38
|
+
DROP POLICY IF EXISTS "cards_update_policy" ON "cards";
|
|
39
|
+
DROP POLICY IF EXISTS "cards_delete_policy" ON "cards";
|
|
40
|
+
|
|
41
|
+
-- Policy: Team members can view cards
|
|
42
|
+
CREATE POLICY "cards_select_policy" ON "cards"
|
|
43
|
+
FOR SELECT TO authenticated
|
|
44
|
+
USING (
|
|
45
|
+
"teamId" = ANY(public.get_user_team_ids())
|
|
46
|
+
OR public.is_superadmin()
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
-- Policy: Team members can create cards
|
|
50
|
+
CREATE POLICY "cards_insert_policy" ON "cards"
|
|
51
|
+
FOR INSERT TO authenticated
|
|
52
|
+
WITH CHECK (
|
|
53
|
+
"teamId" = ANY(public.get_user_team_ids())
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
-- Policy: Team members can update cards
|
|
57
|
+
CREATE POLICY "cards_update_policy" ON "cards"
|
|
58
|
+
FOR UPDATE TO authenticated
|
|
59
|
+
USING (
|
|
60
|
+
"teamId" = ANY(public.get_user_team_ids())
|
|
61
|
+
OR public.is_superadmin()
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
-- Policy: Team members can delete cards
|
|
65
|
+
CREATE POLICY "cards_delete_policy" ON "cards"
|
|
66
|
+
FOR DELETE TO authenticated
|
|
67
|
+
USING (
|
|
68
|
+
"teamId" = ANY(public.get_user_team_ids())
|
|
69
|
+
OR public.is_superadmin()
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
-- Trigger for updatedAt
|
|
73
|
+
CREATE OR REPLACE FUNCTION update_cards_updated_at()
|
|
74
|
+
RETURNS TRIGGER AS $$
|
|
75
|
+
BEGIN
|
|
76
|
+
NEW."updatedAt" = NOW();
|
|
77
|
+
RETURN NEW;
|
|
78
|
+
END;
|
|
79
|
+
$$ LANGUAGE plpgsql;
|
|
80
|
+
|
|
81
|
+
DROP TRIGGER IF EXISTS cards_updated_at_trigger ON "cards";
|
|
82
|
+
CREATE TRIGGER cards_updated_at_trigger
|
|
83
|
+
BEFORE UPDATE ON "cards"
|
|
84
|
+
FOR EACH ROW
|
|
85
|
+
EXECUTE FUNCTION update_cards_updated_at();
|
|
86
|
+
|
|
87
|
+
-- Comments
|
|
88
|
+
COMMENT ON TABLE "cards" IS 'Task cards within lists - the main work items';
|
|
89
|
+
COMMENT ON COLUMN "cards"."labels" IS 'JSON array of label strings';
|
|
90
|
+
COMMENT ON COLUMN "cards"."position" IS 'Display order within the parent list';
|
|
91
|
+
COMMENT ON COLUMN "cards"."assigneeId" IS 'Team member assigned to this card';
|
|
92
|
+
COMMENT ON COLUMN "cards"."priority" IS 'Card priority: low, medium, high, urgent';
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lists Entity Configuration
|
|
3
|
+
*
|
|
4
|
+
* Columns within boards (To Do, In Progress, Done, etc.)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { List } from 'lucide-react'
|
|
8
|
+
import type { EntityConfig } from '@nextsparkjs/core/lib/entities/types'
|
|
9
|
+
import { listFields } from './lists.fields'
|
|
10
|
+
|
|
11
|
+
export const listEntityConfig: EntityConfig = {
|
|
12
|
+
// ==========================================
|
|
13
|
+
// 1. BASIC IDENTIFICATION
|
|
14
|
+
// ==========================================
|
|
15
|
+
slug: 'lists',
|
|
16
|
+
enabled: true,
|
|
17
|
+
names: {
|
|
18
|
+
singular: 'list',
|
|
19
|
+
plural: 'Lists'
|
|
20
|
+
},
|
|
21
|
+
icon: List,
|
|
22
|
+
|
|
23
|
+
// ==========================================
|
|
24
|
+
// 2. ACCESS AND SCOPE CONFIGURATION
|
|
25
|
+
// ==========================================
|
|
26
|
+
access: {
|
|
27
|
+
public: false,
|
|
28
|
+
api: true,
|
|
29
|
+
metadata: false,
|
|
30
|
+
shared: true
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
// ==========================================
|
|
34
|
+
// 3. UI/UX FEATURES
|
|
35
|
+
// ==========================================
|
|
36
|
+
ui: {
|
|
37
|
+
dashboard: {
|
|
38
|
+
showInMenu: false, // Lists shown within boards, not separately
|
|
39
|
+
showInTopbar: false
|
|
40
|
+
},
|
|
41
|
+
public: {
|
|
42
|
+
hasArchivePage: false,
|
|
43
|
+
hasSinglePage: false
|
|
44
|
+
},
|
|
45
|
+
features: {
|
|
46
|
+
searchable: true,
|
|
47
|
+
sortable: true,
|
|
48
|
+
filterable: true,
|
|
49
|
+
bulkOperations: false,
|
|
50
|
+
importExport: false
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
// ==========================================
|
|
55
|
+
// FIELDS
|
|
56
|
+
// ==========================================
|
|
57
|
+
fields: listFields,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export default listEntityConfig
|
|
61
|
+
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lists Entity Fields Configuration
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { EntityField } from '@nextsparkjs/core/lib/entities/types'
|
|
6
|
+
|
|
7
|
+
export const listFields: EntityField[] = [
|
|
8
|
+
{
|
|
9
|
+
name: 'name',
|
|
10
|
+
type: 'text',
|
|
11
|
+
required: true,
|
|
12
|
+
display: {
|
|
13
|
+
label: 'Name',
|
|
14
|
+
description: 'List name',
|
|
15
|
+
placeholder: 'Enter list name...',
|
|
16
|
+
showInList: true,
|
|
17
|
+
showInDetail: true,
|
|
18
|
+
showInForm: true,
|
|
19
|
+
order: 1,
|
|
20
|
+
columnWidth: 12,
|
|
21
|
+
},
|
|
22
|
+
api: {
|
|
23
|
+
readOnly: false,
|
|
24
|
+
searchable: true,
|
|
25
|
+
sortable: true,
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: 'position',
|
|
30
|
+
type: 'number',
|
|
31
|
+
required: false,
|
|
32
|
+
defaultValue: 0,
|
|
33
|
+
display: {
|
|
34
|
+
label: 'Position',
|
|
35
|
+
description: 'Display order within board',
|
|
36
|
+
showInList: false,
|
|
37
|
+
showInDetail: false,
|
|
38
|
+
showInForm: false,
|
|
39
|
+
order: 2,
|
|
40
|
+
},
|
|
41
|
+
api: {
|
|
42
|
+
readOnly: false,
|
|
43
|
+
searchable: false,
|
|
44
|
+
sortable: true,
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: 'boardId',
|
|
49
|
+
type: 'reference',
|
|
50
|
+
required: true,
|
|
51
|
+
referenceEntity: 'boards',
|
|
52
|
+
display: {
|
|
53
|
+
label: 'Board',
|
|
54
|
+
description: 'Parent board',
|
|
55
|
+
placeholder: 'Select board...',
|
|
56
|
+
showInList: true,
|
|
57
|
+
showInDetail: true,
|
|
58
|
+
showInForm: true,
|
|
59
|
+
order: 3,
|
|
60
|
+
columnWidth: 12,
|
|
61
|
+
},
|
|
62
|
+
api: {
|
|
63
|
+
readOnly: false,
|
|
64
|
+
searchable: false,
|
|
65
|
+
sortable: false,
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: 'createdAt',
|
|
70
|
+
type: 'datetime',
|
|
71
|
+
required: false,
|
|
72
|
+
display: {
|
|
73
|
+
label: 'Created At',
|
|
74
|
+
description: 'When the list was created',
|
|
75
|
+
showInList: false,
|
|
76
|
+
showInDetail: true,
|
|
77
|
+
showInForm: false,
|
|
78
|
+
order: 98,
|
|
79
|
+
},
|
|
80
|
+
api: {
|
|
81
|
+
readOnly: true,
|
|
82
|
+
searchable: false,
|
|
83
|
+
sortable: true,
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: 'updatedAt',
|
|
88
|
+
type: 'datetime',
|
|
89
|
+
required: false,
|
|
90
|
+
display: {
|
|
91
|
+
label: 'Updated At',
|
|
92
|
+
description: 'When the list was last modified',
|
|
93
|
+
showInList: false,
|
|
94
|
+
showInDetail: true,
|
|
95
|
+
showInForm: false,
|
|
96
|
+
order: 99,
|
|
97
|
+
},
|
|
98
|
+
api: {
|
|
99
|
+
readOnly: true,
|
|
100
|
+
searchable: false,
|
|
101
|
+
sortable: true,
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
]
|
|
105
|
+
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lists Service
|
|
3
|
+
* Provides data access methods for lists entity.
|
|
4
|
+
*/
|
|
5
|
+
import { queryOneWithRLS, queryWithRLS, mutateWithRLS } from '@nextsparkjs/core/lib/db'
|
|
6
|
+
|
|
7
|
+
export interface List {
|
|
8
|
+
id: string
|
|
9
|
+
name: string
|
|
10
|
+
boardId: string
|
|
11
|
+
position: number
|
|
12
|
+
isArchived: boolean
|
|
13
|
+
userId: string
|
|
14
|
+
teamId: string
|
|
15
|
+
createdAt: Date
|
|
16
|
+
updatedAt: Date
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ListListOptions {
|
|
20
|
+
limit?: number
|
|
21
|
+
offset?: number
|
|
22
|
+
orderBy?: string
|
|
23
|
+
orderDir?: 'asc' | 'desc'
|
|
24
|
+
teamId?: string
|
|
25
|
+
boardId?: string
|
|
26
|
+
isArchived?: boolean
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ListListResult {
|
|
30
|
+
lists: List[]
|
|
31
|
+
total: number
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ListCreateData {
|
|
35
|
+
name: string
|
|
36
|
+
boardId: string
|
|
37
|
+
position?: number
|
|
38
|
+
isArchived?: boolean
|
|
39
|
+
teamId: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ListUpdateData {
|
|
43
|
+
name?: string
|
|
44
|
+
boardId?: string
|
|
45
|
+
position?: number
|
|
46
|
+
isArchived?: boolean
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface DbList {
|
|
50
|
+
id: string
|
|
51
|
+
name: string
|
|
52
|
+
board_id: string
|
|
53
|
+
position: number
|
|
54
|
+
is_archived: boolean
|
|
55
|
+
user_id: string
|
|
56
|
+
team_id: string
|
|
57
|
+
created_at: string
|
|
58
|
+
updated_at: string
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function mapDbList(dbList: DbList): List {
|
|
62
|
+
return {
|
|
63
|
+
id: dbList.id,
|
|
64
|
+
name: dbList.name,
|
|
65
|
+
boardId: dbList.board_id,
|
|
66
|
+
position: dbList.position,
|
|
67
|
+
isArchived: dbList.is_archived,
|
|
68
|
+
userId: dbList.user_id,
|
|
69
|
+
teamId: dbList.team_id,
|
|
70
|
+
createdAt: new Date(dbList.created_at),
|
|
71
|
+
updatedAt: new Date(dbList.updated_at),
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export class ListsService {
|
|
76
|
+
/**
|
|
77
|
+
* Get a list by ID with RLS
|
|
78
|
+
*/
|
|
79
|
+
static async getById(id: string, userId: string): Promise<List | null> {
|
|
80
|
+
const result = await queryOneWithRLS<DbList>(
|
|
81
|
+
userId,
|
|
82
|
+
`SELECT * FROM lists WHERE id = $1`,
|
|
83
|
+
[id]
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
return result ? mapDbList(result) : null
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* List lists with RLS and filtering
|
|
91
|
+
*/
|
|
92
|
+
static async list(userId: string, options: ListListOptions = {}): Promise<ListListResult> {
|
|
93
|
+
const {
|
|
94
|
+
limit = 50,
|
|
95
|
+
offset = 0,
|
|
96
|
+
orderBy = 'position',
|
|
97
|
+
orderDir = 'asc',
|
|
98
|
+
teamId,
|
|
99
|
+
boardId,
|
|
100
|
+
isArchived,
|
|
101
|
+
} = options
|
|
102
|
+
|
|
103
|
+
const conditions: string[] = []
|
|
104
|
+
const params: any[] = []
|
|
105
|
+
let paramIndex = 1
|
|
106
|
+
|
|
107
|
+
if (teamId) {
|
|
108
|
+
conditions.push(`team_id = $${paramIndex++}`)
|
|
109
|
+
params.push(teamId)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (boardId) {
|
|
113
|
+
conditions.push(`board_id = $${paramIndex++}`)
|
|
114
|
+
params.push(boardId)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (isArchived !== undefined) {
|
|
118
|
+
conditions.push(`is_archived = $${paramIndex++}`)
|
|
119
|
+
params.push(isArchived)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''
|
|
123
|
+
|
|
124
|
+
// Get total count
|
|
125
|
+
const countResult = await queryOneWithRLS<{ count: string }>(
|
|
126
|
+
userId,
|
|
127
|
+
`SELECT COUNT(*) as count FROM lists ${whereClause}`,
|
|
128
|
+
params
|
|
129
|
+
)
|
|
130
|
+
const total = parseInt(countResult?.count || '0', 10)
|
|
131
|
+
|
|
132
|
+
// Get lists
|
|
133
|
+
const validOrderBy = ['position', 'name', 'created_at', 'updated_at'].includes(orderBy)
|
|
134
|
+
? orderBy
|
|
135
|
+
: 'position'
|
|
136
|
+
const validOrderDir = orderDir === 'desc' ? 'DESC' : 'ASC'
|
|
137
|
+
|
|
138
|
+
params.push(limit, offset)
|
|
139
|
+
const lists = await queryWithRLS<DbList>(
|
|
140
|
+
userId,
|
|
141
|
+
`SELECT * FROM lists ${whereClause} ORDER BY ${validOrderBy} ${validOrderDir} LIMIT $${paramIndex++} OFFSET $${paramIndex++}`,
|
|
142
|
+
params
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
lists: lists.map(mapDbList),
|
|
147
|
+
total,
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Get all lists for a specific board
|
|
153
|
+
*/
|
|
154
|
+
static async getByBoard(boardId: string, userId: string): Promise<List[]> {
|
|
155
|
+
const result = await this.list(userId, {
|
|
156
|
+
boardId,
|
|
157
|
+
orderBy: 'position',
|
|
158
|
+
orderDir: 'asc',
|
|
159
|
+
isArchived: false,
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
return result.lists
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Create a new list with RLS
|
|
167
|
+
*/
|
|
168
|
+
static async create(userId: string, data: ListCreateData): Promise<List> {
|
|
169
|
+
const {
|
|
170
|
+
name,
|
|
171
|
+
boardId,
|
|
172
|
+
position = 0,
|
|
173
|
+
isArchived = false,
|
|
174
|
+
teamId,
|
|
175
|
+
} = data
|
|
176
|
+
|
|
177
|
+
const result = await mutateWithRLS<DbList>(
|
|
178
|
+
userId,
|
|
179
|
+
`INSERT INTO lists (name, board_id, position, is_archived, user_id, team_id)
|
|
180
|
+
VALUES ($1, $2, $3, $4, $5, $6)
|
|
181
|
+
RETURNING *`,
|
|
182
|
+
[name, boardId, position, isArchived, userId, teamId]
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
if (!result) {
|
|
186
|
+
throw new Error('Failed to create list')
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return mapDbList(result)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Update a list with RLS
|
|
194
|
+
*/
|
|
195
|
+
static async update(userId: string, id: string, data: ListUpdateData): Promise<List> {
|
|
196
|
+
const updates: string[] = []
|
|
197
|
+
const params: any[] = []
|
|
198
|
+
let paramIndex = 1
|
|
199
|
+
|
|
200
|
+
if (data.name !== undefined) {
|
|
201
|
+
updates.push(`name = $${paramIndex++}`)
|
|
202
|
+
params.push(data.name)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (data.boardId !== undefined) {
|
|
206
|
+
updates.push(`board_id = $${paramIndex++}`)
|
|
207
|
+
params.push(data.boardId)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (data.position !== undefined) {
|
|
211
|
+
updates.push(`position = $${paramIndex++}`)
|
|
212
|
+
params.push(data.position)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (data.isArchived !== undefined) {
|
|
216
|
+
updates.push(`is_archived = $${paramIndex++}`)
|
|
217
|
+
params.push(data.isArchived)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (updates.length === 0) {
|
|
221
|
+
throw new Error('No fields to update')
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
updates.push(`updated_at = NOW()`)
|
|
225
|
+
params.push(id)
|
|
226
|
+
|
|
227
|
+
const result = await mutateWithRLS<DbList>(
|
|
228
|
+
userId,
|
|
229
|
+
`UPDATE lists SET ${updates.join(', ')} WHERE id = $${paramIndex} RETURNING *`,
|
|
230
|
+
params
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
if (!result) {
|
|
234
|
+
throw new Error('List not found or access denied')
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return mapDbList(result)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Delete a list with RLS
|
|
242
|
+
*/
|
|
243
|
+
static async delete(userId: string, id: string): Promise<boolean> {
|
|
244
|
+
const result = await mutateWithRLS<DbList>(
|
|
245
|
+
userId,
|
|
246
|
+
`DELETE FROM lists WHERE id = $1 RETURNING *`,
|
|
247
|
+
[id]
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
return result !== null
|
|
251
|
+
}
|
|
252
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* List Service Types
|
|
3
|
+
*
|
|
4
|
+
* Type definitions for the ListsService.
|
|
5
|
+
* Lists represent columns within a board that contain cards.
|
|
6
|
+
*
|
|
7
|
+
* @module ListsTypes
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// Main entity interface
|
|
11
|
+
export interface List {
|
|
12
|
+
id: string
|
|
13
|
+
name: string
|
|
14
|
+
boardId: string
|
|
15
|
+
position: number
|
|
16
|
+
isArchived: boolean
|
|
17
|
+
teamId: string
|
|
18
|
+
userId: string
|
|
19
|
+
createdAt: string
|
|
20
|
+
updatedAt: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// List options
|
|
24
|
+
export interface ListListOptions {
|
|
25
|
+
limit?: number
|
|
26
|
+
offset?: number
|
|
27
|
+
teamId?: string
|
|
28
|
+
boardId?: string
|
|
29
|
+
isArchived?: boolean
|
|
30
|
+
orderBy?: 'name' | 'position' | 'createdAt'
|
|
31
|
+
orderDir?: 'asc' | 'desc'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// List result
|
|
35
|
+
export interface ListListResult {
|
|
36
|
+
lists: List[]
|
|
37
|
+
total: number
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Create data (required fields + teamId + optional fields)
|
|
41
|
+
export interface ListCreateData {
|
|
42
|
+
name: string
|
|
43
|
+
boardId: string
|
|
44
|
+
teamId: string
|
|
45
|
+
position?: number
|
|
46
|
+
isArchived?: boolean
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Update data (all fields optional)
|
|
50
|
+
export interface ListUpdateData {
|
|
51
|
+
name?: string
|
|
52
|
+
boardId?: string
|
|
53
|
+
position?: number
|
|
54
|
+
isArchived?: boolean
|
|
55
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"title": "Lists",
|
|
3
|
+
"singular": "List",
|
|
4
|
+
"plural": "Lists",
|
|
5
|
+
"description": "Columns within boards to organize cards",
|
|
6
|
+
|
|
7
|
+
"fields": {
|
|
8
|
+
"name": {
|
|
9
|
+
"label": "Name",
|
|
10
|
+
"placeholder": "Enter list name...",
|
|
11
|
+
"description": "List name"
|
|
12
|
+
},
|
|
13
|
+
"position": {
|
|
14
|
+
"label": "Position",
|
|
15
|
+
"description": "Display order within board"
|
|
16
|
+
},
|
|
17
|
+
"boardId": {
|
|
18
|
+
"label": "Board",
|
|
19
|
+
"placeholder": "Select board...",
|
|
20
|
+
"description": "Parent board"
|
|
21
|
+
},
|
|
22
|
+
"createdAt": {
|
|
23
|
+
"label": "Created At",
|
|
24
|
+
"description": "When the list was created"
|
|
25
|
+
},
|
|
26
|
+
"updatedAt": {
|
|
27
|
+
"label": "Updated At",
|
|
28
|
+
"description": "When the list was last modified"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
"actions": {
|
|
33
|
+
"create": "Add List",
|
|
34
|
+
"edit": "Edit List",
|
|
35
|
+
"delete": "Delete List",
|
|
36
|
+
"moveLeft": "Move Left",
|
|
37
|
+
"moveRight": "Move Right"
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
"messages": {
|
|
41
|
+
"created": "List created successfully",
|
|
42
|
+
"updated": "List updated successfully",
|
|
43
|
+
"deleted": "List deleted successfully",
|
|
44
|
+
"moved": "List moved",
|
|
45
|
+
"confirmDelete": "Are you sure you want to delete this list? All cards within it will be permanently removed."
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
"empty": {
|
|
49
|
+
"title": "No lists yet",
|
|
50
|
+
"description": "Add a list to start organizing your cards.",
|
|
51
|
+
"action": "Add a list"
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
"defaults": {
|
|
55
|
+
"todo": "To Do",
|
|
56
|
+
"inProgress": "In Progress",
|
|
57
|
+
"done": "Done"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"title": "Listas",
|
|
3
|
+
"singular": "Lista",
|
|
4
|
+
"plural": "Listas",
|
|
5
|
+
"description": "Columnas dentro de los tableros para organizar tarjetas",
|
|
6
|
+
|
|
7
|
+
"fields": {
|
|
8
|
+
"name": {
|
|
9
|
+
"label": "Nombre",
|
|
10
|
+
"placeholder": "Ingresa el nombre de la lista...",
|
|
11
|
+
"description": "Nombre de la lista"
|
|
12
|
+
},
|
|
13
|
+
"position": {
|
|
14
|
+
"label": "Posición",
|
|
15
|
+
"description": "Orden de visualización dentro del tablero"
|
|
16
|
+
},
|
|
17
|
+
"boardId": {
|
|
18
|
+
"label": "Tablero",
|
|
19
|
+
"placeholder": "Seleccionar tablero...",
|
|
20
|
+
"description": "Tablero padre"
|
|
21
|
+
},
|
|
22
|
+
"createdAt": {
|
|
23
|
+
"label": "Creado",
|
|
24
|
+
"description": "Cuándo se creó la lista"
|
|
25
|
+
},
|
|
26
|
+
"updatedAt": {
|
|
27
|
+
"label": "Actualizado",
|
|
28
|
+
"description": "Cuándo se modificó por última vez"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
"actions": {
|
|
33
|
+
"create": "Agregar Lista",
|
|
34
|
+
"edit": "Editar Lista",
|
|
35
|
+
"delete": "Eliminar Lista",
|
|
36
|
+
"moveLeft": "Mover a la Izquierda",
|
|
37
|
+
"moveRight": "Mover a la Derecha"
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
"messages": {
|
|
41
|
+
"created": "Lista creada exitosamente",
|
|
42
|
+
"updated": "Lista actualizada exitosamente",
|
|
43
|
+
"deleted": "Lista eliminada exitosamente",
|
|
44
|
+
"moved": "Lista movida",
|
|
45
|
+
"confirmDelete": "¿Estás seguro de que quieres eliminar esta lista? Todas las tarjetas dentro de ella serán eliminadas permanentemente."
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
"empty": {
|
|
49
|
+
"title": "Aún no hay listas",
|
|
50
|
+
"description": "Agrega una lista para comenzar a organizar tus tarjetas.",
|
|
51
|
+
"action": "Agregar una lista"
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
"defaults": {
|
|
55
|
+
"todo": "Por Hacer",
|
|
56
|
+
"inProgress": "En Progreso",
|
|
57
|
+
"done": "Hecho"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|