create-lego-one 2.0.12 → 2.0.14
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/dist/index.cjs +150 -15
- package/dist/index.cjs.map +1 -1
- package/package.json +1 -1
- package/template/.cursor/rules/rules.mdc +639 -0
- package/template/.dockerignore +58 -0
- package/template/.env.example +18 -0
- package/template/.eslintignore +5 -0
- package/template/.eslintrc.js +28 -0
- package/template/.prettierignore +6 -0
- package/template/.prettierrc +11 -0
- package/template/CLAUDE.md +634 -0
- package/template/Dockerfile +67 -0
- package/template/PROMPT.md +457 -0
- package/template/README.md +325 -0
- package/template/docker-compose.yml +48 -0
- package/template/docker-entrypoint.sh +23 -0
- package/template/docs/checkpoints/.template.md +64 -0
- package/template/docs/checkpoints/framework/01-infrastructure-setup.md +132 -0
- package/template/docs/checkpoints/framework/02-pocketbase-setup.md +155 -0
- package/template/docs/checkpoints/framework/03-host-kernel.md +170 -0
- package/template/docs/checkpoints/framework/04-auth-system.md +163 -0
- package/template/docs/checkpoints/framework/phase-05-multitenancy-rbac.md +223 -0
- package/template/docs/checkpoints/framework/phase-06-ui-components.md +260 -0
- package/template/docs/checkpoints/framework/phase-07-communication-system.md +276 -0
- package/template/docs/checkpoints/framework/phase-08-plugin-system.md +91 -0
- package/template/docs/checkpoints/framework/phase-09-dashboard-plugin.md +111 -0
- package/template/docs/checkpoints/framework/phase-10-todo-plugin.md +169 -0
- package/template/docs/checkpoints/framework/phase-11-testing.md +264 -0
- package/template/docs/checkpoints/framework/phase-12-deployment.md +294 -0
- package/template/docs/checkpoints/framework/phase-13-documentation.md +312 -0
- package/template/docs/framework/plans/00-index.md +164 -0
- package/template/docs/framework/plans/01-infrastructure-setup.md +855 -0
- package/template/docs/framework/plans/02-pocketbase-setup.md +1374 -0
- package/template/docs/framework/plans/03-host-kernel.md +1518 -0
- package/template/docs/framework/plans/04-auth-system.md +1466 -0
- package/template/docs/framework/plans/05-multitenancy-rbac.md +1527 -0
- package/template/docs/framework/plans/06-ui-components.md +1478 -0
- package/template/docs/framework/plans/07-communication-system.md +1106 -0
- package/template/docs/framework/plans/08-plugin-system.md +1179 -0
- package/template/docs/framework/plans/09-dashboard-plugin.md +1137 -0
- package/template/docs/framework/plans/10-todo-plugin.md +1343 -0
- package/template/docs/framework/plans/11-testing.md +935 -0
- package/template/docs/framework/plans/12-deployment.md +896 -0
- package/template/docs/framework/prompts/0-boilerplate-modernjs.md +151 -0
- package/template/docs/framework/research/00-modernjs-audit.md +488 -0
- package/template/docs/framework/research/01-system-blueprint.md +721 -0
- package/template/docs/framework/research/02-data-migration-protocol.md +699 -0
- package/template/docs/framework/research/03-host-setup.md +714 -0
- package/template/docs/framework/research/04-plugin-architecture.md +645 -0
- package/template/docs/framework/research/05-slot-injection-pattern.md +671 -0
- package/template/docs/framework/research/06-cli-strategy.md +615 -0
- package/template/docs/framework/research/07-deployment.md +629 -0
- package/template/docs/framework/research/README.md +282 -0
- package/template/docs/framework/setup/00-index.md +210 -0
- package/template/docs/framework/setup/01-framework-structure.md +308 -0
- package/template/docs/framework/setup/02-development-workflow.md +405 -0
- package/template/docs/framework/setup/03-environment-setup.md +215 -0
- package/template/docs/framework/setup/04-kernel-architecture.md +499 -0
- package/template/docs/framework/setup/05-plugin-system.md +620 -0
- package/template/docs/framework/setup/06-communication-patterns.md +451 -0
- package/template/docs/framework/setup/07-plugin-development.md +582 -0
- package/template/docs/framework/setup/08-component-library.md +658 -0
- package/template/docs/framework/setup/09-data-integration.md +609 -0
- package/template/docs/framework/setup/10-auth-rbac.md +497 -0
- package/template/docs/framework/setup/11-hooks-api.md +393 -0
- package/template/docs/framework/setup/12-components-api.md +665 -0
- package/template/docs/framework/setup/13-deployment-guide.md +566 -0
- package/template/docs/framework/setup/README.md +548 -0
- package/template/host/package.json +1 -1
- package/template/nginx.conf +72 -0
- package/template/package.json +1 -1
- package/template/packages/plugins/@lego/plugin-dashboard/package.json +1 -1
- package/template/packages/plugins/@lego/plugin-todo/package.json +1 -1
- package/template/pocketbase/CHANGELOG.md +911 -0
- package/template/pocketbase/LICENSE.md +17 -0
- package/template/scripts/create-plugin.js +221 -0
- package/template/scripts/deploy.sh +56 -0
- package/template/tsconfig.base.json +26 -0
|
@@ -0,0 +1,1343 @@
|
|
|
1
|
+
# Todo Plugin Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For AI Implementing This Plan:** This is document 10 of 13. Complete documents 01-09 first.
|
|
4
|
+
|
|
5
|
+
**Goal:** Implement the Todo plugin with full CRUD functionality (Create, Read, Update, Delete), demonstrating a complete data-driven plugin with PocketBase integration.
|
|
6
|
+
|
|
7
|
+
**Architecture:** Todo plugin is a Modern.js app registered as a Garfish sub-app. It manages todo items stored in PocketBase with full multi-tenancy support. Uses TanStack Query for data fetching and mutations.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Modern.js, React, TypeScript, TanStack Query, PocketBase, Zod validation, Tailwind CSS
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Prerequisites
|
|
14
|
+
|
|
15
|
+
- ✅ Completed `01-infrastructure-setup.md`
|
|
16
|
+
- ✅ Completed `02-pocketbase-setup.md` (todos collection)
|
|
17
|
+
- ✅ Completed `03-host-kernel.md` through `09-dashboard-plugin.md`
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Task 1: Create Todo Plugin Structure
|
|
22
|
+
|
|
23
|
+
**Files:**
|
|
24
|
+
- Create: `packages/plugins/@lego/plugin-todo/package.json`
|
|
25
|
+
- Create: `packages/plugins/@lego/plugin-todo/tsconfig.json`
|
|
26
|
+
- Create: `packages/plugins/@lego/plugin-todo/modern.config.ts`
|
|
27
|
+
- Create: `packages/plugins/@lego/plugin-todo/.gitignore`
|
|
28
|
+
|
|
29
|
+
### Step 1: Create package.json
|
|
30
|
+
|
|
31
|
+
**File:** `packages/plugins/@lego/plugin-todo/package.json`
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{
|
|
35
|
+
"name": "@lego/plugin-todo",
|
|
36
|
+
"version": "1.0.0",
|
|
37
|
+
"private": true,
|
|
38
|
+
"type": "module",
|
|
39
|
+
"scripts": {
|
|
40
|
+
"dev": "modern dev",
|
|
41
|
+
"build": "modern build",
|
|
42
|
+
"start": "modern start",
|
|
43
|
+
"lint": "eslint src --ext .ts,.tsx",
|
|
44
|
+
"typecheck": "tsc --noEmit"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@garfish/hooks": "^1.22.0",
|
|
48
|
+
"@garfish/router": "^1.22.0",
|
|
49
|
+
"@garfish/react-scope": "^1.22.0",
|
|
50
|
+
"@hookform/resolvers": "^3.9.0",
|
|
51
|
+
"@modern-js/runtime": "^2.60.0",
|
|
52
|
+
"@modern-js/runtime/garfish": "^2.60.0",
|
|
53
|
+
"@radix-ui/react-dialog": "^1.1.2",
|
|
54
|
+
"@radix-ui/react-label": "^2.1.0",
|
|
55
|
+
"@radix-ui/react-slot": "^1.1.0",
|
|
56
|
+
"@tanstack/react-query": "^5.59.0",
|
|
57
|
+
"class-variance-authority": "^0.7.0",
|
|
58
|
+
"clsx": "^2.1.1",
|
|
59
|
+
"lucide-react": "^0.454.0",
|
|
60
|
+
"pocketbase": "^0.21.5",
|
|
61
|
+
"react": "^18.3.1",
|
|
62
|
+
"react-dom": "^18.3.1",
|
|
63
|
+
"react-hook-form": "^7.53.0",
|
|
64
|
+
"tailwind-merge": "^2.5.4",
|
|
65
|
+
"zod": "^3.23.8"
|
|
66
|
+
},
|
|
67
|
+
"devDependencies": {
|
|
68
|
+
"@modern-js/app-tools": "^2.60.0",
|
|
69
|
+
"@modern-js/plugin-garfish": "^2.60.0",
|
|
70
|
+
"@types/react": "^18.3.12",
|
|
71
|
+
"@types/react-dom": "^18.3.1",
|
|
72
|
+
"autoprefixer": "^10.4.20",
|
|
73
|
+
"postcss": "^8.4.49",
|
|
74
|
+
"tailwindcss": "^3.4.15",
|
|
75
|
+
"typescript": "^5.6.3"
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Step 2: Create tsconfig.json
|
|
81
|
+
|
|
82
|
+
**File:** `packages/plugins/@lego/plugin-todo/tsconfig.json`
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"extends": "@modern-js/tsconfig/base.json",
|
|
87
|
+
"compilerOptions": {
|
|
88
|
+
"jsx": "react-jsx",
|
|
89
|
+
"strict": true,
|
|
90
|
+
"esModuleInterop": true,
|
|
91
|
+
"skipLibCheck": true,
|
|
92
|
+
"moduleResolution": "bundler",
|
|
93
|
+
"resolveJsonModule": true,
|
|
94
|
+
"isolatedModules": true,
|
|
95
|
+
"paths": {
|
|
96
|
+
"@/*": ["./src/*"]
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
"include": ["src"],
|
|
100
|
+
"exclude": ["node_modules", "dist"]
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Step 3: Create modern.config.ts
|
|
105
|
+
|
|
106
|
+
**File:** `packages/plugins/@lego/plugin-todo/modern.config.ts`
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
import { appTools, defineConfig } from '@modern-js/app-tools';
|
|
110
|
+
import { garfishPlugin } from '@modern-js/plugin-garfish';
|
|
111
|
+
|
|
112
|
+
export default defineConfig({
|
|
113
|
+
dev: {
|
|
114
|
+
port: 3002,
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
runtime: {
|
|
118
|
+
router: true,
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
deploy: {
|
|
122
|
+
microFrontend: true,
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
plugins: [appTools(), garfishPlugin()],
|
|
126
|
+
});
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Step 4: Create .gitignore
|
|
130
|
+
|
|
131
|
+
**File:** `packages/plugins/@lego/plugin-todo/.gitignore`
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
node_modules/
|
|
135
|
+
dist/
|
|
136
|
+
.modern/
|
|
137
|
+
*.local
|
|
138
|
+
.DS_Store
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Step 5: Install dependencies
|
|
142
|
+
|
|
143
|
+
**Run:** From root directory
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
pnpm install
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Task 2: Create Todo Types and Schemas
|
|
152
|
+
|
|
153
|
+
**Files:**
|
|
154
|
+
- Create: `packages/plugins/@lego/plugin-todo/src/types.ts`
|
|
155
|
+
- Create: `packages/plugins/@lego/plugin-todo/src/schemas.ts`
|
|
156
|
+
|
|
157
|
+
### Step 1: Create todo types
|
|
158
|
+
|
|
159
|
+
**File:** `packages/plugins/@lego/plugin-todo/src/types.ts`
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
import type { Record } from 'pocketbase';
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Todo item from PocketBase
|
|
166
|
+
*/
|
|
167
|
+
export interface Todo {
|
|
168
|
+
id: string;
|
|
169
|
+
title: string;
|
|
170
|
+
description?: string;
|
|
171
|
+
completed: boolean;
|
|
172
|
+
priority: 'low' | 'medium' | 'high';
|
|
173
|
+
dueDate?: string;
|
|
174
|
+
organizationId: string;
|
|
175
|
+
ownerId: string;
|
|
176
|
+
created: string;
|
|
177
|
+
updated: string;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Todo form data
|
|
182
|
+
*/
|
|
183
|
+
export interface TodoFormData {
|
|
184
|
+
title: string;
|
|
185
|
+
description?: string;
|
|
186
|
+
priority: 'low' | 'medium' | 'high';
|
|
187
|
+
dueDate?: string;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Todo filter options
|
|
192
|
+
*/
|
|
193
|
+
export interface TodoFilters {
|
|
194
|
+
status?: 'all' | 'active' | 'completed';
|
|
195
|
+
priority?: 'all' | 'low' | 'medium' | 'high';
|
|
196
|
+
search?: string;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Todo list item with expand
|
|
201
|
+
*/
|
|
202
|
+
export interface TodoWithOwner extends Todo {
|
|
203
|
+
ownerName?: string;
|
|
204
|
+
ownerEmail?: string;
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Step 2: Create Zod schemas
|
|
209
|
+
|
|
210
|
+
**File:** `packages/plugins/@lego/plugin-todo/src/schemas.ts`
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
import { z } from 'zod';
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Todo form validation schema
|
|
217
|
+
*/
|
|
218
|
+
export const todoFormSchema = z.object({
|
|
219
|
+
title: z.string()
|
|
220
|
+
.min(1, 'Title is required')
|
|
221
|
+
.max(200, 'Title must be less than 200 characters'),
|
|
222
|
+
description: z.string()
|
|
223
|
+
.max(1000, 'Description must be less than 1000 characters')
|
|
224
|
+
.optional(),
|
|
225
|
+
priority: z.enum(['low', 'medium', 'high'], {
|
|
226
|
+
required_error: 'Priority is required',
|
|
227
|
+
}),
|
|
228
|
+
dueDate: z.string().optional(),
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
export type TodoFormInput = z.infer<typeof todoFormSchema>;
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Todo update schema (all fields optional)
|
|
235
|
+
*/
|
|
236
|
+
export const todoUpdateSchema = z.object({
|
|
237
|
+
title: z.string()
|
|
238
|
+
.min(1, 'Title is required')
|
|
239
|
+
.max(200, 'Title must be less than 200 characters')
|
|
240
|
+
.optional(),
|
|
241
|
+
description: z.string()
|
|
242
|
+
.max(1000, 'Description must be less than 1000 characters')
|
|
243
|
+
.optional(),
|
|
244
|
+
completed: z.boolean().optional(),
|
|
245
|
+
priority: z.enum(['low', 'medium', 'high']).optional(),
|
|
246
|
+
dueDate: z.string().optional(),
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
export type TodoUpdateInput = z.infer<typeof todoUpdateSchema>;
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## Task 3: Create Plugin Configuration
|
|
255
|
+
|
|
256
|
+
**Files:**
|
|
257
|
+
- Create: `packages/plugins/@lego/plugin-todo/src/plugin.config.ts`
|
|
258
|
+
- Create: `packages/plugins/@lego/plugin-todo/src/plugin.ts`
|
|
259
|
+
|
|
260
|
+
### Step 1: Create plugin config
|
|
261
|
+
|
|
262
|
+
**File:** `packages/plugins/@lego/plugin-todo/src/plugin.config.ts`
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
import type { PluginConfig } from '@lego/kernel/plugins';
|
|
266
|
+
import { SidebarWidget } from './components/SidebarWidget';
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Todo plugin configuration
|
|
270
|
+
*/
|
|
271
|
+
export const pluginConfig: PluginConfig = {
|
|
272
|
+
manifest: {
|
|
273
|
+
name: '@lego/plugin-todo',
|
|
274
|
+
version: '1.0.0',
|
|
275
|
+
displayName: 'Todo',
|
|
276
|
+
description: 'Task management with full CRUD functionality',
|
|
277
|
+
author: 'Lego-One',
|
|
278
|
+
permissions: ['todos.manage'],
|
|
279
|
+
},
|
|
280
|
+
|
|
281
|
+
enabled: true,
|
|
282
|
+
|
|
283
|
+
slots: [
|
|
284
|
+
{
|
|
285
|
+
slot: 'sidebar:nav',
|
|
286
|
+
component: SidebarWidget,
|
|
287
|
+
order: 60, // After Dashboard
|
|
288
|
+
props: {
|
|
289
|
+
to: '/todos',
|
|
290
|
+
icon: 'CheckSquare',
|
|
291
|
+
label: 'Todos',
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
],
|
|
295
|
+
|
|
296
|
+
routes: [
|
|
297
|
+
{
|
|
298
|
+
path: '/todos',
|
|
299
|
+
component: () => import('./pages/TodoPage').then(m => m.default),
|
|
300
|
+
protected: true,
|
|
301
|
+
permissions: ['todos.read'],
|
|
302
|
+
},
|
|
303
|
+
],
|
|
304
|
+
|
|
305
|
+
settings: [
|
|
306
|
+
{
|
|
307
|
+
key: 'defaultPriority',
|
|
308
|
+
type: 'select',
|
|
309
|
+
label: 'Default Priority',
|
|
310
|
+
description: 'Default priority for new todos',
|
|
311
|
+
defaultValue: 'medium',
|
|
312
|
+
options: [
|
|
313
|
+
{ label: 'Low', value: 'low' },
|
|
314
|
+
{ label: 'Medium', value: 'medium' },
|
|
315
|
+
{ label: 'High', value: 'high' },
|
|
316
|
+
],
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
key: 'showCompleted',
|
|
320
|
+
type: 'boolean',
|
|
321
|
+
label: 'Show Completed',
|
|
322
|
+
description: 'Show completed todos in the list',
|
|
323
|
+
defaultValue: true,
|
|
324
|
+
},
|
|
325
|
+
],
|
|
326
|
+
};
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### Step 2: Create plugin entry point
|
|
330
|
+
|
|
331
|
+
**File:** `packages/plugins/@lego/plugin-todo/src/plugin.ts`
|
|
332
|
+
|
|
333
|
+
```typescript
|
|
334
|
+
/**
|
|
335
|
+
* Plugin entry point
|
|
336
|
+
*/
|
|
337
|
+
import { pluginConfig } from './plugin.config';
|
|
338
|
+
import TodoApp from './App';
|
|
339
|
+
|
|
340
|
+
export default {
|
|
341
|
+
config: pluginConfig,
|
|
342
|
+
App: TodoApp,
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
export { pluginConfig };
|
|
346
|
+
export { default as App } from './App';
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
---
|
|
350
|
+
|
|
351
|
+
## Task 4: Create Todo Hooks (PocketBase Integration)
|
|
352
|
+
|
|
353
|
+
**Files:**
|
|
354
|
+
- Create: `packages/plugins/@lego/plugin-todo/src/hooks/usePocketBase.ts`
|
|
355
|
+
- Create: `packages/plugins/@lego/plugin-todo/src/hooks/useTodos.ts`
|
|
356
|
+
- Create: `packages/plugins/@lego/plugin-todo/src/hooks/useCreateTodo.ts`
|
|
357
|
+
- Create: `packages/plugins/@lego/plugin-todo/src/hooks/useUpdateTodo.ts`
|
|
358
|
+
- Create: `packages/plugins/@lego/plugin-todo/src/hooks/useDeleteTodo.ts`
|
|
359
|
+
|
|
360
|
+
### Step 1: Create PocketBase hook
|
|
361
|
+
|
|
362
|
+
**File:** `packages/plugins/@lego/plugin-todo/src/hooks/usePocketBase.ts`
|
|
363
|
+
|
|
364
|
+
```typescript
|
|
365
|
+
import { useEffect, useState } from 'react';
|
|
366
|
+
|
|
367
|
+
export function usePocketBase() {
|
|
368
|
+
const [pb, setPb] = useState<any>(null);
|
|
369
|
+
|
|
370
|
+
useEffect(() => {
|
|
371
|
+
const kernelState = (window as any).__LEGO_KERNEL_STATE__;
|
|
372
|
+
|
|
373
|
+
if (!kernelState) {
|
|
374
|
+
console.error('[Todo] Kernel state bridge not found');
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const pbUrl = import.meta.env.VITE_POCKETBASE_URL || 'http://127.0.0.1:8090';
|
|
379
|
+
const PocketBase = require('pocketbase').default;
|
|
380
|
+
const client = new PocketBase(pbUrl);
|
|
381
|
+
|
|
382
|
+
// Sync auth token
|
|
383
|
+
const state = kernelState.useGlobalKernelState.getState();
|
|
384
|
+
if (state.token) {
|
|
385
|
+
client.authStore.save(state.token, null);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const unsubscribe = kernelState.useGlobalKernelState.subscribe((newState: any) => {
|
|
389
|
+
if (newState.token && newState.token !== client.authStore.token) {
|
|
390
|
+
client.authStore.save(newState.token, null);
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
setPb(client);
|
|
395
|
+
|
|
396
|
+
return () => {
|
|
397
|
+
unsubscribe();
|
|
398
|
+
};
|
|
399
|
+
}, []);
|
|
400
|
+
|
|
401
|
+
return pb;
|
|
402
|
+
}
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
### Step 2: Create useTodos hook
|
|
406
|
+
|
|
407
|
+
**File:** `packages/plugins/@lego/plugin-todo/src/hooks/useTodos.ts`
|
|
408
|
+
|
|
409
|
+
```typescript
|
|
410
|
+
import { useQuery } from '@tanstack/react-query';
|
|
411
|
+
import { usePocketBase } from './usePocketBase';
|
|
412
|
+
import type { TodoWithOwner, TodoFilters } from '../types';
|
|
413
|
+
|
|
414
|
+
export function useTodos(filters: TodoFilters = {}) {
|
|
415
|
+
const pb = usePocketBase();
|
|
416
|
+
|
|
417
|
+
return useQuery({
|
|
418
|
+
queryKey: ['todos', filters],
|
|
419
|
+
queryFn: async (): Promise<TodoWithOwner[]> => {
|
|
420
|
+
if (!pb) {
|
|
421
|
+
throw new Error('PocketBase not initialized');
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const kernelState = (window as any).__LEGO_KERNEL_STATE__;
|
|
425
|
+
const state = kernelState?.useGlobalKernelState?.getState();
|
|
426
|
+
|
|
427
|
+
if (!state?.organization?.id) {
|
|
428
|
+
return [];
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const orgId = state.organization.id;
|
|
432
|
+
|
|
433
|
+
// Build filter
|
|
434
|
+
let filterParts: string[] = [`organizationId = "${orgId}"`];
|
|
435
|
+
|
|
436
|
+
if (filters.status === 'active') {
|
|
437
|
+
filterParts.push('completed = false');
|
|
438
|
+
} else if (filters.status === 'completed') {
|
|
439
|
+
filterParts.push('completed = true');
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (filters.priority && filters.priority !== 'all') {
|
|
443
|
+
filterParts.push(`priority = "${filters.priority}"`);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (filters.search) {
|
|
447
|
+
filterParts.push(`title ~ "${filters.search}"`);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const result = await pb.collection('todos').getList(1, 50, {
|
|
451
|
+
filter: filterParts.join(' && '),
|
|
452
|
+
sort: filters.status === 'completed' ? '-updated,-created' : '-created',
|
|
453
|
+
expand: 'ownerId',
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
return result.items.map((item: any) => ({
|
|
457
|
+
id: item.id,
|
|
458
|
+
title: item.title,
|
|
459
|
+
description: item.description,
|
|
460
|
+
completed: item.completed,
|
|
461
|
+
priority: item.priority,
|
|
462
|
+
dueDate: item.dueDate,
|
|
463
|
+
organizationId: item.organizationId,
|
|
464
|
+
ownerId: item.ownerId,
|
|
465
|
+
created: item.created,
|
|
466
|
+
updated: item.updated,
|
|
467
|
+
ownerName: item.expand?.ownerId?.name || item.expand?.ownerId?.email,
|
|
468
|
+
ownerEmail: item.expand?.ownerId?.email,
|
|
469
|
+
}));
|
|
470
|
+
},
|
|
471
|
+
enabled: !!pb,
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
### Step 3: Create useCreateTodo hook
|
|
477
|
+
|
|
478
|
+
**File:** `packages/plugins/@lego/plugin-todo/src/hooks/useCreateTodo.ts`
|
|
479
|
+
|
|
480
|
+
```typescript
|
|
481
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
482
|
+
import { usePocketBase } from './usePocketBase';
|
|
483
|
+
import type { TodoFormData } from '../types';
|
|
484
|
+
import { useToastChannel } from '@lego/kernel/channels';
|
|
485
|
+
|
|
486
|
+
export function useCreateTodo() {
|
|
487
|
+
const pb = usePocketBase();
|
|
488
|
+
const queryClient = useQueryClient();
|
|
489
|
+
const publishToast = useToastChannel();
|
|
490
|
+
|
|
491
|
+
return useMutation({
|
|
492
|
+
mutationFn: async (data: TodoFormData) => {
|
|
493
|
+
if (!pb) {
|
|
494
|
+
throw new Error('PocketBase not initialized');
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const kernelState = (window as any).__LEGO_KERNEL_STATE__;
|
|
498
|
+
const state = kernelState?.useGlobalKernelState?.getState();
|
|
499
|
+
|
|
500
|
+
if (!state?.organization?.id) {
|
|
501
|
+
throw new Error('No organization selected');
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const result = await pb.collection('todos').create({
|
|
505
|
+
title: data.title,
|
|
506
|
+
description: data.description || '',
|
|
507
|
+
completed: false,
|
|
508
|
+
priority: data.priority,
|
|
509
|
+
dueDate: data.dueDate || null,
|
|
510
|
+
organizationId: state.organization.id,
|
|
511
|
+
ownerId: state.user?.id,
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
return result;
|
|
515
|
+
},
|
|
516
|
+
onSuccess: () => {
|
|
517
|
+
queryClient.invalidateQueries({ queryKey: ['todos'] });
|
|
518
|
+
publishToast({
|
|
519
|
+
type: 'success',
|
|
520
|
+
title: 'Todo created',
|
|
521
|
+
description: 'Your todo has been created successfully',
|
|
522
|
+
});
|
|
523
|
+
},
|
|
524
|
+
onError: (error: any) => {
|
|
525
|
+
publishToast({
|
|
526
|
+
type: 'error',
|
|
527
|
+
title: 'Failed to create todo',
|
|
528
|
+
description: error.message || 'An error occurred',
|
|
529
|
+
});
|
|
530
|
+
},
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
### Step 4: Create useUpdateTodo hook
|
|
536
|
+
|
|
537
|
+
**File:** `packages/plugins/@lego/plugin-todo/src/hooks/useUpdateTodo.ts`
|
|
538
|
+
|
|
539
|
+
```typescript
|
|
540
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
541
|
+
import { usePocketBase } from './usePocketBase';
|
|
542
|
+
import { useToastChannel } from '@lego/kernel/channels';
|
|
543
|
+
|
|
544
|
+
export function useUpdateTodo() {
|
|
545
|
+
const pb = usePocketBase();
|
|
546
|
+
const queryClient = useQueryClient();
|
|
547
|
+
const publishToast = useToastChannel();
|
|
548
|
+
|
|
549
|
+
return useMutation({
|
|
550
|
+
mutationFn: async ({ id, data }: { id: string; data: Partial<any> }) => {
|
|
551
|
+
if (!pb) {
|
|
552
|
+
throw new Error('PocketBase not initialized');
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const result = await pb.collection('todos').update(id, data);
|
|
556
|
+
|
|
557
|
+
return result;
|
|
558
|
+
},
|
|
559
|
+
onSuccess: () => {
|
|
560
|
+
queryClient.invalidateQueries({ queryKey: ['todos'] });
|
|
561
|
+
},
|
|
562
|
+
onError: (error: any) => {
|
|
563
|
+
publishToast({
|
|
564
|
+
type: 'error',
|
|
565
|
+
title: 'Failed to update todo',
|
|
566
|
+
description: error.message || 'An error occurred',
|
|
567
|
+
});
|
|
568
|
+
},
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
### Step 5: Create useDeleteTodo hook
|
|
574
|
+
|
|
575
|
+
**File:** `packages/plugins/@lego/plugin-todo/src/hooks/useDeleteTodo.ts`
|
|
576
|
+
|
|
577
|
+
```typescript
|
|
578
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
579
|
+
import { usePocketBase } from './usePocketBase';
|
|
580
|
+
import { useToastChannel } from '@lego/kernel/channels';
|
|
581
|
+
|
|
582
|
+
export function useDeleteTodo() {
|
|
583
|
+
const pb = usePocketBase();
|
|
584
|
+
const queryClient = useQueryClient();
|
|
585
|
+
const publishToast = useToastChannel();
|
|
586
|
+
|
|
587
|
+
return useMutation({
|
|
588
|
+
mutationFn: async (id: string) => {
|
|
589
|
+
if (!pb) {
|
|
590
|
+
throw new Error('PocketBase not initialized');
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
await pb.collection('todos').delete(id);
|
|
594
|
+
|
|
595
|
+
return id;
|
|
596
|
+
},
|
|
597
|
+
onSuccess: () => {
|
|
598
|
+
queryClient.invalidateQueries({ queryKey: ['todos'] });
|
|
599
|
+
publishToast({
|
|
600
|
+
type: 'success',
|
|
601
|
+
title: 'Todo deleted',
|
|
602
|
+
description: 'Your todo has been deleted',
|
|
603
|
+
});
|
|
604
|
+
},
|
|
605
|
+
onError: (error: any) => {
|
|
606
|
+
publishToast({
|
|
607
|
+
type: 'error',
|
|
608
|
+
title: 'Failed to delete todo',
|
|
609
|
+
description: error.message || 'An error occurred',
|
|
610
|
+
});
|
|
611
|
+
},
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
---
|
|
617
|
+
|
|
618
|
+
## Task 5: Create Todo Components
|
|
619
|
+
|
|
620
|
+
**Files:**
|
|
621
|
+
- Create: `packages/plugins/@lego/plugin-todo/src/components/TodoList.tsx`
|
|
622
|
+
- Create: `packages/plugins/@lego/plugin-todo/src/components/TodoItem.tsx`
|
|
623
|
+
- Create: `packages/plugins/@lego/plugin-todo/src/components/TodoForm.tsx`
|
|
624
|
+
- Create: `packages/plugins/@lego/plugin-todo/src/components/TodoDialog.tsx`
|
|
625
|
+
- Create: `packages/plugins/@lego/plugin-todo/src/components/TodoFilters.tsx`
|
|
626
|
+
- Create: `packages/plugins/@lego/plugin-todo/src/components/SidebarWidget.tsx`
|
|
627
|
+
|
|
628
|
+
### Step 1: Create todo list component
|
|
629
|
+
|
|
630
|
+
**File:** `packages/plugins/@lego/plugin-todo/src/components/TodoList.tsx`
|
|
631
|
+
|
|
632
|
+
```typescript
|
|
633
|
+
import { TodoWithOwner } from '../types';
|
|
634
|
+
import { TodoItem } from './TodoItem';
|
|
635
|
+
|
|
636
|
+
interface TodoListProps {
|
|
637
|
+
todos: TodoWithOwner[];
|
|
638
|
+
isLoading?: boolean;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
export function TodoList({ todos, isLoading }: TodoListProps) {
|
|
642
|
+
if (isLoading) {
|
|
643
|
+
return (
|
|
644
|
+
<div className="space-y-3">
|
|
645
|
+
{[1, 2, 3, 4, 5].map((i) => (
|
|
646
|
+
<div key={i} className="h-20 animate-pulse rounded-lg bg-muted" />
|
|
647
|
+
))}
|
|
648
|
+
</div>
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
if (todos.length === 0) {
|
|
653
|
+
return (
|
|
654
|
+
<div className="flex flex-col items-center justify-center py-12 text-center">
|
|
655
|
+
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-muted">
|
|
656
|
+
<span className="text-2xl">📝</span>
|
|
657
|
+
</div>
|
|
658
|
+
<h3 className="mt-4 text-lg font-semibold">No todos found</h3>
|
|
659
|
+
<p className="mt-2 text-sm text-muted-foreground">
|
|
660
|
+
Create your first todo to get started
|
|
661
|
+
</p>
|
|
662
|
+
</div>
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
return (
|
|
667
|
+
<div className="space-y-3">
|
|
668
|
+
{todos.map((todo) => (
|
|
669
|
+
<TodoItem key={todo.id} todo={todo} />
|
|
670
|
+
))}
|
|
671
|
+
</div>
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
### Step 2: Create todo item component
|
|
677
|
+
|
|
678
|
+
**File:** `packages/plugins/@lego/plugin-todo/src/components/TodoItem.tsx`
|
|
679
|
+
|
|
680
|
+
```typescript
|
|
681
|
+
import { useState } from 'react';
|
|
682
|
+
import { CheckSquare, Square, Trash2, Edit2, Calendar } from 'lucide-react';
|
|
683
|
+
import { TodoWithOwner } from '../types';
|
|
684
|
+
import { useUpdateTodo } from '../hooks/useUpdateTodo';
|
|
685
|
+
import { useDeleteTodo } from '../hooks/useDeleteTodo';
|
|
686
|
+
import { cn } from '@lego/kernel/lib/utils';
|
|
687
|
+
import { formatRelativeTime } from '@lego/kernel/lib/utils';
|
|
688
|
+
|
|
689
|
+
interface TodoItemProps {
|
|
690
|
+
todo: TodoWithOwner;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
const priorityColors = {
|
|
694
|
+
low: 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300',
|
|
695
|
+
medium: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900 dark:text-yellow-300',
|
|
696
|
+
high: 'bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-300',
|
|
697
|
+
};
|
|
698
|
+
|
|
699
|
+
export function TodoItem({ todo }: TodoItemProps) {
|
|
700
|
+
const updateTodo = useUpdateTodo();
|
|
701
|
+
const deleteTodo = useDeleteTodo();
|
|
702
|
+
const [isEditing, setIsEditing] = useState(false);
|
|
703
|
+
|
|
704
|
+
const handleToggleComplete = () => {
|
|
705
|
+
updateTodo.mutate({
|
|
706
|
+
id: todo.id,
|
|
707
|
+
data: { completed: !todo.completed },
|
|
708
|
+
});
|
|
709
|
+
};
|
|
710
|
+
|
|
711
|
+
const handleDelete = () => {
|
|
712
|
+
if (confirm('Are you sure you want to delete this todo?')) {
|
|
713
|
+
deleteTodo.mutate(todo.id);
|
|
714
|
+
}
|
|
715
|
+
};
|
|
716
|
+
|
|
717
|
+
return (
|
|
718
|
+
<div
|
|
719
|
+
className={cn(
|
|
720
|
+
'group rounded-lg border p-4 transition-colors hover:bg-muted/50',
|
|
721
|
+
todo.completed && 'opacity-60'
|
|
722
|
+
)}
|
|
723
|
+
>
|
|
724
|
+
<div className="flex items-start gap-3">
|
|
725
|
+
{/* Checkbox */}
|
|
726
|
+
<button
|
|
727
|
+
onClick={handleToggleComplete}
|
|
728
|
+
disabled={updateTodo.isPending}
|
|
729
|
+
className="mt-0.5 shrink-0"
|
|
730
|
+
>
|
|
731
|
+
{todo.completed ? (
|
|
732
|
+
<CheckSquare className="h-5 w-5 text-primary" />
|
|
733
|
+
) : (
|
|
734
|
+
<Square className="h-5 w-5 text-muted-foreground" />
|
|
735
|
+
)}
|
|
736
|
+
</button>
|
|
737
|
+
|
|
738
|
+
{/* Content */}
|
|
739
|
+
<div className="flex-1 space-y-1">
|
|
740
|
+
<div className="flex items-start justify-between gap-2">
|
|
741
|
+
<h4
|
|
742
|
+
className={cn(
|
|
743
|
+
'font-medium',
|
|
744
|
+
todo.completed && 'line-through text-muted-foreground'
|
|
745
|
+
)}
|
|
746
|
+
>
|
|
747
|
+
{todo.title}
|
|
748
|
+
</h4>
|
|
749
|
+
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100">
|
|
750
|
+
<button
|
|
751
|
+
onClick={() => setIsEditing(true)}
|
|
752
|
+
className="rounded p-1 hover:bg-muted"
|
|
753
|
+
>
|
|
754
|
+
<Edit2 className="h-4 w-4" />
|
|
755
|
+
</button>
|
|
756
|
+
<button
|
|
757
|
+
onClick={handleDelete}
|
|
758
|
+
disabled={deleteTodo.isPending}
|
|
759
|
+
className="rounded p-1 hover:bg-destructive hover:text-destructive-foreground"
|
|
760
|
+
>
|
|
761
|
+
<Trash2 className="h-4 w-4" />
|
|
762
|
+
</button>
|
|
763
|
+
</div>
|
|
764
|
+
</div>
|
|
765
|
+
|
|
766
|
+
{todo.description && (
|
|
767
|
+
<p className="text-sm text-muted-foreground">{todo.description}</p>
|
|
768
|
+
)}
|
|
769
|
+
|
|
770
|
+
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
|
771
|
+
<span className={cn('rounded-full px-2 py-0.5', priorityColors[todo.priority])}>
|
|
772
|
+
{todo.priority}
|
|
773
|
+
</span>
|
|
774
|
+
{todo.dueDate && (
|
|
775
|
+
<span className="flex items-center gap-1">
|
|
776
|
+
<Calendar className="h-3 w-3" />
|
|
777
|
+
{formatRelativeTime(todo.dueDate)}
|
|
778
|
+
</span>
|
|
779
|
+
)}
|
|
780
|
+
{todo.ownerName && (
|
|
781
|
+
<span>Created by {todo.ownerName}</span>
|
|
782
|
+
)}
|
|
783
|
+
</div>
|
|
784
|
+
</div>
|
|
785
|
+
</div>
|
|
786
|
+
|
|
787
|
+
{/* Edit dialog would go here - for now using inline state */}
|
|
788
|
+
{isEditing && (
|
|
789
|
+
<div className="mt-4 pt-4 border-t">
|
|
790
|
+
<p className="text-sm text-muted-foreground">Edit functionality coming soon</p>
|
|
791
|
+
<button
|
|
792
|
+
onClick={() => setIsEditing(false)}
|
|
793
|
+
className="mt-2 text-sm text-primary hover:underline"
|
|
794
|
+
>
|
|
795
|
+
Close
|
|
796
|
+
</button>
|
|
797
|
+
</div>
|
|
798
|
+
)}
|
|
799
|
+
</div>
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
```
|
|
803
|
+
|
|
804
|
+
### Step 3: Create todo form component
|
|
805
|
+
|
|
806
|
+
**File:** `packages/plugins/@lego/plugin-todo/src/components/TodoForm.tsx`
|
|
807
|
+
|
|
808
|
+
```typescript
|
|
809
|
+
import { useForm } from 'react-hook-form';
|
|
810
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
811
|
+
import { todoFormSchema, type TodoFormInput } from '../schemas';
|
|
812
|
+
import { Button } from '@lego/kernel/components';
|
|
813
|
+
import { Input } from '@lego/kernel/components';
|
|
814
|
+
import { Label } from '@lego/kernel/components';
|
|
815
|
+
import {
|
|
816
|
+
Select,
|
|
817
|
+
SelectContent,
|
|
818
|
+
SelectItem,
|
|
819
|
+
SelectTrigger,
|
|
820
|
+
SelectValue,
|
|
821
|
+
} from '@lego/kernel/components';
|
|
822
|
+
|
|
823
|
+
interface TodoFormProps {
|
|
824
|
+
onSubmit: (data: TodoFormInput) => void;
|
|
825
|
+
isLoading?: boolean;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
export function TodoForm({ onSubmit, isLoading }: TodoFormProps) {
|
|
829
|
+
const {
|
|
830
|
+
register,
|
|
831
|
+
handleSubmit,
|
|
832
|
+
setValue,
|
|
833
|
+
formState: { errors },
|
|
834
|
+
} = useForm<TodoFormInput>({
|
|
835
|
+
resolver: zodResolver(todoFormSchema),
|
|
836
|
+
defaultValues: {
|
|
837
|
+
priority: 'medium',
|
|
838
|
+
},
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
return (
|
|
842
|
+
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
|
843
|
+
<div className="space-y-2">
|
|
844
|
+
<Label htmlFor="title">Title *</Label>
|
|
845
|
+
<Input
|
|
846
|
+
id="title"
|
|
847
|
+
placeholder="What needs to be done?"
|
|
848
|
+
{...register('title')}
|
|
849
|
+
/>
|
|
850
|
+
{errors.title && (
|
|
851
|
+
<p className="text-sm text-destructive">{errors.title.message}</p>
|
|
852
|
+
)}
|
|
853
|
+
</div>
|
|
854
|
+
|
|
855
|
+
<div className="space-y-2">
|
|
856
|
+
<Label htmlFor="description">Description</Label>
|
|
857
|
+
<Input
|
|
858
|
+
id="description"
|
|
859
|
+
placeholder="Add details (optional)"
|
|
860
|
+
{...register('description')}
|
|
861
|
+
/>
|
|
862
|
+
{errors.description && (
|
|
863
|
+
<p className="text-sm text-destructive">{errors.description.message}</p>
|
|
864
|
+
)}
|
|
865
|
+
</div>
|
|
866
|
+
|
|
867
|
+
<div className="space-y-2">
|
|
868
|
+
<Label htmlFor="priority">Priority *</Label>
|
|
869
|
+
<Select
|
|
870
|
+
onValueChange={(value) => setValue('priority', value as any)}
|
|
871
|
+
defaultValue="medium"
|
|
872
|
+
>
|
|
873
|
+
<SelectTrigger>
|
|
874
|
+
<SelectValue placeholder="Select priority" />
|
|
875
|
+
</SelectTrigger>
|
|
876
|
+
<SelectContent>
|
|
877
|
+
<SelectItem value="low">Low</SelectItem>
|
|
878
|
+
<SelectItem value="medium">Medium</SelectItem>
|
|
879
|
+
<SelectItem value="high">High</SelectItem>
|
|
880
|
+
</SelectContent>
|
|
881
|
+
</Select>
|
|
882
|
+
{errors.priority && (
|
|
883
|
+
<p className="text-sm text-destructive">{errors.priority.message}</p>
|
|
884
|
+
)}
|
|
885
|
+
</div>
|
|
886
|
+
|
|
887
|
+
<div className="space-y-2">
|
|
888
|
+
<Label htmlFor="dueDate">Due Date</Label>
|
|
889
|
+
<Input
|
|
890
|
+
id="dueDate"
|
|
891
|
+
type="date"
|
|
892
|
+
{...register('dueDate')}
|
|
893
|
+
/>
|
|
894
|
+
</div>
|
|
895
|
+
|
|
896
|
+
<Button type="submit" className="w-full" disabled={isLoading}>
|
|
897
|
+
{isLoading ? 'Creating...' : 'Create Todo'}
|
|
898
|
+
</Button>
|
|
899
|
+
</form>
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
```
|
|
903
|
+
|
|
904
|
+
### Step 4: Create todo dialog component
|
|
905
|
+
|
|
906
|
+
**File:** `packages/plugins/@lego/plugin-todo/src/components/TodoDialog.tsx`
|
|
907
|
+
|
|
908
|
+
```typescript
|
|
909
|
+
import { useState } from 'react';
|
|
910
|
+
import { Plus } from 'lucide-react';
|
|
911
|
+
import { Button } from '@lego/kernel/components';
|
|
912
|
+
import {
|
|
913
|
+
Dialog,
|
|
914
|
+
DialogContent,
|
|
915
|
+
DialogDescription,
|
|
916
|
+
DialogHeader,
|
|
917
|
+
DialogTitle,
|
|
918
|
+
DialogTrigger,
|
|
919
|
+
} from '@lego/kernel/components';
|
|
920
|
+
import { TodoForm } from './TodoForm';
|
|
921
|
+
import { useCreateTodo } from '../hooks/useCreateTodo';
|
|
922
|
+
import type { TodoFormInput } from '../schemas';
|
|
923
|
+
|
|
924
|
+
export function TodoDialog() {
|
|
925
|
+
const [open, setOpen] = useState(false);
|
|
926
|
+
const createTodo = useCreateTodo();
|
|
927
|
+
|
|
928
|
+
const handleSubmit = (data: TodoFormInput) => {
|
|
929
|
+
createTodo.mutate(data, {
|
|
930
|
+
onSuccess: () => {
|
|
931
|
+
setOpen(false);
|
|
932
|
+
},
|
|
933
|
+
});
|
|
934
|
+
};
|
|
935
|
+
|
|
936
|
+
return (
|
|
937
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
938
|
+
<DialogTrigger asChild>
|
|
939
|
+
<Button>
|
|
940
|
+
<Plus className="mr-2 h-4 w-4" />
|
|
941
|
+
New Todo
|
|
942
|
+
</Button>
|
|
943
|
+
</DialogTrigger>
|
|
944
|
+
<DialogContent>
|
|
945
|
+
<DialogHeader>
|
|
946
|
+
<DialogTitle>Create Todo</DialogTitle>
|
|
947
|
+
<DialogDescription>
|
|
948
|
+
Add a new todo item to your list
|
|
949
|
+
</DialogDescription>
|
|
950
|
+
</DialogHeader>
|
|
951
|
+
<TodoForm onSubmit={handleSubmit} isLoading={createTodo.isPending} />
|
|
952
|
+
</DialogContent>
|
|
953
|
+
</Dialog>
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
```
|
|
957
|
+
|
|
958
|
+
### Step 5: Create todo filters component
|
|
959
|
+
|
|
960
|
+
**File:** `packages/plugins/@lego/plugin-todo/src/components/TodoFilters.tsx`
|
|
961
|
+
|
|
962
|
+
```typescript
|
|
963
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@lego/kernel/components';
|
|
964
|
+
import { Input } from '@lego/kernel/components';
|
|
965
|
+
import { Label } from '@lego/kernel/components';
|
|
966
|
+
import type { TodoFilters } from '../types';
|
|
967
|
+
|
|
968
|
+
interface TodoFiltersProps {
|
|
969
|
+
filters: TodoFilters;
|
|
970
|
+
onFiltersChange: (filters: TodoFilters) => void;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
export function TodoFilters({ filters, onFiltersChange }: TodoFiltersProps) {
|
|
974
|
+
return (
|
|
975
|
+
<div className="flex flex-wrap items-center gap-4">
|
|
976
|
+
<div className="flex items-center gap-2">
|
|
977
|
+
<Label htmlFor="status">Status</Label>
|
|
978
|
+
<Select
|
|
979
|
+
value={filters.status || 'all'}
|
|
980
|
+
onValueChange={(value) =>
|
|
981
|
+
onFiltersChange({ ...filters, status: value as any })
|
|
982
|
+
}
|
|
983
|
+
>
|
|
984
|
+
<SelectTrigger id="status" className="w-32">
|
|
985
|
+
<SelectValue />
|
|
986
|
+
</SelectTrigger>
|
|
987
|
+
<SelectContent>
|
|
988
|
+
<SelectItem value="all">All</SelectItem>
|
|
989
|
+
<SelectItem value="active">Active</SelectItem>
|
|
990
|
+
<SelectItem value="completed">Completed</SelectItem>
|
|
991
|
+
</SelectContent>
|
|
992
|
+
</Select>
|
|
993
|
+
</div>
|
|
994
|
+
|
|
995
|
+
<div className="flex items-center gap-2">
|
|
996
|
+
<Label htmlFor="priority">Priority</Label>
|
|
997
|
+
<Select
|
|
998
|
+
value={filters.priority || 'all'}
|
|
999
|
+
onValueChange={(value) =>
|
|
1000
|
+
onFiltersChange({ ...filters, priority: value as any })
|
|
1001
|
+
}
|
|
1002
|
+
>
|
|
1003
|
+
<SelectTrigger id="priority" className="w-32">
|
|
1004
|
+
<SelectValue />
|
|
1005
|
+
</SelectTrigger>
|
|
1006
|
+
<SelectContent>
|
|
1007
|
+
<SelectItem value="all">All</SelectItem>
|
|
1008
|
+
<SelectItem value="low">Low</SelectItem>
|
|
1009
|
+
<SelectItem value="medium">Medium</SelectItem>
|
|
1010
|
+
<SelectItem value="high">High</SelectItem>
|
|
1011
|
+
</SelectContent>
|
|
1012
|
+
</Select>
|
|
1013
|
+
</div>
|
|
1014
|
+
|
|
1015
|
+
<div className="flex items-center gap-2">
|
|
1016
|
+
<Label htmlFor="search">Search</Label>
|
|
1017
|
+
<Input
|
|
1018
|
+
id="search"
|
|
1019
|
+
placeholder="Search todos..."
|
|
1020
|
+
value={filters.search || ''}
|
|
1021
|
+
onChange={(e) =>
|
|
1022
|
+
onFiltersChange({ ...filters, search: e.target.value })
|
|
1023
|
+
}
|
|
1024
|
+
className="w-64"
|
|
1025
|
+
/>
|
|
1026
|
+
</div>
|
|
1027
|
+
</div>
|
|
1028
|
+
);
|
|
1029
|
+
}
|
|
1030
|
+
```
|
|
1031
|
+
|
|
1032
|
+
### Step 6: Create sidebar widget
|
|
1033
|
+
|
|
1034
|
+
**File:** `packages/plugins/@lego/plugin-todo/src/components/SidebarWidget.tsx`
|
|
1035
|
+
|
|
1036
|
+
```typescript
|
|
1037
|
+
import { NavLink } from '@modern-js/runtime/router';
|
|
1038
|
+
import { cn } from '@lego/kernel/lib/utils';
|
|
1039
|
+
import { CheckSquare } from 'lucide-react';
|
|
1040
|
+
|
|
1041
|
+
interface SidebarWidgetProps {
|
|
1042
|
+
to: string;
|
|
1043
|
+
icon: string;
|
|
1044
|
+
label: string;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
export function SidebarWidget({ to, label }: SidebarWidgetProps) {
|
|
1048
|
+
return (
|
|
1049
|
+
<NavLink
|
|
1050
|
+
to={to}
|
|
1051
|
+
className={({ isActive }) =>
|
|
1052
|
+
cn(
|
|
1053
|
+
'flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
|
|
1054
|
+
isActive
|
|
1055
|
+
? 'bg-primary text-primary-foreground'
|
|
1056
|
+
: 'text-muted-foreground hover:bg-muted hover:text-foreground'
|
|
1057
|
+
)
|
|
1058
|
+
}
|
|
1059
|
+
>
|
|
1060
|
+
<CheckSquare className="h-5 w-5" />
|
|
1061
|
+
<span>{label}</span>
|
|
1062
|
+
</NavLink>
|
|
1063
|
+
);
|
|
1064
|
+
}
|
|
1065
|
+
```
|
|
1066
|
+
|
|
1067
|
+
---
|
|
1068
|
+
|
|
1069
|
+
## Task 6: Create Todo Page
|
|
1070
|
+
|
|
1071
|
+
**Files:**
|
|
1072
|
+
- Create: `packages/plugins/@lego/plugin-todo/src/pages/TodoPage.tsx`
|
|
1073
|
+
- Create: `packages/plugins/@lego/plugin-todo/src/App.tsx`
|
|
1074
|
+
- Create: `packages/plugins/@lego/plugin-todo/src/global.css`
|
|
1075
|
+
|
|
1076
|
+
### Step 1: Create todo page
|
|
1077
|
+
|
|
1078
|
+
**File:** `packages/plugins/@lego/plugin-todo/src/pages/TodoPage.tsx`
|
|
1079
|
+
|
|
1080
|
+
```typescript
|
|
1081
|
+
import { useState } from 'react';
|
|
1082
|
+
import { useTodos } from '../hooks/useTodos';
|
|
1083
|
+
import { TodoList } from '../components/TodoList';
|
|
1084
|
+
import { TodoDialog } from '../components/TodoDialog';
|
|
1085
|
+
import { TodoFilters } from '../components/TodoFilters';
|
|
1086
|
+
import type { TodoFilters as TodoFiltersType } from '../types';
|
|
1087
|
+
import { usePluginReady } from '@lego/kernel/channels';
|
|
1088
|
+
|
|
1089
|
+
export default function TodoPage() {
|
|
1090
|
+
const [filters, setFilters] = useState<TodoFiltersType>({
|
|
1091
|
+
status: 'all',
|
|
1092
|
+
priority: 'all',
|
|
1093
|
+
});
|
|
1094
|
+
|
|
1095
|
+
const { data: todos, isLoading } = useTodos(filters);
|
|
1096
|
+
|
|
1097
|
+
// Notify host that plugin is ready
|
|
1098
|
+
usePluginReady('@lego/plugin-todo', '1.0.0');
|
|
1099
|
+
|
|
1100
|
+
return (
|
|
1101
|
+
<div className="space-y-6">
|
|
1102
|
+
{/* Header */}
|
|
1103
|
+
<div className="flex items-center justify-between">
|
|
1104
|
+
<div>
|
|
1105
|
+
<h1 className="text-3xl font-bold tracking-tight">Todos</h1>
|
|
1106
|
+
<p className="text-muted-foreground">
|
|
1107
|
+
Manage your tasks and stay organized
|
|
1108
|
+
</p>
|
|
1109
|
+
</div>
|
|
1110
|
+
<TodoDialog />
|
|
1111
|
+
</div>
|
|
1112
|
+
|
|
1113
|
+
{/* Filters */}
|
|
1114
|
+
<TodoFilters filters={filters} onFiltersChange={setFilters} />
|
|
1115
|
+
|
|
1116
|
+
{/* Todo List */}
|
|
1117
|
+
<TodoList todos={todos || []} isLoading={isLoading} />
|
|
1118
|
+
</div>
|
|
1119
|
+
);
|
|
1120
|
+
}
|
|
1121
|
+
```
|
|
1122
|
+
|
|
1123
|
+
### Step 2: Create app component
|
|
1124
|
+
|
|
1125
|
+
**File:** `packages/plugins/@lego/plugin-todo/src/App.tsx`
|
|
1126
|
+
|
|
1127
|
+
```typescript
|
|
1128
|
+
import './global.css';
|
|
1129
|
+
|
|
1130
|
+
interface TodoAppProps {
|
|
1131
|
+
basename?: string;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
export default function TodoApp({ basename }: TodoAppProps) {
|
|
1135
|
+
return (
|
|
1136
|
+
<div className="todo-plugin">
|
|
1137
|
+
{/* Routes handled by host */}
|
|
1138
|
+
</div>
|
|
1139
|
+
);
|
|
1140
|
+
}
|
|
1141
|
+
```
|
|
1142
|
+
|
|
1143
|
+
### Step 3: Create global CSS
|
|
1144
|
+
|
|
1145
|
+
**File:** `packages/plugins/@lego/plugin-todo/src/global.css`
|
|
1146
|
+
|
|
1147
|
+
```css
|
|
1148
|
+
@tailwind base;
|
|
1149
|
+
@tailwind components;
|
|
1150
|
+
@tailwind utilities;
|
|
1151
|
+
|
|
1152
|
+
@layer base {
|
|
1153
|
+
:root {
|
|
1154
|
+
--background: 0 0% 100%;
|
|
1155
|
+
--foreground: 222.2 84% 4.9%;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
.dark {
|
|
1159
|
+
--background: 222.2 84% 4.9%;
|
|
1160
|
+
--foreground: 210 40% 98%;
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
@layer base {
|
|
1165
|
+
* {
|
|
1166
|
+
@apply border-border;
|
|
1167
|
+
}
|
|
1168
|
+
body {
|
|
1169
|
+
@apply bg-background text-foreground;
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
```
|
|
1173
|
+
|
|
1174
|
+
---
|
|
1175
|
+
|
|
1176
|
+
## Task 7: Create Tailwind Config
|
|
1177
|
+
|
|
1178
|
+
**Files:**
|
|
1179
|
+
- Create: `packages/plugins/@lego/plugin-todo/tailwind.config.ts`
|
|
1180
|
+
- Create: `packages/plugins/@lego/plugin-todo/postcss.config.js`
|
|
1181
|
+
|
|
1182
|
+
### Step 1: Create Tailwind config
|
|
1183
|
+
|
|
1184
|
+
**File:** `packages/plugins/@lego/plugin-todo/tailwind.config.ts`
|
|
1185
|
+
|
|
1186
|
+
```typescript
|
|
1187
|
+
import type { Config } from 'tailwindcss';
|
|
1188
|
+
|
|
1189
|
+
const config: Config = {
|
|
1190
|
+
darkMode: ['class'],
|
|
1191
|
+
content: ['./src/**/*.{js,ts,jsx,tsx,mdx}'],
|
|
1192
|
+
theme: {
|
|
1193
|
+
extend: {
|
|
1194
|
+
colors: {
|
|
1195
|
+
border: 'hsl(var(--border))',
|
|
1196
|
+
background: 'hsl(var(--background))',
|
|
1197
|
+
foreground: 'hsl(var(--foreground))',
|
|
1198
|
+
primary: {
|
|
1199
|
+
DEFAULT: 'hsl(var(--primary))',
|
|
1200
|
+
foreground: 'hsl(var(--primary-foreground))',
|
|
1201
|
+
},
|
|
1202
|
+
muted: {
|
|
1203
|
+
DEFAULT: 'hsl(var(--muted))',
|
|
1204
|
+
foreground: 'hsl(var(--muted-foreground))',
|
|
1205
|
+
},
|
|
1206
|
+
card: {
|
|
1207
|
+
DEFAULT: 'hsl(var(--card))',
|
|
1208
|
+
foreground: 'hsl(var(--card-foreground))',
|
|
1209
|
+
},
|
|
1210
|
+
destructive: {
|
|
1211
|
+
DEFAULT: 'hsl(var(--destructive))',
|
|
1212
|
+
foreground: 'hsl(var(--destructive-foreground))',
|
|
1213
|
+
},
|
|
1214
|
+
},
|
|
1215
|
+
borderRadius: {
|
|
1216
|
+
lg: 'var(--radius)',
|
|
1217
|
+
},
|
|
1218
|
+
},
|
|
1219
|
+
},
|
|
1220
|
+
plugins: [],
|
|
1221
|
+
};
|
|
1222
|
+
|
|
1223
|
+
export default config;
|
|
1224
|
+
```
|
|
1225
|
+
|
|
1226
|
+
### Step 2: Create PostCSS config
|
|
1227
|
+
|
|
1228
|
+
**File:** `packages/plugins/@lego/plugin-todo/postcss.config.js`
|
|
1229
|
+
|
|
1230
|
+
```javascript
|
|
1231
|
+
module.exports = {
|
|
1232
|
+
plugins: {
|
|
1233
|
+
tailwindcss: {},
|
|
1234
|
+
autoprefixer: {},
|
|
1235
|
+
},
|
|
1236
|
+
};
|
|
1237
|
+
```
|
|
1238
|
+
|
|
1239
|
+
---
|
|
1240
|
+
|
|
1241
|
+
## Verification
|
|
1242
|
+
|
|
1243
|
+
### Step 1: Build the todo plugin
|
|
1244
|
+
|
|
1245
|
+
**Run:**
|
|
1246
|
+
|
|
1247
|
+
```bash
|
|
1248
|
+
cd packages/plugins/@lego/plugin-todo
|
|
1249
|
+
pnpm run build
|
|
1250
|
+
```
|
|
1251
|
+
|
|
1252
|
+
Expected: Build completes without errors.
|
|
1253
|
+
|
|
1254
|
+
### Step 2: Start todo plugin dev server
|
|
1255
|
+
|
|
1256
|
+
**Run:**
|
|
1257
|
+
|
|
1258
|
+
```bash
|
|
1259
|
+
cd packages/plugins/@lego/plugin-todo
|
|
1260
|
+
pnpm run dev
|
|
1261
|
+
```
|
|
1262
|
+
|
|
1263
|
+
Expected: Server starts on http://localhost:3002
|
|
1264
|
+
|
|
1265
|
+
### Step 3: Start host dev server (separate terminal)
|
|
1266
|
+
|
|
1267
|
+
**Run:**
|
|
1268
|
+
|
|
1269
|
+
```bash
|
|
1270
|
+
cd host
|
|
1271
|
+
pnpm run dev
|
|
1272
|
+
```
|
|
1273
|
+
|
|
1274
|
+
Expected: Server starts on http://localhost:8080
|
|
1275
|
+
|
|
1276
|
+
### Step 4: Test todo plugin
|
|
1277
|
+
|
|
1278
|
+
1. Open http://localhost:8080
|
|
1279
|
+
2. Login with admin credentials
|
|
1280
|
+
3. Navigate to /todos
|
|
1281
|
+
4. Should see:
|
|
1282
|
+
- Todos page with filters
|
|
1283
|
+
- "New Todo" button
|
|
1284
|
+
- Todo list (empty initially)
|
|
1285
|
+
5. Click "New Todo" and create a todo
|
|
1286
|
+
6. Verify todo appears in list
|
|
1287
|
+
7. Test completing a todo (click checkbox)
|
|
1288
|
+
8. Test deleting a todo
|
|
1289
|
+
9. Test filters (status, priority, search)
|
|
1290
|
+
|
|
1291
|
+
---
|
|
1292
|
+
|
|
1293
|
+
## Summary
|
|
1294
|
+
|
|
1295
|
+
After completing this document, you will have:
|
|
1296
|
+
|
|
1297
|
+
1. ✅ Complete Todo plugin as Modern.js + Garfish sub-app
|
|
1298
|
+
2. ✅ Full CRUD operations for todos
|
|
1299
|
+
3. ✅ PocketBase integration with multi-tenancy
|
|
1300
|
+
4. ✅ Form validation with Zod
|
|
1301
|
+
5. ✅ Todo filters (status, priority, search)
|
|
1302
|
+
6. ✅ Create todo dialog with form
|
|
1303
|
+
7. ✅ Todo list with items (toggle, delete)
|
|
1304
|
+
8. ✅ Channel integration (toasts, ready event)
|
|
1305
|
+
9. ✅ Independent dev server (:3002)
|
|
1306
|
+
|
|
1307
|
+
**Next:** `11-testing.md` - Implement unit, component, and E2E tests with Vitest and Playwright.
|
|
1308
|
+
|
|
1309
|
+
---
|
|
1310
|
+
|
|
1311
|
+
## Files Created
|
|
1312
|
+
|
|
1313
|
+
```
|
|
1314
|
+
packages/plugins/@lego/plugin-todo/
|
|
1315
|
+
├── package.json
|
|
1316
|
+
├── tsconfig.json
|
|
1317
|
+
├── modern.config.ts
|
|
1318
|
+
├── tailwind.config.ts
|
|
1319
|
+
├── postcss.config.js
|
|
1320
|
+
├── .gitignore
|
|
1321
|
+
└── src/
|
|
1322
|
+
├── global.css
|
|
1323
|
+
├── plugin.ts
|
|
1324
|
+
├── plugin.config.ts
|
|
1325
|
+
├── types.ts
|
|
1326
|
+
├── schemas.ts
|
|
1327
|
+
├── App.tsx
|
|
1328
|
+
├── pages/
|
|
1329
|
+
│ └── TodoPage.tsx
|
|
1330
|
+
├── components/
|
|
1331
|
+
│ ├── TodoList.tsx
|
|
1332
|
+
│ ├── TodoItem.tsx
|
|
1333
|
+
│ ├── TodoForm.tsx
|
|
1334
|
+
│ ├── TodoDialog.tsx
|
|
1335
|
+
│ ├── TodoFilters.tsx
|
|
1336
|
+
│ └── SidebarWidget.tsx
|
|
1337
|
+
└── hooks/
|
|
1338
|
+
├── usePocketBase.ts
|
|
1339
|
+
├── useTodos.ts
|
|
1340
|
+
├── useCreateTodo.ts
|
|
1341
|
+
├── useUpdateTodo.ts
|
|
1342
|
+
└── useDeleteTodo.ts
|
|
1343
|
+
```
|