• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

SNApp-notes / web / 19855984353

02 Dec 2025 10:48AM UTC coverage: 86.761% (+0.04%) from 86.718%
19855984353

push

github

jcubic
update save confirmation

609 of 730 branches covered (83.42%)

Branch coverage included in aggregate %.

1 of 1 new or added line in 1 file covered. (100.0%)

13 existing lines in 3 files now uncovered.

1108 of 1249 relevant lines covered (88.71%)

2276.72 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

73.81
/src/hooks/useNodeSelection.ts
1
/**
2
 * @module hooks/useNodeSelection
3
 * @description Custom React hook for managing note tree selection and state.
4
 * Provides centralized state management for note selection, content updates, dirty flags, and save status.
5
 *
6
 * @dependencies
7
 * - @/types/tree: NoteTreeNode type definition
8
 * - @/types/notes: SaveStatus type definition
9
 *
10
 * @remarks
11
 * - Used in NotesLayoutWrapper to manage note list and selection state
12
 * - Ensures only one note is selected at a time
13
 * - Tracks dirty flags for unsaved changes
14
 * - Optimized with useCallback to prevent unnecessary re-renders
15
 * - Immutable state updates for predictable behavior
16
 *
17
 * @example
18
 * ```tsx
19
 * function NotesManager() {
20
 *   const {
21
 *     notes,
22
 *     selectedNoteId,
23
 *     saveStatus,
24
 *     updateSelection,
25
 *     updateNoteContent,
26
 *     updateDirtyFlag
27
 *   } = useNodeSelection(initialNotes, firstNoteId);
28
 *
29
 *   return (
30
 *     <div>
31
 *       <NotesList notes={notes} onSelect={updateSelection} />
32
 *       <NoteEditor
33
 *         noteId={selectedNoteId}
34
 *         onChange={(content) => updateNoteContent(selectedNoteId, content)}
35
 *       />
36
 *     </div>
37
 *   );
38
 * }
39
 * ```
40
 */
41

42
import { useState, useCallback } from 'react';
43
import type { NoteTreeNode } from '@/types/tree';
44
import type { SaveStatus } from '@/types/notes';
45
import { selectNode } from '@/lib/utils';
46

47
/**
48
 * Custom hook for managing note selection and state in the note tree.
49
 * Provides state and functions for note selection, content updates, and dirty tracking.
50
 *
51
 * @param {NoteTreeNode[]} [initialNotes=[]] - Initial array of note tree nodes
52
 * @param {number | null} [initialSelectedId=null] - Initially selected note ID
53
 *
54
 * @returns {{
55
 *   notes: NoteTreeNode[],
56
 *   selectedNoteId: number | null,
57
 *   saveStatus: SaveStatus,
58
 *   setNotes: (notes: NoteTreeNode[]) => void,
59
 *   setSaveStatus: (status: SaveStatus) => void,
60
 *   updateSelection: (noteId: number | null) => void,
61
 *   updateDirtyFlag: (noteId: number, dirty: boolean) => void,
62
 *   updateNoteContent: (noteId: number, content: string) => void,
63
 *   updateNoteName: (noteId: number, name: string) => void
64
 * }} Hook state and updater functions
65
 *
66
 * @example
67
 * ```tsx
68
 * const {
69
 *   notes,
70
 *   selectedNoteId,
71
 *   updateSelection,
72
 *   updateNoteContent
73
 * } = useNodeSelection(initialNotes, 1);
74
 *
75
 * // Select a different note
76
 * updateSelection(2);
77
 *
78
 * // Update note content (marks as dirty)
79
 * updateNoteContent(2, '# New Content');
80
 * ```
81
 *
82
 * @remarks
83
 * - updateSelection ensures only one note is selected at a time
84
 * - updateNoteContent automatically sets dirty flag to true
85
 * - All updater functions use useCallback for performance
86
 * - State updates are immutable (creates new arrays/objects)
87
 * - saveStatus is independent of note state (managed separately)
88
 */
89
export function useNodeSelection(
90
  initialNotes: NoteTreeNode[] = [],
×
91
  initialSelectedId: number | null = null
×
92
) {
93
  const [notes, setNotes] = useState<NoteTreeNode[]>(initialNotes);
3,028✔
94
  const [selectedNoteId, setSelectedNoteId] = useState<number | null>(initialSelectedId);
3,028✔
95
  const [saveStatus, setSaveStatus] = useState<SaveStatus>('idle');
3,028✔
96

97
  const updateSelection = useCallback((newSelectedId: number | null) => {
3,028✔
98
    setNotes((prevNotes) => {
×
99
      return selectNode(prevNotes, newSelectedId);
250✔
100
    });
101

102
    setSelectedNoteId(newSelectedId);
107✔
103
  }, []);
104

105
  const updateDirtyFlag = useCallback((noteId: number, dirty: boolean) => {
3,028✔
106
    setNotes((prevNotes) =>
×
107
      prevNotes.map((node) => {
×
108
        if (node.id === noteId) {
106✔
109
          return {
10✔
110
            ...node,
111
            data: { ...node.data!, dirty }
112
          };
113
        }
114
        return node;
96✔
115
      })
116
    );
117
  }, []);
118

119
  const updateNoteContent = useCallback((noteId: number, content: string) => {
3,028✔
120
    setNotes((prevNotes) =>
×
121
      prevNotes.map((node) => {
×
122
        if (node.id === noteId) {
24,508✔
123
          return {
1,976✔
124
            ...node,
125
            data: { ...node.data!, content, dirty: true }
126
          };
127
        }
128
        return node;
22,532✔
129
      })
130
    );
131
  }, []);
132

133
  const updateNoteName = useCallback((noteId: number, name: string) => {
3,028✔
134
    setNotes((prevNotes) =>
×
135
      prevNotes.map((node) => {
×
136
        if (node.id === noteId) {
38✔
137
          return { ...node, name };
6✔
138
        }
139
        return node;
32✔
140
      })
141
    );
142
  }, []);
143

144
  const updateNoteTimestamp = useCallback((noteId: number, updatedAt: Date) => {
3,028✔
UNCOV
145
    setNotes((prevNotes) =>
×
UNCOV
146
      prevNotes.map((node) => {
×
147
        if (node.id === noteId) {
106✔
148
          return {
10✔
149
            ...node,
150
            data: { ...node.data!, updatedAt }
151
          };
152
        }
153
        return node;
96✔
154
      })
155
    );
156
  }, []);
157

158
  return {
3,028✔
159
    notes,
160
    selectedNoteId,
161
    saveStatus,
162
    setNotes,
163
    setSaveStatus,
164
    updateSelection,
165
    updateDirtyFlag,
166
    updateNoteContent,
167
    updateNoteName,
168
    updateNoteTimestamp
169
  };
170
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc