Skip to content

Commit 4cdb206

Browse files
Merge pull request #8 from owlistic-notes/block-task-improvements
Block task improvements
2 parents 223a448 + 21813fc commit 4cdb206

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

65 files changed

+1504
-1750
lines changed

src/backend/models/block.go

Lines changed: 71 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -13,28 +13,13 @@ import (
1313
type BlockType string
1414

1515
const (
16-
TextBlock BlockType = "text"
17-
TaskBlock BlockType = "task"
16+
TextBlock BlockType = "text"
17+
TaskBlock BlockType = "task"
18+
HeadingBlock BlockType = "heading"
1819
)
1920

20-
// BlockContent stores the structured content of a block
2121
type BlockContent map[string]interface{}
2222

23-
// InlineStyle represents a formatting span within block text
24-
type InlineStyle struct {
25-
Type string `json:"type"`
26-
Start int `json:"start"`
27-
End int `json:"end"`
28-
Href string `json:"href,omitempty"` // For link styles
29-
}
30-
31-
// StyleOptions defines the styling configuration for a block's content
32-
type StyleOptions struct {
33-
RawMarkdown string `json:"raw_markdown,omitempty"` // Original markdown text
34-
InlineStyles []InlineStyle `json:"styles,omitempty"` // Extracted style information
35-
PreserveFormat bool `json:"preserve_format,omitempty"` // Whether to preserve formatting in storage
36-
}
37-
3823
// Value implements the driver.Valuer interface for JSONB storage
3924
func (bc BlockContent) Value() (driver.Value, error) {
4025
if bc == nil {
@@ -58,55 +43,92 @@ func (bc *BlockContent) Scan(value interface{}) error {
5843
return json.Unmarshal(bytes, bc)
5944
}
6045

46+
// BlockMetadata stores flexible metadata specific to each block type
47+
type BlockMetadata map[string]interface{}
48+
49+
// Value implements the driver.Valuer interface for JSONB storage
50+
func (bm BlockMetadata) Value() (driver.Value, error) {
51+
if bm == nil {
52+
return nil, nil
53+
}
54+
return json.Marshal(bm)
55+
}
56+
57+
// Scan implements the sql.Scanner interface for JSONB retrieval
58+
func (bm *BlockMetadata) Scan(value interface{}) error {
59+
if value == nil {
60+
*bm = make(BlockMetadata)
61+
return nil
62+
}
63+
64+
bytes, ok := value.([]byte)
65+
if !ok {
66+
return errors.New("type assertion to []byte failed")
67+
}
68+
69+
return json.Unmarshal(bytes, bm)
70+
}
71+
6172
// Block represents a content block within a note
6273
type Block struct {
6374
ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id"`
6475
UserID uuid.UUID `gorm:"type:uuid;not null;constraint:OnDelete:CASCADE;" json:"user_id"`
6576
NoteID uuid.UUID `gorm:"type:uuid;not null;constraint:OnDelete:CASCADE;" json:"note_id"`
6677
Type BlockType `gorm:"type:varchar(20);not null" json:"type"`
67-
Content BlockContent `gorm:"type:jsonb;not null;default:'{}'::jsonb" json:"content"`
68-
Metadata BlockContent `gorm:"type:jsonb;default:'{}'::jsonb" json:"metadata,omitempty"`
69-
Tasks []Task `gorm:"foreignKey:BlockID" json:"tasks,omitempty"`
70-
Order float64 `gorm:"not null" json:"order"` // Changed from int to float64
78+
Content BlockContent `gorm:"type:jsonb;default:'{}'::jsonb" json:"content"`
79+
Metadata BlockMetadata `gorm:"type:jsonb;default:'{}'::jsonb" json:"metadata"`
80+
Order float64 `gorm:"not null" json:"order"`
7181
CreatedAt time.Time `gorm:"not null;default:now()" json:"created_at"`
7282
UpdatedAt time.Time `gorm:"not null;default:now()" json:"updated_at"`
7383
DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at,omitempty"`
7484
}
7585

76-
// GetInlineStyles returns the block's inline style information
77-
func (b *Block) GetInlineStyles() []InlineStyle {
78-
if b.Content == nil {
79-
return nil
86+
// GetHeadingLevel returns the heading level (for heading blocks)
87+
func (b *Block) GetHeadingLevel() int {
88+
if b.Type != HeadingBlock {
89+
return 0
90+
}
91+
92+
if level, ok := b.Metadata["level"].(float64); ok {
93+
return int(level)
8094
}
95+
return 1 // Default heading level
96+
}
8197

82-
// Try getting styles directly from the content map
83-
if spans, ok := b.Content["spans"].([]interface{}); ok {
84-
styles := make([]InlineStyle, 0, len(spans))
98+
// GetSpans returns formatting spans from metadata
99+
func (b *Block) GetSpans() []map[string]interface{} {
100+
if spans, ok := b.Metadata["spans"].([]interface{}); ok {
101+
result := make([]map[string]interface{}, 0, len(spans))
85102
for _, span := range spans {
86103
if spanMap, ok := span.(map[string]interface{}); ok {
87-
style := InlineStyle{}
88-
89-
if t, ok := spanMap["type"].(string); ok {
90-
style.Type = t
91-
}
92-
93-
if start, ok := spanMap["start"].(float64); ok {
94-
style.Start = int(start)
95-
}
104+
result = append(result, spanMap)
105+
}
106+
}
107+
return result
108+
}
109+
return nil
110+
}
96111

97-
if end, ok := spanMap["end"].(float64); ok {
98-
style.End = int(end)
99-
}
112+
// IsTaskCompleted returns whether a task is completed (for task blocks)
113+
func (b *Block) IsTaskCompleted() bool {
114+
if b.Type != TaskBlock {
115+
return false
116+
}
100117

101-
if href, ok := spanMap["href"].(string); ok {
102-
style.Href = href
103-
}
118+
if completed, ok := b.Metadata["is_completed"].(bool); ok {
119+
return completed
120+
}
121+
return false
122+
}
104123

105-
styles = append(styles, style)
106-
}
107-
}
108-
return styles
124+
// GetTaskID returns the associated task ID (for task blocks)
125+
func (b *Block) GetTaskID() string {
126+
if b.Type != TaskBlock {
127+
return ""
109128
}
110129

111-
return nil
130+
if taskID, ok := b.Metadata["task_id"].(string); ok {
131+
return taskID
132+
}
133+
return ""
112134
}

src/backend/models/block_test.go

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,8 @@ func TestBlockJSON(t *testing.T) {
1414
NoteID: uuid.New(),
1515
Type: TextBlock,
1616
Content: BlockContent{"text": "Test Content"},
17-
Metadata: BlockContent{"format": "markdown"},
17+
Metadata: BlockMetadata{"format": "markdown"},
1818
Order: 1,
19-
Tasks: []Task{},
2019
}
2120

2221
data, err := json.Marshal(block)
@@ -36,7 +35,7 @@ func TestBlockWithTasks(t *testing.T) {
3635
task := Task{
3736
ID: uuid.New(),
3837
UserID: uuid.New(),
39-
BlockID: uuid.New(),
38+
NoteID: uuid.New(),
4039
Title: "Test Task",
4140
Description: "Test Description",
4241
}
@@ -46,7 +45,7 @@ func TestBlockWithTasks(t *testing.T) {
4645
NoteID: uuid.New(),
4746
Type: TaskBlock,
4847
Content: BlockContent{"text": "Task Block"},
49-
Tasks: []Task{task},
48+
Metadata: BlockMetadata{"task_id": task.ID},
5049
Order: 1,
5150
}
5251

@@ -56,8 +55,7 @@ func TestBlockWithTasks(t *testing.T) {
5655
var result Block
5756
err = json.Unmarshal(data, &result)
5857
assert.NoError(t, err)
59-
assert.Equal(t, 1, len(result.Tasks))
60-
assert.Equal(t, task.Title, result.Tasks[0].Title)
58+
assert.Equal(t, task.ID, result.GetTaskID())
6159
}
6260

6361
func TestBlockContentSerialization(t *testing.T) {

src/backend/models/task.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ func (tm *TaskMetadata) Scan(value interface{}) error {
3939
type Task struct {
4040
ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id"`
4141
UserID uuid.UUID `gorm:"type:uuid;not null;constraint:OnDelete:CASCADE;" json:"user_id"`
42-
BlockID uuid.UUID `gorm:"type:uuid;constraint:OnDelete:CASCADE;" json:"block_id"`
42+
NoteID uuid.UUID `gorm:"type:uuid;not null;constraint:OnDelete:CASCADE;" json:"note_id"` // Direct note relationship
4343
Title string `gorm:"not null" json:"title"`
4444
Description string `json:"description"`
4545
IsCompleted bool `gorm:"default:false" json:"is_completed"`

src/backend/routes/blocks_test.go

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -195,39 +195,6 @@ func (m *MockBlockService) ListBlocksByNote(db *database.Database, noteID string
195195
return []models.Block{}, nil
196196
}
197197

198-
// GetBlockWithStyles retrieves a block with its associated style information
199-
func (m *MockBlockService) GetBlockWithStyles(db *database.Database, id string, params map[string]interface{}) (models.Block, map[string]interface{}, error) {
200-
// Check permissions using the params (simplified for tests)
201-
_, hasUserID := params["user_id"]
202-
if !hasUserID {
203-
return models.Block{}, nil, errors.New("user_id must be provided in parameters")
204-
}
205-
206-
if id == "123e4567-e89b-12d3-a456-426614174000" {
207-
block := models.Block{
208-
ID: uuid.Must(uuid.Parse(id)),
209-
NoteID: uuid.Must(uuid.Parse("90a12345-f12a-98c4-a456-513432930000")),
210-
Type: models.TextBlock,
211-
Content: models.BlockContent{"text": "Test Content"},
212-
Order: 1,
213-
}
214-
215-
styleInfo := map[string]interface{}{
216-
"spans": []map[string]interface{}{
217-
{
218-
"start": 0,
219-
"end": 4,
220-
"type": "bold",
221-
},
222-
},
223-
}
224-
225-
return block, styleInfo, nil
226-
}
227-
228-
return models.Block{}, nil, services.ErrBlockNotFound
229-
}
230-
231198
func TestCreateBlock(t *testing.T) {
232199
router := gin.Default()
233200
db := &database.Database{}

src/backend/routes/tasks.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,8 +171,8 @@ func GetTasks(c *gin.Context, db *database.Database, taskService services.TaskSe
171171
params["block_id"] = blockID
172172
}
173173

174-
if completed := c.Query("completed"); completed != "" {
175-
params["completed"] = completed
174+
if completed := c.Query("is_completed"); completed != "" {
175+
params["is_completed"] = completed
176176
}
177177

178178
if title := c.Query("title"); title != "" {

src/backend/routes/tasks_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ type MockTaskService struct{}
2020
// Add GetTasks method for query parameter support
2121
func (m *MockTaskService) GetTasks(db *database.Database, params map[string]interface{}) ([]models.Task, error) {
2222
userID, hasUserID := params["user_id"].(string)
23-
completed, hasCompleted := params["completed"].(string)
23+
completed, hasCompleted := params["is_completed"].(string)
2424

2525
tasks := []models.Task{
2626
{
@@ -72,17 +72,17 @@ func (m *MockTaskService) CreateTask(db *database.Database, taskData map[string]
7272
userID = uuid.Must(uuid.Parse(userIDStr))
7373
}
7474

75-
blockID := uuid.New()
75+
noteID := uuid.New()
7676
noteIDStr, noteIDExists := taskData["note_id"].(string)
7777
if noteIDExists && noteIDStr != "" {
7878
// Simulate finding or creating a block for the note
79-
blockID = uuid.New()
79+
noteID = uuid.New()
8080
}
8181

8282
return models.Task{
8383
ID: uuid.New(),
8484
UserID: userID,
85-
BlockID: blockID,
85+
NoteID: noteID,
8686
Title: title,
8787
}, nil
8888
}

0 commit comments

Comments
 (0)