@robsun/create-keystone-app 0.1.18 → 0.2.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 +18 -5
- package/{bin → dist}/create-keystone-app.js +114 -116
- package/dist/create-module.js +638 -0
- package/package.json +11 -7
- package/template/README.md +17 -13
- package/template/apps/server/config.example.yaml +0 -1
- package/template/apps/server/config.yaml +0 -1
- package/template/apps/server/internal/modules/example/api/handler/item_handler.go +162 -0
- package/template/apps/server/internal/modules/example/bootstrap/migrations/item.go +21 -0
- package/template/apps/server/internal/modules/example/bootstrap/seeds/item.go +33 -0
- package/template/apps/server/internal/modules/example/domain/models/item.go +30 -0
- package/template/apps/server/internal/modules/{demo → example}/domain/service/errors.go +1 -1
- package/template/apps/server/internal/modules/example/domain/service/item_service.go +110 -0
- package/template/apps/server/internal/modules/example/infra/repository/item_repository.go +49 -0
- package/template/apps/server/internal/modules/example/module.go +55 -17
- package/template/apps/server/internal/modules/manifest.go +0 -2
- package/template/apps/web/src/app.config.ts +1 -1
- package/template/apps/web/src/main.tsx +1 -3
- package/template/apps/web/src/modules/example/help/faq.md +23 -0
- package/template/apps/web/src/modules/example/help/items.md +26 -0
- package/template/apps/web/src/modules/example/help/overview.md +18 -4
- package/template/apps/web/src/modules/example/pages/ExampleItemsPage.tsx +227 -0
- package/template/apps/web/src/modules/example/routes.tsx +33 -10
- package/template/apps/web/src/modules/example/services/exampleItems.ts +32 -0
- package/template/apps/web/src/modules/example/types.ts +10 -0
- package/template/docs/CONVENTIONS.md +44 -0
- package/template/docs/GETTING_STARTED.md +54 -0
- package/template/package.json +2 -1
- package/template/scripts/check-modules.js +7 -1
- package/template/apps/server/internal/modules/demo/api/handler/task_handler.go +0 -152
- package/template/apps/server/internal/modules/demo/bootstrap/migrations/task.go +0 -21
- package/template/apps/server/internal/modules/demo/bootstrap/seeds/task.go +0 -33
- package/template/apps/server/internal/modules/demo/domain/models/task.go +0 -30
- package/template/apps/server/internal/modules/demo/domain/service/task_service.go +0 -95
- package/template/apps/server/internal/modules/demo/infra/repository/task_repository.go +0 -49
- package/template/apps/server/internal/modules/demo/module.go +0 -91
- package/template/apps/server/internal/modules/example/handlers.go +0 -19
- package/template/apps/web/src/modules/demo/help/overview.md +0 -12
- package/template/apps/web/src/modules/demo/index.ts +0 -7
- package/template/apps/web/src/modules/demo/pages/DemoTasksPage.tsx +0 -185
- package/template/apps/web/src/modules/demo/routes.tsx +0 -43
- package/template/apps/web/src/modules/demo/services/demoTasks.ts +0 -28
- package/template/apps/web/src/modules/demo/types.ts +0 -9
- package/template/apps/web/src/modules/example/pages/ExamplePage.tsx +0 -41
- package/template/apps/web/src/modules/example/services/api.ts +0 -8
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
package seeds
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"log"
|
|
5
|
-
|
|
6
|
-
"gorm.io/gorm"
|
|
7
|
-
|
|
8
|
-
"__APP_NAME__/apps/server/internal/modules/demo/domain/models"
|
|
9
|
-
)
|
|
10
|
-
|
|
11
|
-
func Seed(db *gorm.DB) error {
|
|
12
|
-
if db == nil {
|
|
13
|
-
return nil
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
var count int64
|
|
17
|
-
if err := db.Model(&models.DemoTask{}).Count(&count).Error; err != nil {
|
|
18
|
-
return err
|
|
19
|
-
}
|
|
20
|
-
if count > 0 {
|
|
21
|
-
return nil
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
log.Println("[demo] Seeding initial data...")
|
|
25
|
-
tasks := []models.DemoTask{
|
|
26
|
-
{Title: "Set up project", Status: models.StatusTodo},
|
|
27
|
-
{Title: "Review UI states", Status: models.StatusDone},
|
|
28
|
-
}
|
|
29
|
-
for i := range tasks {
|
|
30
|
-
tasks[i].TenantID = 1
|
|
31
|
-
}
|
|
32
|
-
return db.Create(&tasks).Error
|
|
33
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
package models
|
|
2
|
-
|
|
3
|
-
import "github.com/robsuncn/keystone/domain/models"
|
|
4
|
-
|
|
5
|
-
type TaskStatus string
|
|
6
|
-
|
|
7
|
-
const (
|
|
8
|
-
StatusTodo TaskStatus = "todo"
|
|
9
|
-
StatusInProgress TaskStatus = "in_progress"
|
|
10
|
-
StatusDone TaskStatus = "done"
|
|
11
|
-
)
|
|
12
|
-
|
|
13
|
-
func (s TaskStatus) IsValid() bool {
|
|
14
|
-
switch s {
|
|
15
|
-
case StatusTodo, StatusInProgress, StatusDone:
|
|
16
|
-
return true
|
|
17
|
-
default:
|
|
18
|
-
return false
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
type DemoTask struct {
|
|
23
|
-
models.BaseModel
|
|
24
|
-
Title string `gorm:"size:200;not null" json:"title"`
|
|
25
|
-
Status TaskStatus `gorm:"size:20;not null;default:'todo'" json:"status"`
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
func (DemoTask) TableName() string {
|
|
29
|
-
return "demo_tasks"
|
|
30
|
-
}
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
package service
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"context"
|
|
5
|
-
"strings"
|
|
6
|
-
|
|
7
|
-
"__APP_NAME__/apps/server/internal/modules/demo/domain/models"
|
|
8
|
-
)
|
|
9
|
-
|
|
10
|
-
type TaskRepository interface {
|
|
11
|
-
List(ctx context.Context, tenantID uint) ([]models.DemoTask, error)
|
|
12
|
-
FindByID(tenantID, id uint) (*models.DemoTask, error)
|
|
13
|
-
Create(ctx context.Context, task *models.DemoTask) error
|
|
14
|
-
Update(ctx context.Context, task *models.DemoTask) error
|
|
15
|
-
Delete(ctx context.Context, task *models.DemoTask) error
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
type TaskService struct {
|
|
19
|
-
tasks TaskRepository
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
type TaskInput struct {
|
|
23
|
-
Title string
|
|
24
|
-
Status models.TaskStatus
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
type TaskUpdateInput struct {
|
|
28
|
-
Title string
|
|
29
|
-
Status models.TaskStatus
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
func NewTaskService(tasks TaskRepository) *TaskService {
|
|
33
|
-
return &TaskService{tasks: tasks}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
func (s *TaskService) List(ctx context.Context, tenantID uint) ([]models.DemoTask, error) {
|
|
37
|
-
return s.tasks.List(ctx, tenantID)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
func (s *TaskService) Create(ctx context.Context, tenantID uint, input TaskInput) (*models.DemoTask, error) {
|
|
41
|
-
title := strings.TrimSpace(input.Title)
|
|
42
|
-
if title == "" {
|
|
43
|
-
return nil, ErrTitleRequired
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
status := input.Status
|
|
47
|
-
if status == "" {
|
|
48
|
-
status = models.StatusTodo
|
|
49
|
-
}
|
|
50
|
-
if !status.IsValid() {
|
|
51
|
-
return nil, ErrStatusInvalid
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
task := &models.DemoTask{
|
|
55
|
-
Title: title,
|
|
56
|
-
Status: status,
|
|
57
|
-
}
|
|
58
|
-
task.TenantID = tenantID
|
|
59
|
-
|
|
60
|
-
if err := s.tasks.Create(ctx, task); err != nil {
|
|
61
|
-
return nil, err
|
|
62
|
-
}
|
|
63
|
-
return task, nil
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
func (s *TaskService) Update(ctx context.Context, tenantID, id uint, input TaskUpdateInput) (*models.DemoTask, error) {
|
|
67
|
-
task, err := s.tasks.FindByID(tenantID, id)
|
|
68
|
-
if err != nil {
|
|
69
|
-
return nil, err
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if title := strings.TrimSpace(input.Title); title != "" {
|
|
73
|
-
task.Title = title
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if input.Status != "" {
|
|
77
|
-
if !input.Status.IsValid() {
|
|
78
|
-
return nil, ErrStatusInvalid
|
|
79
|
-
}
|
|
80
|
-
task.Status = input.Status
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if err := s.tasks.Update(ctx, task); err != nil {
|
|
84
|
-
return nil, err
|
|
85
|
-
}
|
|
86
|
-
return task, nil
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
func (s *TaskService) Delete(ctx context.Context, tenantID, id uint) error {
|
|
90
|
-
task, err := s.tasks.FindByID(tenantID, id)
|
|
91
|
-
if err != nil {
|
|
92
|
-
return err
|
|
93
|
-
}
|
|
94
|
-
return s.tasks.Delete(ctx, task)
|
|
95
|
-
}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
package repository
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"context"
|
|
5
|
-
"errors"
|
|
6
|
-
|
|
7
|
-
"gorm.io/gorm"
|
|
8
|
-
|
|
9
|
-
"__APP_NAME__/apps/server/internal/modules/demo/domain/models"
|
|
10
|
-
"__APP_NAME__/apps/server/internal/modules/demo/domain/service"
|
|
11
|
-
)
|
|
12
|
-
|
|
13
|
-
type TaskRepository struct {
|
|
14
|
-
db *gorm.DB
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
func NewTaskRepository(db *gorm.DB) *TaskRepository {
|
|
18
|
-
return &TaskRepository{db: db}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
func (r *TaskRepository) List(ctx context.Context, tenantID uint) ([]models.DemoTask, error) {
|
|
22
|
-
var tasks []models.DemoTask
|
|
23
|
-
err := r.db.WithContext(ctx).
|
|
24
|
-
Where("tenant_id = ?", tenantID).
|
|
25
|
-
Order("created_at desc").
|
|
26
|
-
Find(&tasks).Error
|
|
27
|
-
return tasks, err
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
func (r *TaskRepository) FindByID(tenantID, id uint) (*models.DemoTask, error) {
|
|
31
|
-
var task models.DemoTask
|
|
32
|
-
err := r.db.Where("tenant_id = ? AND id = ?", tenantID, id).First(&task).Error
|
|
33
|
-
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
34
|
-
return nil, service.ErrTaskNotFound
|
|
35
|
-
}
|
|
36
|
-
return &task, err
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
func (r *TaskRepository) Create(ctx context.Context, task *models.DemoTask) error {
|
|
40
|
-
return r.db.WithContext(ctx).Create(task).Error
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
func (r *TaskRepository) Update(ctx context.Context, task *models.DemoTask) error {
|
|
44
|
-
return r.db.WithContext(ctx).Save(task).Error
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
func (r *TaskRepository) Delete(ctx context.Context, task *models.DemoTask) error {
|
|
48
|
-
return r.db.WithContext(ctx).Delete(task).Error
|
|
49
|
-
}
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
package demo
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"github.com/gin-gonic/gin"
|
|
5
|
-
"gorm.io/gorm"
|
|
6
|
-
|
|
7
|
-
"github.com/robsuncn/keystone/domain/permissions"
|
|
8
|
-
"github.com/robsuncn/keystone/infra/jobs"
|
|
9
|
-
|
|
10
|
-
demohandler "__APP_NAME__/apps/server/internal/modules/demo/api/handler"
|
|
11
|
-
demomigrations "__APP_NAME__/apps/server/internal/modules/demo/bootstrap/migrations"
|
|
12
|
-
demoseeds "__APP_NAME__/apps/server/internal/modules/demo/bootstrap/seeds"
|
|
13
|
-
demomodels "__APP_NAME__/apps/server/internal/modules/demo/domain/models"
|
|
14
|
-
demoservice "__APP_NAME__/apps/server/internal/modules/demo/domain/service"
|
|
15
|
-
demorepository "__APP_NAME__/apps/server/internal/modules/demo/infra/repository"
|
|
16
|
-
)
|
|
17
|
-
|
|
18
|
-
type Module struct {
|
|
19
|
-
tasks *demoservice.TaskService
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
func NewModule() *Module {
|
|
23
|
-
return &Module{}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
func (m *Module) Name() string {
|
|
27
|
-
return "demo"
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
func (m *Module) RegisterRoutes(rg *gin.RouterGroup) {
|
|
31
|
-
if rg == nil || m == nil {
|
|
32
|
-
return
|
|
33
|
-
}
|
|
34
|
-
handler := demohandler.NewTaskHandler(m.tasks)
|
|
35
|
-
if handler == nil {
|
|
36
|
-
return
|
|
37
|
-
}
|
|
38
|
-
group := rg.Group("/demo")
|
|
39
|
-
group.GET("/tasks", handler.List)
|
|
40
|
-
group.POST("/tasks", handler.Create)
|
|
41
|
-
group.PATCH("/tasks/:id", handler.Update)
|
|
42
|
-
group.DELETE("/tasks/:id", handler.Delete)
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
func (m *Module) RegisterModels() []interface{} {
|
|
46
|
-
return []interface{}{&demomodels.DemoTask{}}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
func (m *Module) RegisterPermissions(reg *permissions.Registry) error {
|
|
50
|
-
if reg == nil {
|
|
51
|
-
return nil
|
|
52
|
-
}
|
|
53
|
-
if err := reg.CreateMenu("demo:task", "Demo Tasks", "demo", 10); err != nil {
|
|
54
|
-
return err
|
|
55
|
-
}
|
|
56
|
-
if err := reg.CreateAction("demo:task:view", "View Tasks", "demo", "demo:task"); err != nil {
|
|
57
|
-
return err
|
|
58
|
-
}
|
|
59
|
-
if err := reg.CreateAction("demo:task:manage", "Manage Tasks", "demo", "demo:task"); err != nil {
|
|
60
|
-
return err
|
|
61
|
-
}
|
|
62
|
-
return nil
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
func (m *Module) RegisterJobs(_ *jobs.Registry) error {
|
|
66
|
-
return nil
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
func (m *Module) Migrate(db *gorm.DB) error {
|
|
70
|
-
if db == nil {
|
|
71
|
-
return nil
|
|
72
|
-
}
|
|
73
|
-
m.ensureServices(db)
|
|
74
|
-
return demomigrations.Migrate(db)
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
func (m *Module) Seed(db *gorm.DB) error {
|
|
78
|
-
if db == nil {
|
|
79
|
-
return nil
|
|
80
|
-
}
|
|
81
|
-
m.ensureServices(db)
|
|
82
|
-
return demoseeds.Seed(db)
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
func (m *Module) ensureServices(db *gorm.DB) {
|
|
86
|
-
if m == nil || db == nil || m.tasks != nil {
|
|
87
|
-
return
|
|
88
|
-
}
|
|
89
|
-
repo := demorepository.NewTaskRepository(db)
|
|
90
|
-
m.tasks = demoservice.NewTaskService(repo)
|
|
91
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
package example
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"github.com/gin-gonic/gin"
|
|
5
|
-
|
|
6
|
-
"github.com/robsuncn/keystone/api/response"
|
|
7
|
-
)
|
|
8
|
-
|
|
9
|
-
type HelloResponse struct {
|
|
10
|
-
Message string `json:"message"`
|
|
11
|
-
Module string `json:"module"`
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
func handleHello(c *gin.Context) {
|
|
15
|
-
response.Success(c, HelloResponse{
|
|
16
|
-
Message: "Hello from Example module!",
|
|
17
|
-
Module: "example",
|
|
18
|
-
})
|
|
19
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
helpKey: "demo/tasks"
|
|
3
|
-
title: "Demo Tasks"
|
|
4
|
-
description: "Manage sample tasks in the scaffold."
|
|
5
|
-
category: "demo"
|
|
6
|
-
tags: ["demo", "tasks"]
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# Demo Tasks
|
|
10
|
-
|
|
11
|
-
Use this module as a reference for list + create + update flows. Replace it
|
|
12
|
-
with real business logic when you build new modules.
|
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
import { useCallback, useEffect, useMemo, useState } from 'react'
|
|
2
|
-
import { App, Button, Card, Form, Input, Popconfirm, Select, Space, Table, Tag } from 'antd'
|
|
3
|
-
import type { ColumnsType } from 'antd/es/table'
|
|
4
|
-
import dayjs from 'dayjs'
|
|
5
|
-
import { createDemoTask, deleteDemoTask, listDemoTasks, updateDemoTask } from '../services/demoTasks'
|
|
6
|
-
import type { DemoTask, DemoTaskStatus } from '../types'
|
|
7
|
-
|
|
8
|
-
type CreateValues = {
|
|
9
|
-
title: string
|
|
10
|
-
status?: DemoTaskStatus
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const statusMeta: Record<DemoTaskStatus, { label: string; color: string }> = {
|
|
14
|
-
todo: { label: 'Todo', color: 'default' },
|
|
15
|
-
in_progress: { label: 'In Progress', color: 'processing' },
|
|
16
|
-
done: { label: 'Done', color: 'success' },
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const statusOptions = [
|
|
20
|
-
{ value: 'todo', label: 'Todo' },
|
|
21
|
-
{ value: 'in_progress', label: 'In Progress' },
|
|
22
|
-
{ value: 'done', label: 'Done' },
|
|
23
|
-
]
|
|
24
|
-
|
|
25
|
-
export function DemoTasksPage() {
|
|
26
|
-
const { message } = App.useApp()
|
|
27
|
-
const [items, setItems] = useState<DemoTask[]>([])
|
|
28
|
-
const [loading, setLoading] = useState(false)
|
|
29
|
-
const [submitting, setSubmitting] = useState(false)
|
|
30
|
-
const [form] = Form.useForm<CreateValues>()
|
|
31
|
-
|
|
32
|
-
const fetchTasks = useCallback(async () => {
|
|
33
|
-
setLoading(true)
|
|
34
|
-
try {
|
|
35
|
-
const data = await listDemoTasks()
|
|
36
|
-
setItems(data)
|
|
37
|
-
} catch (err) {
|
|
38
|
-
const detail = err instanceof Error ? err.message : 'Failed to load tasks'
|
|
39
|
-
message.error(detail)
|
|
40
|
-
} finally {
|
|
41
|
-
setLoading(false)
|
|
42
|
-
}
|
|
43
|
-
}, [message])
|
|
44
|
-
|
|
45
|
-
useEffect(() => {
|
|
46
|
-
void fetchTasks()
|
|
47
|
-
}, [fetchTasks])
|
|
48
|
-
|
|
49
|
-
const handleCreate = useCallback(
|
|
50
|
-
async (values: CreateValues) => {
|
|
51
|
-
const title = values.title?.trim()
|
|
52
|
-
if (!title) {
|
|
53
|
-
message.error('Title is required')
|
|
54
|
-
return
|
|
55
|
-
}
|
|
56
|
-
setSubmitting(true)
|
|
57
|
-
try {
|
|
58
|
-
await createDemoTask({ title, status: values.status })
|
|
59
|
-
form.resetFields()
|
|
60
|
-
await fetchTasks()
|
|
61
|
-
message.success('Task created')
|
|
62
|
-
} catch (err) {
|
|
63
|
-
const detail = err instanceof Error ? err.message : 'Failed to create task'
|
|
64
|
-
message.error(detail)
|
|
65
|
-
} finally {
|
|
66
|
-
setSubmitting(false)
|
|
67
|
-
}
|
|
68
|
-
},
|
|
69
|
-
[fetchTasks, form, message]
|
|
70
|
-
)
|
|
71
|
-
|
|
72
|
-
const handleStatusChange = useCallback(
|
|
73
|
-
async (id: number, status: DemoTaskStatus) => {
|
|
74
|
-
try {
|
|
75
|
-
await updateDemoTask(id, { status })
|
|
76
|
-
await fetchTasks()
|
|
77
|
-
message.success('Task updated')
|
|
78
|
-
} catch (err) {
|
|
79
|
-
const detail = err instanceof Error ? err.message : 'Failed to update task'
|
|
80
|
-
message.error(detail)
|
|
81
|
-
}
|
|
82
|
-
},
|
|
83
|
-
[fetchTasks, message]
|
|
84
|
-
)
|
|
85
|
-
|
|
86
|
-
const handleDelete = useCallback(
|
|
87
|
-
async (id: number) => {
|
|
88
|
-
try {
|
|
89
|
-
await deleteDemoTask(id)
|
|
90
|
-
await fetchTasks()
|
|
91
|
-
message.success('Task deleted')
|
|
92
|
-
} catch (err) {
|
|
93
|
-
const detail = err instanceof Error ? err.message : 'Failed to delete task'
|
|
94
|
-
message.error(detail)
|
|
95
|
-
}
|
|
96
|
-
},
|
|
97
|
-
[fetchTasks, message]
|
|
98
|
-
)
|
|
99
|
-
|
|
100
|
-
const columns: ColumnsType<DemoTask> = useMemo(
|
|
101
|
-
() => [
|
|
102
|
-
{ title: 'Title', dataIndex: 'title', key: 'title' },
|
|
103
|
-
{
|
|
104
|
-
title: 'Status',
|
|
105
|
-
dataIndex: 'status',
|
|
106
|
-
key: 'status',
|
|
107
|
-
render: (value: DemoTaskStatus) => {
|
|
108
|
-
const meta = statusMeta[value]
|
|
109
|
-
return <Tag color={meta.color}>{meta.label}</Tag>
|
|
110
|
-
},
|
|
111
|
-
},
|
|
112
|
-
{
|
|
113
|
-
title: 'Updated',
|
|
114
|
-
dataIndex: 'updated_at',
|
|
115
|
-
key: 'updated_at',
|
|
116
|
-
render: (value: string) => (value ? dayjs(value).format('YYYY-MM-DD HH:mm') : '-'),
|
|
117
|
-
},
|
|
118
|
-
{
|
|
119
|
-
title: 'Actions',
|
|
120
|
-
key: 'actions',
|
|
121
|
-
render: (_, record) => (
|
|
122
|
-
<Space>
|
|
123
|
-
<Select
|
|
124
|
-
size="small"
|
|
125
|
-
value={record.status}
|
|
126
|
-
options={statusOptions}
|
|
127
|
-
style={{ width: 140 }}
|
|
128
|
-
onChange={(value) => handleStatusChange(record.id, value as DemoTaskStatus)}
|
|
129
|
-
/>
|
|
130
|
-
<Popconfirm
|
|
131
|
-
title="Delete this task?"
|
|
132
|
-
onConfirm={() => handleDelete(record.id)}
|
|
133
|
-
okText="Delete"
|
|
134
|
-
>
|
|
135
|
-
<Button size="small" danger>
|
|
136
|
-
Delete
|
|
137
|
-
</Button>
|
|
138
|
-
</Popconfirm>
|
|
139
|
-
</Space>
|
|
140
|
-
),
|
|
141
|
-
},
|
|
142
|
-
],
|
|
143
|
-
[handleDelete, handleStatusChange]
|
|
144
|
-
)
|
|
145
|
-
|
|
146
|
-
return (
|
|
147
|
-
<Card
|
|
148
|
-
title="Demo Tasks"
|
|
149
|
-
extra={
|
|
150
|
-
<Button onClick={fetchTasks} loading={loading}>
|
|
151
|
-
Refresh
|
|
152
|
-
</Button>
|
|
153
|
-
}
|
|
154
|
-
>
|
|
155
|
-
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
|
156
|
-
<Form
|
|
157
|
-
form={form}
|
|
158
|
-
layout="inline"
|
|
159
|
-
onFinish={handleCreate}
|
|
160
|
-
initialValues={{ status: 'todo' }}
|
|
161
|
-
>
|
|
162
|
-
<Form.Item name="title" rules={[{ required: true, message: 'Title is required' }]}>
|
|
163
|
-
<Input placeholder="Task title" allowClear style={{ width: 240 }} />
|
|
164
|
-
</Form.Item>
|
|
165
|
-
<Form.Item name="status">
|
|
166
|
-
<Select options={statusOptions} style={{ width: 160 }} />
|
|
167
|
-
</Form.Item>
|
|
168
|
-
<Form.Item>
|
|
169
|
-
<Button type="primary" htmlType="submit" loading={submitting}>
|
|
170
|
-
Add Task
|
|
171
|
-
</Button>
|
|
172
|
-
</Form.Item>
|
|
173
|
-
</Form>
|
|
174
|
-
|
|
175
|
-
<Table<DemoTask>
|
|
176
|
-
rowKey="id"
|
|
177
|
-
loading={loading}
|
|
178
|
-
columns={columns}
|
|
179
|
-
dataSource={items}
|
|
180
|
-
pagination={false}
|
|
181
|
-
/>
|
|
182
|
-
</Space>
|
|
183
|
-
</Card>
|
|
184
|
-
)
|
|
185
|
-
}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { lazy, Suspense, type ComponentType, type ReactElement } from 'react'
|
|
2
|
-
import type { RouteObject } from 'react-router-dom'
|
|
3
|
-
import { AppstoreOutlined } from '@ant-design/icons'
|
|
4
|
-
import { Spin } from 'antd'
|
|
5
|
-
|
|
6
|
-
const lazyNamed = <T extends Record<string, ComponentType>, K extends keyof T>(
|
|
7
|
-
factory: () => Promise<T>,
|
|
8
|
-
name: K
|
|
9
|
-
) =>
|
|
10
|
-
lazy(async () => {
|
|
11
|
-
const module = await factory()
|
|
12
|
-
return { default: module[name] }
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
const withSuspense = (element: ReactElement) => (
|
|
16
|
-
<Suspense
|
|
17
|
-
fallback={
|
|
18
|
-
<div style={{ padding: 24, display: 'flex', justifyContent: 'center' }}>
|
|
19
|
-
<Spin />
|
|
20
|
-
</div>
|
|
21
|
-
}
|
|
22
|
-
>
|
|
23
|
-
{element}
|
|
24
|
-
</Suspense>
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
const DemoTasksPage = lazyNamed(() => import('./pages/DemoTasksPage'), 'DemoTasksPage')
|
|
28
|
-
|
|
29
|
-
export const demoRoutes: RouteObject[] = [
|
|
30
|
-
{
|
|
31
|
-
path: 'demo',
|
|
32
|
-
element: <DemoTasksPage />,
|
|
33
|
-
handle: {
|
|
34
|
-
menu: { label: 'Demo Tasks', icon: <AppstoreOutlined /> },
|
|
35
|
-
breadcrumb: 'Demo Tasks',
|
|
36
|
-
permission: 'demo:task:view',
|
|
37
|
-
helpKey: 'demo/tasks',
|
|
38
|
-
},
|
|
39
|
-
},
|
|
40
|
-
].map((route) => ({
|
|
41
|
-
...route,
|
|
42
|
-
element: route.element ? withSuspense(route.element) : route.element,
|
|
43
|
-
}))
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { api, type ApiResponse } from '@robsun/keystone-web-core'
|
|
2
|
-
import type { DemoTask, DemoTaskStatus } from '../types'
|
|
3
|
-
|
|
4
|
-
type TaskListResponse = {
|
|
5
|
-
items: DemoTask[]
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export const listDemoTasks = async () => {
|
|
9
|
-
const { data } = await api.get<ApiResponse<TaskListResponse>>('/demo/tasks')
|
|
10
|
-
return data.data.items
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export const createDemoTask = async (payload: { title: string; status?: DemoTaskStatus }) => {
|
|
14
|
-
const { data } = await api.post<ApiResponse<DemoTask>>('/demo/tasks', payload)
|
|
15
|
-
return data.data
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export const updateDemoTask = async (
|
|
19
|
-
id: number,
|
|
20
|
-
payload: { title?: string; status?: DemoTaskStatus }
|
|
21
|
-
) => {
|
|
22
|
-
const { data } = await api.patch<ApiResponse<DemoTask>>(`/demo/tasks/${id}`, payload)
|
|
23
|
-
return data.data
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export const deleteDemoTask = async (id: number) => {
|
|
27
|
-
await api.delete<ApiResponse<{ id: number }>>(`/demo/tasks/${id}`)
|
|
28
|
-
}
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { useCallback, useEffect, useState } from 'react'
|
|
2
|
-
import { App, Button, Card, Space, Typography } from 'antd'
|
|
3
|
-
import { getHello } from '../services/api'
|
|
4
|
-
|
|
5
|
-
export function ExamplePage() {
|
|
6
|
-
const { message } = App.useApp()
|
|
7
|
-
const [data, setData] = useState<{ message: string; module: string } | null>(null)
|
|
8
|
-
const [loading, setLoading] = useState(false)
|
|
9
|
-
|
|
10
|
-
const fetchData = useCallback(async () => {
|
|
11
|
-
setLoading(true)
|
|
12
|
-
try {
|
|
13
|
-
setData(await getHello())
|
|
14
|
-
} catch (err) {
|
|
15
|
-
message.error(err instanceof Error ? err.message : 'Failed')
|
|
16
|
-
} finally {
|
|
17
|
-
setLoading(false)
|
|
18
|
-
}
|
|
19
|
-
}, [message])
|
|
20
|
-
|
|
21
|
-
useEffect(() => {
|
|
22
|
-
void fetchData()
|
|
23
|
-
}, [fetchData])
|
|
24
|
-
|
|
25
|
-
return (
|
|
26
|
-
<Card title="Example Module">
|
|
27
|
-
<Space direction="vertical">
|
|
28
|
-
<Typography.Title level={4}>Welcome!</Typography.Title>
|
|
29
|
-
<Typography.Text>This module demonstrates core Keystone patterns.</Typography.Text>
|
|
30
|
-
{data && (
|
|
31
|
-
<Card size="small">
|
|
32
|
-
<pre>{JSON.stringify(data, null, 2)}</pre>
|
|
33
|
-
</Card>
|
|
34
|
-
)}
|
|
35
|
-
<Button onClick={fetchData} loading={loading}>
|
|
36
|
-
Refresh
|
|
37
|
-
</Button>
|
|
38
|
-
</Space>
|
|
39
|
-
</Card>
|
|
40
|
-
)
|
|
41
|
-
}
|