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
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 56s
This commit is contained in:
1 parent
1168c7e2ea
commit
290848a93e
5 files changed
+67
-42
No files matched your search
+24
-33
@@ -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>Triage</span>
|
<span className={`relative inline-flex items-center ${stats.inboxCount > 0 ? 'pr-2' : ''}`}>
|
||||||
{stats.inboxCount > 0 && (
|
<span>Triage</span>
|
||||||
<span
|
{stats.inboxCount > 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 === 'triage'
|
{stats.inboxCount}
|
||||||
? 'badge-accent'
|
</span>
|
||||||
: 'badge-neutral'
|
)}
|
||||||
}
|
</span>
|
||||||
>
|
|
||||||
{stats.inboxCount}
|
|
||||||
</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>Aufgaben</span>
|
<span className={`relative inline-flex items-center ${stats.tasksOpenCount > 0 ? 'pr-2' : ''}`}>
|
||||||
{stats.tasksOpenCount > 0 && (
|
<span>Aufgaben</span>
|
||||||
<span
|
{stats.tasksOpenCount > 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 === 'tasks'
|
{stats.tasksOpenCount}
|
||||||
? 'badge-accent'
|
</span>
|
||||||
: 'badge-neutral'
|
)}
|
||||||
}
|
</span>
|
||||||
>
|
|
||||||
{stats.tasksOpenCount}
|
|
||||||
</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>Workspace</span>
|
<span className={`relative inline-flex items-center ${(stats.docsTodosOpenCount ?? 0) > 0 ? 'pr-2' : ''}`}>
|
||||||
<span
|
<span>Workspace</span>
|
||||||
className={
|
{(stats.docsTodosOpenCount ?? 0) > 0 && (
|
||||||
activeTab === 'workspace'
|
<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">
|
||||||
? 'badge-accent'
|
{stats.docsTodosOpenCount}
|
||||||
: 'badge-neutral'
|
</span>
|
||||||
}
|
)}
|
||||||
>
|
|
||||||
{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'
|
||||||
}`}
|
}`}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -330,12 +330,13 @@ type WSMessage struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type DashboardStats struct {
|
type DashboardStats struct {
|
||||||
InboxCount int `json:"inboxCount"`
|
InboxCount int `json:"inboxCount"`
|
||||||
TasksOpenCount int `json:"tasksOpenCount"`
|
TasksOpenCount int `json:"tasksOpenCount"`
|
||||||
TasksDoneCount int `json:"tasksDoneCount"`
|
TasksDoneCount int `json:"tasksDoneCount"`
|
||||||
DocsCount int `json:"docsCount"`
|
DocsCount int `json:"docsCount"`
|
||||||
ListsCount int `json:"listsCount"`
|
DocsTodosOpenCount int `json:"docsTodosOpenCount"`
|
||||||
DiscardedCount int `json:"discardedCount"`
|
ListsCount int `json:"listsCount"`
|
||||||
|
DiscardedCount int `json:"discardedCount"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FullExport struct {
|
type FullExport struct {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user