feat(tasks): refine enhanced task linking, outliner badge positioning, completion sync and unlink behavior
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 59s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 59s
This commit is contained in:
1 parent
6129681035
commit
5cf6b59377
5 files changed
+139
-60
No files matched your search
@@ -2196,8 +2196,24 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Badges: Priority, Status, Tags */}
|
{/* Badges: In Notiz, Priority, Status, Tags */}
|
||||||
<div className="flex flex-wrap items-center gap-1 pt-0.5" onClick={(e) => e.stopPropagation()}>
|
<div className="flex flex-wrap items-center gap-1 pt-0.5" onClick={(e) => e.stopPropagation()}>
|
||||||
|
{(task.convertedType === 'document_block' || task.docId) && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const docId = task.docId || (task.convertedRefId ? task.convertedRefId.split(':')[0] : '');
|
||||||
|
const blockId = task.blockId || (task.convertedRefId ? task.convertedRefId.split(':')[1] : '');
|
||||||
|
if (docId) onOpenDocument(docId, blockId);
|
||||||
|
}}
|
||||||
|
className="h-4 px-1.5 text-[9px] rounded-btn border border-accent/40 bg-accent/10 text-accent hover:bg-accent/20 cursor-pointer flex items-center gap-1 font-medium shrink-0"
|
||||||
|
title="In Notiz an der passenden Stelle öffnen"
|
||||||
|
>
|
||||||
|
<FileText className="w-2.5 h-2.5" />
|
||||||
|
<span>In Notiz</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
{task.priority === 'urgent' && (
|
{task.priority === 'urgent' && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -619,23 +619,20 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
|
|||||||
{localContent ? renderInlineMarkdown(localContent) : ''}
|
{localContent ? renderInlineMarkdown(localContent) : ''}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{/* Inline Badge or 3-Dots directly behind the text */}
|
{/* Inline Badge and 3-Dots directly behind the text */}
|
||||||
{block.type === 'todo' && (
|
{block.type === 'todo' && (
|
||||||
<span className="inline-flex items-center align-middle ml-1.5 gap-1 shrink-0 pointer-events-auto z-10 relative select-none">
|
<span className="inline-flex items-center align-middle ml-1.5 gap-1 shrink-0 pointer-events-auto z-10 relative select-none">
|
||||||
{block.taskId ? (
|
{block.taskId && (
|
||||||
<button
|
<span
|
||||||
type="button"
|
className="badge-accent text-[9px] inline-flex items-center gap-0.5 px-1.5 py-0.2 rounded font-medium shrink-0"
|
||||||
onClick={(e) => {
|
title="Mit erweiterter Aufgabe synchronisiert"
|
||||||
e.stopPropagation();
|
|
||||||
onOpenTask?.(block.taskId!);
|
|
||||||
}}
|
|
||||||
className="badge-accent text-[9px] inline-flex items-center gap-0.5 px-1.5 py-0.5 rounded font-medium cursor-pointer hover:opacity-80 transition-opacity"
|
|
||||||
title="Zu dieser erweiterten Aufgabe wechseln"
|
|
||||||
>
|
>
|
||||||
<CheckSquare className="w-2.5 h-2.5" />
|
<CheckSquare className="w-2.5 h-2.5" />
|
||||||
<span>Erweitert</span>
|
<span>Erweitert</span>
|
||||||
</button>
|
</span>
|
||||||
) : canEdit ? (
|
)}
|
||||||
|
|
||||||
|
{canEdit && (
|
||||||
<span className="relative inline-flex items-center">
|
<span className="relative inline-flex items-center">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -653,37 +650,63 @@ export const OutlinerBlock: React.FC<OutlinerBlockProps> = ({
|
|||||||
className="absolute left-0 top-full mt-1 bg-panel border border-border shadow-lg rounded-card p-1 z-30 w-52 text-xs"
|
className="absolute left-0 top-full mt-1 bg-panel border border-border shadow-lg rounded-card p-1 z-30 w-52 text-xs"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<button
|
{!block.taskId ? (
|
||||||
type="button"
|
<button
|
||||||
onClick={async () => {
|
type="button"
|
||||||
setShowTodoMenu(false);
|
onClick={async () => {
|
||||||
try {
|
setShowTodoMenu(false);
|
||||||
const targetDocId = docId || block.documentId || block.docId || '';
|
try {
|
||||||
const thought = await api.createEnhancedTask(targetDocId, block.id, {
|
const targetDocId = docId || block.documentId || block.docId || '';
|
||||||
content: localContent || 'Aufgabe',
|
const thought = await api.createEnhancedTask(targetDocId, block.id, {
|
||||||
checked: Boolean(block.checked),
|
content: localContent || 'Aufgabe',
|
||||||
});
|
checked: Boolean(block.checked),
|
||||||
const updatedBlock: DocumentBlock = {
|
});
|
||||||
...latestBlockRef.current,
|
const updatedBlock: DocumentBlock = {
|
||||||
content: localContent,
|
...latestBlockRef.current,
|
||||||
taskId: thought.id,
|
content: localContent,
|
||||||
type: 'todo',
|
taskId: thought.id,
|
||||||
};
|
type: 'todo',
|
||||||
onUpdateBlockRef.current(updatedBlock, true);
|
};
|
||||||
onOpenTask?.(thought.id);
|
onUpdateBlockRef.current(updatedBlock, true);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
alert(err.message || 'Fehler beim Erstellen der erweiterten Aufgabe');
|
alert(err.message || 'Fehler beim Erstellen der erweiterten Aufgabe');
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="w-full text-left px-2 py-1.5 rounded-btn hover:bg-panel-elevated hover:text-accent flex items-center gap-1.5 text-main font-medium cursor-pointer"
|
className="w-full text-left px-2 py-1.5 rounded-btn hover:bg-panel-elevated hover:text-accent flex items-center gap-1.5 text-main font-medium cursor-pointer"
|
||||||
>
|
>
|
||||||
<CheckSquare className="w-3.5 h-3.5 text-accent" />
|
<CheckSquare className="w-3.5 h-3.5 text-accent" />
|
||||||
<span>In erweiterte Aufgabe umwandeln</span>
|
<span>In erweiterte Aufgabe umwandeln</span>
|
||||||
</button>
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={async () => {
|
||||||
|
setShowTodoMenu(false);
|
||||||
|
try {
|
||||||
|
if (block.taskId) {
|
||||||
|
await api.unlinkEnhancedTask(block.taskId).catch(() => {});
|
||||||
|
}
|
||||||
|
const updatedBlock: DocumentBlock = {
|
||||||
|
...latestBlockRef.current,
|
||||||
|
content: localContent,
|
||||||
|
taskId: '',
|
||||||
|
type: 'todo',
|
||||||
|
};
|
||||||
|
onUpdateBlockRef.current(updatedBlock, true);
|
||||||
|
} catch (err: any) {
|
||||||
|
alert(err.message || 'Fehler beim Aufheben der Verknüpfung');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="w-full text-left px-2 py-1.5 rounded-btn hover:bg-panel-elevated hover:text-status-warn flex items-center gap-1.5 text-main font-medium cursor-pointer"
|
||||||
|
>
|
||||||
|
<X className="w-3.5 h-3.5 text-status-warn" />
|
||||||
|
<span>Verknüpfung aufheben</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -664,6 +664,12 @@ export const api = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async unlinkEnhancedTask(taskId: string): Promise<{ unlinked: boolean }> {
|
||||||
|
return request<{ unlinked: boolean }>(`/api/v1/tasks/${encodeURIComponent(taskId)}/unlink`, {
|
||||||
|
method: 'POST',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
// --- DOCUMENT SHARING ---
|
// --- DOCUMENT SHARING ---
|
||||||
async getDocShares(docId: string): Promise<DocShare[]> {
|
async getDocShares(docId: string): Promise<DocShare[]> {
|
||||||
const res = await request<DocShare[]>(`/api/v1/documents/${docId}/shares`);
|
const res = await request<DocShare[]>(`/api/v1/documents/${docId}/shares`);
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ func (s *Server) Router() http.Handler {
|
|||||||
r.Get("/api/v1/triage/deck", s.handleGetTriageDeck)
|
r.Get("/api/v1/triage/deck", s.handleGetTriageDeck)
|
||||||
r.Patch("/api/v1/triage/{id}", s.handleTriageAction)
|
r.Patch("/api/v1/triage/{id}", s.handleTriageAction)
|
||||||
r.Get("/api/v1/tasks", s.handleGetTasks)
|
r.Get("/api/v1/tasks", s.handleGetTasks)
|
||||||
|
r.Post("/api/v1/tasks/{id}/unlink", s.handleUnlinkEnhancedTask)
|
||||||
r.Post("/api/v1/tasks/reorder", s.handleReorderTasks)
|
r.Post("/api/v1/tasks/reorder", s.handleReorderTasks)
|
||||||
r.Post("/api/v1/task-lists/{id}/tasks/reorder", s.handleReorderTasks)
|
r.Post("/api/v1/task-lists/{id}/tasks/reorder", s.handleReorderTasks)
|
||||||
|
|
||||||
@@ -842,13 +843,18 @@ func (s *Server) handleUpdateThought(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeError(w, http.StatusBadRequest, err.Error())
|
writeError(w, http.StatusBadRequest, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if req.IsCompleted != nil {
|
s.store.SyncDocBlockAndTask(userID, thoughtID, updated.IsCompleted)
|
||||||
s.store.SyncDocBlockAndTask(userID, thoughtID, *req.IsCompleted)
|
|
||||||
}
|
|
||||||
s.wsHub.BroadcastToAll("TASK_UPDATED", updated)
|
s.wsHub.BroadcastToAll("TASK_UPDATED", updated)
|
||||||
writeJSON(w, http.StatusOK, updated)
|
writeJSON(w, http.StatusOK, updated)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleUnlinkEnhancedTask(w http.ResponseWriter, r *http.Request) {
|
||||||
|
userID := auth.GetUserID(r.Context())
|
||||||
|
thoughtID := chi.URLParam(r, "id")
|
||||||
|
s.store.UnlinkTaskFromDocBlock(userID, thoughtID)
|
||||||
|
writeJSON(w, http.StatusOK, map[string]bool{"unlinked": true})
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) handleDeleteThought(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleDeleteThought(w http.ResponseWriter, r *http.Request) {
|
||||||
userID := auth.GetUserID(r.Context())
|
userID := auth.GetUserID(r.Context())
|
||||||
thoughtID := chi.URLParam(r, "id")
|
thoughtID := chi.URLParam(r, "id")
|
||||||
|
|||||||
+44
-16
@@ -2404,6 +2404,29 @@ func (s *Store) UpdateDocument(userID, docID string, title, icon, folderPath *st
|
|||||||
s.SyncTaskFromBlockCheck(doc.UserID, b.TaskID, b.Checked)
|
s.SyncTaskFromBlockCheck(doc.UserID, b.TaskID, b.Checked)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When a block is deleted from the note, unlink the corresponding task in thoughts without deleting the task
|
||||||
|
remainingBlockIDs := make(map[string]bool)
|
||||||
|
for _, b := range doc.Blocks {
|
||||||
|
remainingBlockIDs[b.ID] = true
|
||||||
|
}
|
||||||
|
rows, err := s.db.Query(`SELECT id, converted_ref_id, block_id FROM thoughts WHERE (doc_id = ? OR converted_ref_id LIKE ?) AND converted_type = 'document_block'`, docID, docID+":%")
|
||||||
|
if err == nil {
|
||||||
|
for rows.Next() {
|
||||||
|
var tid, refID, bID string
|
||||||
|
if err := rows.Scan(&tid, &refID, &bID); err == nil {
|
||||||
|
bid := bID
|
||||||
|
if bid == "" && strings.Contains(refID, ":") {
|
||||||
|
parts := strings.SplitN(refID, ":", 2)
|
||||||
|
bid = parts[1]
|
||||||
|
}
|
||||||
|
if !remainingBlockIDs[bid] {
|
||||||
|
_, _ = s.db.Exec(`UPDATE thoughts SET converted_type = '', converted_ref_id = '', doc_id = '', block_id = '' WHERE id = ?`, tid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = rows.Close()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return doc, nil
|
return doc, nil
|
||||||
}
|
}
|
||||||
@@ -4239,27 +4262,32 @@ func (s *Store) SyncDocBlockAndTask(userID, taskID string, isCompleted bool) {
|
|||||||
|
|
||||||
func (s *Store) UnlinkTaskFromDocBlock(userID, taskID string) {
|
func (s *Store) UnlinkTaskFromDocBlock(userID, taskID string) {
|
||||||
t, err := s.GetThoughtByID(userID, taskID)
|
t, err := s.GetThoughtByID(userID, taskID)
|
||||||
if err != nil || t == nil || t.ConvertedType != "document_block" || !strings.Contains(t.ConvertedRefID, ":") {
|
if err != nil || t == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
parts := strings.SplitN(t.ConvertedRefID, ":", 2)
|
docID := t.DocID
|
||||||
docID, blockID := parts[0], parts[1]
|
blockID := t.BlockID
|
||||||
doc, err := s.GetDocumentByID(userID, docID)
|
if (docID == "" || blockID == "") && strings.Contains(t.ConvertedRefID, ":") {
|
||||||
if err != nil || doc == nil {
|
parts := strings.SplitN(t.ConvertedRefID, ":", 2)
|
||||||
return
|
docID, blockID = parts[0], parts[1]
|
||||||
}
|
}
|
||||||
var updatedBlocks []domain.DocumentBlock
|
if docID != "" {
|
||||||
found := false
|
if doc, err := s.GetDocumentByID(userID, docID); err == nil && doc != nil {
|
||||||
for _, b := range doc.Blocks {
|
var updatedBlocks []domain.DocumentBlock
|
||||||
if b.ID == blockID {
|
found := false
|
||||||
b.TaskID = ""
|
for _, b := range doc.Blocks {
|
||||||
found = true
|
if b.ID == blockID || b.TaskID == taskID {
|
||||||
|
b.TaskID = ""
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
updatedBlocks = append(updatedBlocks, b)
|
||||||
|
}
|
||||||
|
if found {
|
||||||
|
_, _ = s.UpdateDocument(userID, docID, nil, nil, nil, &updatedBlocks, nil, nil)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
updatedBlocks = append(updatedBlocks, b)
|
|
||||||
}
|
|
||||||
if found {
|
|
||||||
_, _ = s.UpdateDocument(userID, docID, nil, nil, nil, &updatedBlocks, nil, nil)
|
|
||||||
}
|
}
|
||||||
|
_, _ = s.db.Exec(`UPDATE thoughts SET converted_type = '', converted_ref_id = '', doc_id = '', block_id = '' WHERE id = ?`, taskID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) SyncTaskFromBlockCheck(userID, taskID string, checked bool) {
|
func (s *Store) SyncTaskFromBlockCheck(userID, taskID string, checked bool) {
|
||||||
|
|||||||
Reference in new issue
Block a user