feat(navigation): count open workspace todos and style tab badges as notification dots
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 56s

This commit is contained in:
sandrodach committed 2026-10-25 10:25:21 +01:00
1 parent 1168c7e2ea
commit 290848a93e
5 files changed
+49 -24

No files matched your search

+13 -22
View File
@@ -102,6 +102,7 @@ export const App: React.FC = () => {
tasksOpenCount: 0, tasksOpenCount: 0,
tasksDoneCount: 0, tasksDoneCount: 0,
docsCount: 0, docsCount: 0,
docsTodosOpenCount: 0,
listsCount: 0, listsCount: 0,
discardedCount: 0, discardedCount: 0,
}); });
@@ -478,18 +479,14 @@ export const App: React.FC = () => {
className={`tab-btn ${activeTab === 'triage' ? 'tab-btn-active' : ''}`} className={`tab-btn ${activeTab === 'triage' ? 'tab-btn-active' : ''}`}
> >
<Layers className="w-4 h-4" /> <Layers className="w-4 h-4" />
<span className={`relative inline-flex items-center ${stats.inboxCount > 0 ? 'pr-2' : ''}`}>
<span>Triage</span> <span>Triage</span>
{stats.inboxCount > 0 && ( {stats.inboxCount > 0 && (
<span <span className="absolute -top-1.5 -right-1 min-w-[15px] h-[15px] px-0.5 rounded-full text-[9px] font-bold leading-none bg-accent text-on-accent flex items-center justify-center shadow-sm pointer-events-none">
className={
activeTab === 'triage'
? 'badge-accent'
: 'badge-neutral'
}
>
{stats.inboxCount} {stats.inboxCount}
</span> </span>
)} )}
</span>
</button> </button>
<button <button
@@ -500,18 +497,14 @@ export const App: React.FC = () => {
className={`tab-btn ${activeTab === 'tasks' ? 'tab-btn-active' : ''}`} className={`tab-btn ${activeTab === 'tasks' ? 'tab-btn-active' : ''}`}
> >
<CheckSquare className="w-4 h-4" /> <CheckSquare className="w-4 h-4" />
<span className={`relative inline-flex items-center ${stats.tasksOpenCount > 0 ? 'pr-2' : ''}`}>
<span>Aufgaben</span> <span>Aufgaben</span>
{stats.tasksOpenCount > 0 && ( {stats.tasksOpenCount > 0 && (
<span <span className="absolute -top-1.5 -right-1 min-w-[15px] h-[15px] px-0.5 rounded-full text-[9px] font-bold leading-none bg-accent text-on-accent flex items-center justify-center shadow-sm pointer-events-none">
className={
activeTab === 'tasks'
? 'badge-accent'
: 'badge-neutral'
}
>
{stats.tasksOpenCount} {stats.tasksOpenCount}
</span> </span>
)} )}
</span>
</button> </button>
<button <button
@@ -522,15 +515,13 @@ export const App: React.FC = () => {
className={`tab-btn ${activeTab === 'workspace' ? 'tab-btn-active' : ''}`} className={`tab-btn ${activeTab === 'workspace' ? 'tab-btn-active' : ''}`}
> >
<FileText className="w-4 h-4" /> <FileText className="w-4 h-4" />
<span className={`relative inline-flex items-center ${(stats.docsTodosOpenCount ?? 0) > 0 ? 'pr-2' : ''}`}>
<span>Workspace</span> <span>Workspace</span>
<span {(stats.docsTodosOpenCount ?? 0) > 0 && (
className={ <span className="absolute -top-1.5 -right-1 min-w-[15px] h-[15px] px-0.5 rounded-full text-[9px] font-bold leading-none bg-accent text-on-accent flex items-center justify-center shadow-sm pointer-events-none">
activeTab === 'workspace' {stats.docsTodosOpenCount}
? 'badge-accent' </span>
: 'badge-neutral' )}
}
>
{stats.docsCount}
</span> </span>
</button> </button>
@@ -260,6 +260,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
} }
: prev : prev
); );
onStatsChange();
} catch (e: any) { } catch (e: any) {
if (e.message && e.message.includes('423')) { if (e.message && e.message.includes('423')) {
api.getDocLock(docToSave.id).then((res) => { api.getDocLock(docToSave.id).then((res) => {
@@ -2162,9 +2163,15 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
type="checkbox" type="checkbox"
disabled={isReadOnly} disabled={isReadOnly}
checked={!!block.checked} checked={!!block.checked}
onChange={(e) => onChange={(e) => {
!isReadOnly && updateBlock(block.id, { checked: e.target.checked }) if (!isReadOnly && selectedDoc) {
const nextBlocks = selectedDoc.blocks.map((b) =>
b.id === block.id ? { ...b, checked: e.target.checked } : b
);
persistDocument({ ...selectedDoc, blocks: nextBlocks }, true);
onStatsChange();
} }
}}
className={`w-4 h-4 checkbox-square flex-shrink-0 ${ className={`w-4 h-4 checkbox-square flex-shrink-0 ${
isReadOnly ? 'cursor-not-allowed opacity-60' : 'cursor-pointer' isReadOnly ? 'cursor-not-allowed opacity-60' : 'cursor-pointer'
}`} }`}
+1
View File
@@ -233,6 +233,7 @@ export interface DashboardStats {
tasksOpenCount: number; tasksOpenCount: number;
tasksDoneCount: number; tasksDoneCount: number;
docsCount: number; docsCount: number;
docsTodosOpenCount?: number;
listsCount: number; listsCount: number;
discardedCount: number; discardedCount: number;
} }
+1
View File
@@ -334,6 +334,7 @@ type DashboardStats struct {
TasksOpenCount int `json:"tasksOpenCount"` TasksOpenCount int `json:"tasksOpenCount"`
TasksDoneCount int `json:"tasksDoneCount"` TasksDoneCount int `json:"tasksDoneCount"`
DocsCount int `json:"docsCount"` DocsCount int `json:"docsCount"`
DocsTodosOpenCount int `json:"docsTodosOpenCount"`
ListsCount int `json:"listsCount"` ListsCount int `json:"listsCount"`
DiscardedCount int `json:"discardedCount"` DiscardedCount int `json:"discardedCount"`
} }
+25
View File
@@ -1910,6 +1910,31 @@ func (s *Store) GetDashboardStats(userID string) (*domain.DashboardStats, error)
_ = s.db.QueryRow(`SELECT COUNT(*) FROM documents WHERE user_id = ? AND is_archived = 0`, userID).Scan(&st.DocsCount) _ = s.db.QueryRow(`SELECT COUNT(*) FROM documents WHERE user_id = ? AND is_archived = 0`, userID).Scan(&st.DocsCount)
_ = s.db.QueryRow(`SELECT COUNT(*) FROM thoughts WHERE user_id = ? AND status = 'discarded'`, userID).Scan(&st.DiscardedCount) _ = s.db.QueryRow(`SELECT COUNT(*) FROM thoughts WHERE user_id = ? AND status = 'discarded'`, userID).Scan(&st.DiscardedCount)
_ = s.db.QueryRow(`SELECT COUNT(*) FROM lists l JOIN space_members sm ON sm.space_id = l.space_id WHERE sm.user_id = ?`, userID).Scan(&st.ListsCount) _ = s.db.QueryRow(`SELECT COUNT(*) FROM lists l JOIN space_members sm ON sm.space_id = l.space_id WHERE sm.user_id = ?`, userID).Scan(&st.ListsCount)
// Count open (uncompleted) To-Dos across accessible workspace documents (owned or shared)
docRows, err := s.db.Query(`
SELECT blocks_json FROM documents
WHERE (user_id = ? OR id IN (SELECT doc_id FROM doc_shares WHERE user_id = ?))
AND is_archived = 0`,
userID, userID,
)
if err == nil {
for docRows.Next() {
var bj string
if err := docRows.Scan(&bj); err == nil && bj != "" {
var blocks []domain.DocumentBlock
if err := json.Unmarshal([]byte(bj), &blocks); err == nil {
for _, b := range blocks {
if b.Type == "todo" && !b.Checked {
st.DocsTodosOpenCount++
}
}
}
}
}
_ = docRows.Close()
}
return st, nil return st, nil
} }