feat: complete workspace, triage, outliner and sharing overhaul (tasks 1-26)
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 57s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 57s
This commit is contained in:
1 parent
df2f2bac67
commit
d39d1d1122
17 files changed
+1173
-501
No files matched your search
+201
-17
@@ -414,9 +414,10 @@ func (s *Store) migrate() error {
|
||||
// Backfill username for existing accounts where username is empty
|
||||
_, _ = s.db.Exec(`UPDATE users SET username = LOWER(SUBSTR(email, 1, INSTR(email, '@') - 1)) WHERE (username IS NULL OR username = '') AND INSTR(email, '@') > 0`)
|
||||
|
||||
// Ensure all existing document shares are strictly read-only
|
||||
// Ensure all existing document & folder shares are strictly read-only
|
||||
_, _ = s.db.Exec(`UPDATE doc_shares SET permission = 'read' WHERE permission != 'read'`)
|
||||
_, _ = s.db.Exec(`UPDATE doc_team_shares SET permission = 'read' WHERE permission != 'read'`)
|
||||
_, _ = s.db.Exec(`UPDATE workspace_folder_shares SET permission = 'read' WHERE permission != 'read'`)
|
||||
|
||||
// Default system settings (public registration disabled by default; invite or admin creation required)
|
||||
_, _ = s.db.Exec(`INSERT OR IGNORE INTO system_settings (key, value) VALUES
|
||||
@@ -911,6 +912,30 @@ func (s *Store) CreateThoughtWithDetails(userID, content, priority, source, dueD
|
||||
}
|
||||
|
||||
tags := domain.ExtractTags(content)
|
||||
if strings.EqualFold(source, "voice") {
|
||||
hasDiktat := false
|
||||
for _, tag := range tags {
|
||||
if strings.EqualFold(tag, "diktat") {
|
||||
hasDiktat = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasDiktat {
|
||||
tags = append(tags, "diktat")
|
||||
}
|
||||
}
|
||||
if strings.EqualFold(source, "webhook") {
|
||||
hasWebhook := false
|
||||
for _, tag := range tags {
|
||||
if strings.EqualFold(tag, "webhook") {
|
||||
hasWebhook = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasWebhook {
|
||||
tags = append(tags, "webhook")
|
||||
}
|
||||
}
|
||||
tagsJSON, _ := json.Marshal(tags)
|
||||
|
||||
t := &domain.Thought{
|
||||
@@ -968,6 +993,11 @@ func scanThought(rows interface {
|
||||
}
|
||||
t.Tags = []string{}
|
||||
_ = json.Unmarshal([]byte(tagsStr), &t.Tags)
|
||||
if t.ConvertedType == "document_block" && strings.Contains(t.ConvertedRefID, ":") {
|
||||
parts := strings.SplitN(t.ConvertedRefID, ":", 2)
|
||||
t.DocID = parts[0]
|
||||
t.BlockID = parts[1]
|
||||
}
|
||||
t.Subtasks = []domain.SubTask{}
|
||||
if subtasksStr != "" && subtasksStr != "null" {
|
||||
_ = json.Unmarshal([]byte(subtasksStr), &t.Subtasks)
|
||||
@@ -1137,23 +1167,23 @@ func (s *Store) UpdateThoughtTriage(userID, thoughtID, action, convertTo, target
|
||||
title = title[:72] + "…"
|
||||
}
|
||||
blocks := make([]domain.DocumentBlock, 0)
|
||||
formattedDate := domain.FormatGermanDateTime(thought.CreatedAt)
|
||||
calloutText := "Ursprünglicher Gedanke aus deiner Inbox"
|
||||
if formattedDate != "" {
|
||||
calloutText += " vom " + formattedDate
|
||||
}
|
||||
|
||||
// 17. Wenn ich eine Aufgabe zu einer Notiz mache, muss die erste Zeile das "To-Do" noch mal als "To-Do" enthalten, auch wenn es schon in der Überschrift steht. Wenn es vorher schon abgehakt war, dann den Zustand natürlich übernehmen.
|
||||
blocks = append(blocks, domain.DocumentBlock{
|
||||
ID: domain.NewID("blk"),
|
||||
Type: "callout",
|
||||
Content: calloutText,
|
||||
Type: "todo",
|
||||
Content: thought.Content,
|
||||
Checked: thought.IsCompleted,
|
||||
Level: 0,
|
||||
})
|
||||
|
||||
// 17a. Die anderen Details aus der Aufgabe sollen natürlich ebenfalls übernommen werden (Notizen, Subtasks, Fälligkeit/Priorität).
|
||||
if strings.TrimSpace(thought.Notes) != "" {
|
||||
blocks = append(blocks, domain.DocumentBlock{
|
||||
ID: domain.NewID("blk"),
|
||||
Type: "bullet",
|
||||
Content: strings.TrimSpace(thought.Notes),
|
||||
Level: 0,
|
||||
Level: 1,
|
||||
})
|
||||
}
|
||||
if len(thought.Subtasks) > 0 {
|
||||
@@ -1163,10 +1193,25 @@ func (s *Store) UpdateThoughtTriage(userID, thoughtID, action, convertTo, target
|
||||
Type: "todo",
|
||||
Content: st.Title,
|
||||
Checked: st.IsCompleted,
|
||||
Level: 0,
|
||||
Level: 1,
|
||||
})
|
||||
}
|
||||
}
|
||||
var metaItems []string
|
||||
if thought.DueDate != "" {
|
||||
metaItems = append(metaItems, "Fällig: "+thought.DueDate)
|
||||
}
|
||||
if thought.Priority != "" && thought.Priority != "normal" {
|
||||
metaItems = append(metaItems, "Priorität: "+thought.Priority)
|
||||
}
|
||||
if len(metaItems) > 0 {
|
||||
blocks = append(blocks, domain.DocumentBlock{
|
||||
ID: domain.NewID("blk"),
|
||||
Type: "bullet",
|
||||
Content: strings.Join(metaItems, " • "),
|
||||
Level: 1,
|
||||
})
|
||||
}
|
||||
if len(blocks) == 1 {
|
||||
blocks = append(blocks, domain.DocumentBlock{
|
||||
ID: domain.NewID("blk"),
|
||||
@@ -2354,6 +2399,11 @@ func (s *Store) UpdateDocument(userID, docID string, title, icon, folderPath *st
|
||||
}
|
||||
if blocks != nil {
|
||||
_ = s.indexDocBlockTags(doc.ID, doc.UserID, doc.Blocks)
|
||||
for _, b := range doc.Blocks {
|
||||
if b.TaskID != "" {
|
||||
s.SyncTaskFromBlockCheck(doc.UserID, b.TaskID, b.Checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
return doc, nil
|
||||
}
|
||||
@@ -2888,8 +2938,8 @@ func (s *Store) MoveDocument(userID, docID, targetFolderPath string) (*domain.Do
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !shFolder.IsOwner && shFolder.Permission != "write" {
|
||||
return nil, fmt.Errorf("keine Schreibberechtigung für diesen geteilten Ordner")
|
||||
if !shFolder.IsOwner {
|
||||
return nil, fmt.Errorf("nur der Eigentümer dieses geteilten Ordners kann Dateien dorthin verschieben")
|
||||
}
|
||||
|
||||
// Update document to belong to targetSharedFolderID
|
||||
@@ -3338,8 +3388,8 @@ func (s *Store) CreateFolderShare(currentUserID, folderID, targetType, targetIde
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !f.IsOwner && f.Permission != "write" {
|
||||
return nil, fmt.Errorf("nur der Eigentümer oder Bearbeiter kann Ordner-Freigaben verwalten")
|
||||
if !f.IsOwner {
|
||||
return nil, fmt.Errorf("nur der Eigentümer kann Ordner-Freigaben verwalten")
|
||||
}
|
||||
|
||||
cleanType := strings.TrimSpace(targetType)
|
||||
@@ -3352,9 +3402,8 @@ func (s *Store) CreateFolderShare(currentUserID, folderID, targetType, targetIde
|
||||
}
|
||||
}
|
||||
|
||||
if permission != "read" && permission != "write" {
|
||||
permission = "read"
|
||||
}
|
||||
// Geteilte Ordner können ausschließlich mit Leserberechtigung geteilt werden (wie Dokumente)
|
||||
permission = "read"
|
||||
|
||||
var targetID, targetName, email, avatarURL string
|
||||
if cleanType == "team" {
|
||||
@@ -4090,6 +4139,141 @@ func (s *Store) ToggleListItem(itemID string, isCompleted bool, userID, userName
|
||||
return &it, nil
|
||||
}
|
||||
|
||||
func (s *Store) UpdateListItem(itemID string, content *string, category *string, quantity *string) (*domain.ListItem, error) {
|
||||
now := domain.NowISO()
|
||||
var it domain.ListItem
|
||||
var dInt int
|
||||
err := s.db.QueryRow(
|
||||
`SELECT id, list_id, content, category, quantity, is_completed, completed_by, completed_by_name, sort_order, created_at, completed_at
|
||||
FROM list_items WHERE id = ?`,
|
||||
itemID,
|
||||
).Scan(&it.ID, &it.ListID, &it.Content, &it.Category, &it.Quantity, &dInt, &it.CompletedBy, &it.CompletedByName, &it.SortOrder, &it.CreatedAt, &it.CompletedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
it.IsCompleted = dInt != 0
|
||||
|
||||
if content != nil {
|
||||
it.Content = *content
|
||||
}
|
||||
if category != nil {
|
||||
it.Category = *category
|
||||
}
|
||||
if quantity != nil {
|
||||
it.Quantity = *quantity
|
||||
}
|
||||
|
||||
_, err = s.db.Exec(`UPDATE list_items SET content = ?, category = ?, quantity = ? WHERE id = ?`, it.Content, it.Category, it.Quantity, itemID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, _ = s.db.Exec(`UPDATE lists SET updated_at = ? WHERE id = ?`, now, it.ListID)
|
||||
return &it, nil
|
||||
}
|
||||
|
||||
func (s *Store) CreateTaskFromDocBlock(userID, docID, blockID, content string, checked bool) (*domain.Thought, error) {
|
||||
doc, err := s.GetDocumentByID(userID, docID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t, err := s.CreateThoughtWithDetails(userID, content, "normal", "manual", "", "", "todo", "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t.ConvertedType = "document_block"
|
||||
t.ConvertedRefID = docID + ":" + blockID
|
||||
t.DocID = docID
|
||||
t.BlockID = blockID
|
||||
t.IsCompleted = checked
|
||||
doneInt := 0
|
||||
if checked {
|
||||
doneInt = 1
|
||||
t.KanbanStatus = "done"
|
||||
}
|
||||
_, _ = s.db.Exec(`UPDATE thoughts SET converted_type = 'document_block', converted_ref_id = ?, is_completed = ?, kanban_status = ? WHERE id = ?`,
|
||||
t.ConvertedRefID, doneInt, t.KanbanStatus, t.ID)
|
||||
|
||||
var updatedBlocks []domain.DocumentBlock
|
||||
found := false
|
||||
for _, b := range doc.Blocks {
|
||||
if b.ID == blockID {
|
||||
b.TaskID = t.ID
|
||||
b.Checked = checked
|
||||
found = true
|
||||
}
|
||||
updatedBlocks = append(updatedBlocks, b)
|
||||
}
|
||||
if found {
|
||||
_, _ = s.UpdateDocument(userID, docID, nil, nil, nil, &updatedBlocks, nil, nil)
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func (s *Store) SyncDocBlockAndTask(userID, taskID string, isCompleted bool) {
|
||||
t, err := s.GetThoughtByID(userID, taskID)
|
||||
if err != nil || t == nil || t.ConvertedType != "document_block" || !strings.Contains(t.ConvertedRefID, ":") {
|
||||
return
|
||||
}
|
||||
parts := strings.SplitN(t.ConvertedRefID, ":", 2)
|
||||
docID, blockID := parts[0], parts[1]
|
||||
doc, err := s.GetDocumentByID(userID, docID)
|
||||
if err != nil || doc == nil {
|
||||
return
|
||||
}
|
||||
var updatedBlocks []domain.DocumentBlock
|
||||
found := false
|
||||
for _, b := range doc.Blocks {
|
||||
if b.ID == blockID {
|
||||
if b.Checked != isCompleted {
|
||||
b.Checked = isCompleted
|
||||
found = true
|
||||
}
|
||||
}
|
||||
updatedBlocks = append(updatedBlocks, b)
|
||||
}
|
||||
if found {
|
||||
_, _ = s.UpdateDocument(userID, docID, nil, nil, nil, &updatedBlocks, nil, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Store) UnlinkTaskFromDocBlock(userID, taskID string) {
|
||||
t, err := s.GetThoughtByID(userID, taskID)
|
||||
if err != nil || t == nil || t.ConvertedType != "document_block" || !strings.Contains(t.ConvertedRefID, ":") {
|
||||
return
|
||||
}
|
||||
parts := strings.SplitN(t.ConvertedRefID, ":", 2)
|
||||
docID, blockID := parts[0], parts[1]
|
||||
doc, err := s.GetDocumentByID(userID, docID)
|
||||
if err != nil || doc == nil {
|
||||
return
|
||||
}
|
||||
var updatedBlocks []domain.DocumentBlock
|
||||
found := false
|
||||
for _, b := range doc.Blocks {
|
||||
if b.ID == blockID {
|
||||
b.TaskID = ""
|
||||
found = true
|
||||
}
|
||||
updatedBlocks = append(updatedBlocks, b)
|
||||
}
|
||||
if found {
|
||||
_, _ = s.UpdateDocument(userID, docID, nil, nil, nil, &updatedBlocks, nil, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Store) SyncTaskFromBlockCheck(userID, taskID string, checked bool) {
|
||||
if taskID == "" {
|
||||
return
|
||||
}
|
||||
doneInt := 0
|
||||
kanban := "todo"
|
||||
if checked {
|
||||
doneInt = 1
|
||||
kanban = "done"
|
||||
}
|
||||
_, _ = s.db.Exec(`UPDATE thoughts SET is_completed = ?, kanban_status = ? WHERE id = ? AND user_id = ?`, doneInt, kanban, taskID, userID)
|
||||
}
|
||||
|
||||
func (s *Store) DeleteListItem(itemID string) (string, error) {
|
||||
var listID string
|
||||
_ = s.db.QueryRow(`SELECT list_id FROM list_items WHERE id = ?`, itemID).Scan(&listID)
|
||||
|
||||
Reference in new issue
Block a user