2026-04-08 14:01:19 +02:00
|
|
|
import { BulkActionBar } from '../BulkActionBar'
|
|
|
|
|
import { FilterPills } from './FilterPills'
|
|
|
|
|
import { NoteListHeader } from './NoteListHeader'
|
|
|
|
|
import { EntityView, ListView } from './NoteListViews'
|
|
|
|
|
import type { useNoteListModel } from './useNoteListModel'
|
|
|
|
|
|
2026-04-16 00:38:43 +02:00
|
|
|
type NoteListLayoutProps = ReturnType<typeof useNoteListModel> & {
|
|
|
|
|
handleBulkOrganize?: () => void
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-29 22:52:19 +02:00
|
|
|
const NOTE_LIST_LOADING_ROWS = [
|
|
|
|
|
{ title: 184, line: 254, selected: false },
|
|
|
|
|
{ title: 142, line: 220, selected: true },
|
|
|
|
|
{ title: 98, line: 242, selected: false },
|
|
|
|
|
{ title: 212, line: 198, selected: false },
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
function NoteListLoadingBar({ width }: { width: number }) {
|
|
|
|
|
return <span aria-hidden="true" className="block h-4 rounded bg-muted" style={{ width }} />
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function NoteListLoadingRow({
|
|
|
|
|
title,
|
|
|
|
|
line,
|
|
|
|
|
selected,
|
|
|
|
|
}: {
|
|
|
|
|
title: number
|
|
|
|
|
line: number
|
|
|
|
|
selected: boolean
|
|
|
|
|
}) {
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
className="border-b border-border"
|
|
|
|
|
style={{ padding: '12px 12px 10px', background: selected ? 'var(--accent-green-light)' : undefined }}
|
|
|
|
|
>
|
|
|
|
|
<div className="mb-3 flex items-start justify-between gap-3">
|
|
|
|
|
<NoteListLoadingBar width={title} />
|
|
|
|
|
<span aria-hidden="true" className="h-4 w-4 shrink-0 rounded bg-muted" />
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex flex-col gap-2">
|
|
|
|
|
<NoteListLoadingBar width={line} />
|
|
|
|
|
<NoteListLoadingBar width={Math.round(line * 0.72)} />
|
|
|
|
|
</div>
|
|
|
|
|
<div className="mt-3 flex items-center justify-between">
|
|
|
|
|
<NoteListLoadingBar width={44} />
|
|
|
|
|
<NoteListLoadingBar width={82} />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function NoteListLoadingSkeleton() {
|
|
|
|
|
return (
|
|
|
|
|
<div data-testid="note-list-loading-skeleton" className="animate-pulse">
|
|
|
|
|
{NOTE_LIST_LOADING_ROWS.map((row, index) => (
|
|
|
|
|
<NoteListLoadingRow key={index} {...row} />
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-16 00:38:43 +02:00
|
|
|
function MultiSelectBar({
|
|
|
|
|
multiSelect,
|
|
|
|
|
isArchivedView,
|
|
|
|
|
handleBulkOrganize,
|
|
|
|
|
handleBulkArchive,
|
|
|
|
|
handleBulkDeletePermanently,
|
|
|
|
|
handleBulkUnarchive,
|
|
|
|
|
}: Pick<NoteListLayoutProps, 'multiSelect' | 'isArchivedView' | 'handleBulkOrganize' | 'handleBulkArchive' | 'handleBulkDeletePermanently' | 'handleBulkUnarchive'>) {
|
|
|
|
|
if (!multiSelect.isMultiSelecting) return null
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<BulkActionBar
|
|
|
|
|
count={multiSelect.selectedPaths.size}
|
|
|
|
|
isArchivedView={isArchivedView}
|
|
|
|
|
onOrganize={handleBulkOrganize}
|
|
|
|
|
onArchive={handleBulkArchive}
|
|
|
|
|
onDelete={handleBulkDeletePermanently}
|
|
|
|
|
onUnarchive={handleBulkUnarchive}
|
|
|
|
|
onClear={multiSelect.clear}
|
|
|
|
|
/>
|
|
|
|
|
)
|
|
|
|
|
}
|
2026-04-08 14:01:19 +02:00
|
|
|
|
2026-04-16 09:04:13 +02:00
|
|
|
function NoteListContent({
|
|
|
|
|
entitySelection,
|
|
|
|
|
searchedGroups,
|
|
|
|
|
query,
|
|
|
|
|
collapsedGroups,
|
|
|
|
|
sortPrefs,
|
|
|
|
|
toggleGroup,
|
|
|
|
|
handleSortChange,
|
|
|
|
|
renderItem,
|
|
|
|
|
isArchivedView,
|
|
|
|
|
isChangesView,
|
|
|
|
|
isInboxView,
|
|
|
|
|
modifiedFilesError,
|
|
|
|
|
searched,
|
|
|
|
|
noteListVirtuosoRef,
|
2026-04-26 08:18:47 +02:00
|
|
|
locale,
|
2026-04-29 22:52:19 +02:00
|
|
|
loading,
|
2026-04-16 09:04:13 +02:00
|
|
|
}: Pick<
|
|
|
|
|
NoteListLayoutProps,
|
|
|
|
|
| 'entitySelection'
|
|
|
|
|
| 'searchedGroups'
|
|
|
|
|
| 'query'
|
|
|
|
|
| 'collapsedGroups'
|
|
|
|
|
| 'sortPrefs'
|
|
|
|
|
| 'toggleGroup'
|
|
|
|
|
| 'handleSortChange'
|
|
|
|
|
| 'renderItem'
|
|
|
|
|
| 'isArchivedView'
|
|
|
|
|
| 'isChangesView'
|
|
|
|
|
| 'isInboxView'
|
|
|
|
|
| 'modifiedFilesError'
|
|
|
|
|
| 'searched'
|
|
|
|
|
| 'noteListVirtuosoRef'
|
2026-04-26 08:18:47 +02:00
|
|
|
| 'locale'
|
2026-04-29 22:52:19 +02:00
|
|
|
| 'loading'
|
2026-04-16 09:04:13 +02:00
|
|
|
>) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex-1 overflow-hidden" style={{ minHeight: 0 }}>
|
2026-04-29 22:52:19 +02:00
|
|
|
{loading ? (
|
|
|
|
|
<NoteListLoadingSkeleton />
|
|
|
|
|
) : entitySelection ? (
|
2026-04-16 09:04:13 +02:00
|
|
|
<EntityView
|
|
|
|
|
entity={entitySelection.entry}
|
|
|
|
|
groups={searchedGroups}
|
|
|
|
|
query={query}
|
|
|
|
|
collapsedGroups={collapsedGroups}
|
|
|
|
|
sortPrefs={sortPrefs}
|
|
|
|
|
onToggleGroup={toggleGroup}
|
|
|
|
|
onSortChange={handleSortChange}
|
|
|
|
|
renderItem={renderItem}
|
2026-04-26 08:18:47 +02:00
|
|
|
locale={locale}
|
2026-04-16 09:04:13 +02:00
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<ListView
|
|
|
|
|
isArchivedView={isArchivedView}
|
|
|
|
|
isChangesView={isChangesView}
|
|
|
|
|
isInboxView={isInboxView}
|
|
|
|
|
changesError={modifiedFilesError}
|
|
|
|
|
searched={searched}
|
|
|
|
|
query={query}
|
|
|
|
|
renderItem={renderItem}
|
|
|
|
|
virtuosoRef={noteListVirtuosoRef}
|
2026-04-26 08:18:47 +02:00
|
|
|
locale={locale}
|
2026-04-16 09:04:13 +02:00
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function NoteListBody({
|
|
|
|
|
handleListKeyDown,
|
|
|
|
|
noteListContainerRef,
|
|
|
|
|
handleNoteListBlur,
|
|
|
|
|
handleNoteListFocus,
|
|
|
|
|
focusNoteList,
|
|
|
|
|
noteListVirtuosoRef,
|
|
|
|
|
entitySelection,
|
|
|
|
|
searchedGroups,
|
|
|
|
|
query,
|
|
|
|
|
collapsedGroups,
|
|
|
|
|
sortPrefs,
|
|
|
|
|
toggleGroup,
|
|
|
|
|
handleSortChange,
|
|
|
|
|
renderItem,
|
|
|
|
|
isArchivedView,
|
|
|
|
|
isChangesView,
|
|
|
|
|
isInboxView,
|
|
|
|
|
modifiedFilesError,
|
|
|
|
|
searched,
|
2026-04-26 08:18:47 +02:00
|
|
|
locale,
|
2026-04-16 09:04:13 +02:00
|
|
|
showFilterPills,
|
|
|
|
|
noteListFilter,
|
|
|
|
|
filterCounts,
|
|
|
|
|
onNoteListFilterChange,
|
2026-04-29 22:52:19 +02:00
|
|
|
loading,
|
2026-04-16 09:04:13 +02:00
|
|
|
}: Pick<
|
|
|
|
|
NoteListLayoutProps,
|
|
|
|
|
| 'handleListKeyDown'
|
|
|
|
|
| 'noteListContainerRef'
|
|
|
|
|
| 'handleNoteListBlur'
|
|
|
|
|
| 'handleNoteListFocus'
|
|
|
|
|
| 'focusNoteList'
|
|
|
|
|
| 'noteListVirtuosoRef'
|
|
|
|
|
| 'entitySelection'
|
|
|
|
|
| 'searchedGroups'
|
|
|
|
|
| 'query'
|
|
|
|
|
| 'collapsedGroups'
|
|
|
|
|
| 'sortPrefs'
|
|
|
|
|
| 'toggleGroup'
|
|
|
|
|
| 'handleSortChange'
|
|
|
|
|
| 'renderItem'
|
|
|
|
|
| 'isArchivedView'
|
|
|
|
|
| 'isChangesView'
|
|
|
|
|
| 'isInboxView'
|
|
|
|
|
| 'modifiedFilesError'
|
|
|
|
|
| 'searched'
|
2026-04-26 08:18:47 +02:00
|
|
|
| 'locale'
|
2026-04-16 09:04:13 +02:00
|
|
|
| 'showFilterPills'
|
|
|
|
|
| 'noteListFilter'
|
|
|
|
|
| 'filterCounts'
|
|
|
|
|
| 'onNoteListFilterChange'
|
2026-04-29 22:52:19 +02:00
|
|
|
| 'loading'
|
2026-04-16 09:04:13 +02:00
|
|
|
>) {
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
ref={noteListContainerRef}
|
|
|
|
|
className="relative flex flex-1 flex-col overflow-hidden outline-none"
|
|
|
|
|
style={{ minHeight: 0 }}
|
|
|
|
|
tabIndex={0}
|
|
|
|
|
onBlur={handleNoteListBlur}
|
|
|
|
|
onKeyDown={handleListKeyDown}
|
|
|
|
|
onFocus={handleNoteListFocus}
|
|
|
|
|
onClickCapture={focusNoteList}
|
|
|
|
|
data-testid="note-list-container"
|
|
|
|
|
>
|
|
|
|
|
<NoteListContent
|
|
|
|
|
entitySelection={entitySelection}
|
|
|
|
|
searchedGroups={searchedGroups}
|
|
|
|
|
query={query}
|
|
|
|
|
collapsedGroups={collapsedGroups}
|
|
|
|
|
sortPrefs={sortPrefs}
|
|
|
|
|
toggleGroup={toggleGroup}
|
|
|
|
|
handleSortChange={handleSortChange}
|
|
|
|
|
renderItem={renderItem}
|
|
|
|
|
isArchivedView={isArchivedView}
|
|
|
|
|
isChangesView={isChangesView}
|
|
|
|
|
isInboxView={isInboxView}
|
|
|
|
|
modifiedFilesError={modifiedFilesError}
|
|
|
|
|
searched={searched}
|
|
|
|
|
noteListVirtuosoRef={noteListVirtuosoRef}
|
2026-04-26 08:18:47 +02:00
|
|
|
locale={locale}
|
2026-04-29 22:52:19 +02:00
|
|
|
loading={loading}
|
2026-04-16 09:04:13 +02:00
|
|
|
/>
|
|
|
|
|
{showFilterPills && (
|
|
|
|
|
<FilterPills
|
|
|
|
|
active={noteListFilter}
|
|
|
|
|
counts={filterCounts}
|
|
|
|
|
onChange={onNoteListFilterChange}
|
|
|
|
|
position="bottom"
|
2026-04-26 19:37:21 +02:00
|
|
|
locale={locale}
|
2026-04-16 09:04:13 +02:00
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-26 08:18:47 +02:00
|
|
|
function NoteListLayoutHeader({
|
2026-04-08 14:01:19 +02:00
|
|
|
title,
|
|
|
|
|
typeDocument,
|
|
|
|
|
isEntityView,
|
2026-05-11 16:37:06 +02:00
|
|
|
isChangesView,
|
2026-04-08 14:01:19 +02:00
|
|
|
listSort,
|
|
|
|
|
listDirection,
|
|
|
|
|
customProperties,
|
2026-05-11 16:37:06 +02:00
|
|
|
gitRepositories,
|
|
|
|
|
selectedGitRepositoryPath,
|
|
|
|
|
onGitRepositoryChange,
|
2026-04-26 08:18:47 +02:00
|
|
|
locale,
|
2026-04-08 14:01:19 +02:00
|
|
|
sidebarCollapsed,
|
|
|
|
|
searchVisible,
|
|
|
|
|
search,
|
2026-04-21 00:47:45 +02:00
|
|
|
isSearching,
|
|
|
|
|
searchInputRef,
|
2026-04-08 14:01:19 +02:00
|
|
|
propertyPicker,
|
|
|
|
|
handleSortChange,
|
|
|
|
|
handleCreateNote,
|
|
|
|
|
onOpenType,
|
|
|
|
|
toggleSearch,
|
|
|
|
|
setSearch,
|
2026-04-21 00:47:45 +02:00
|
|
|
handleSearchKeyDown,
|
2026-04-26 08:18:47 +02:00
|
|
|
}: Pick<
|
|
|
|
|
NoteListLayoutProps,
|
|
|
|
|
| 'title'
|
|
|
|
|
| 'typeDocument'
|
|
|
|
|
| 'isEntityView'
|
2026-05-11 16:37:06 +02:00
|
|
|
| 'isChangesView'
|
2026-04-26 08:18:47 +02:00
|
|
|
| 'listSort'
|
|
|
|
|
| 'listDirection'
|
|
|
|
|
| 'customProperties'
|
2026-05-11 16:37:06 +02:00
|
|
|
| 'gitRepositories'
|
|
|
|
|
| 'selectedGitRepositoryPath'
|
|
|
|
|
| 'onGitRepositoryChange'
|
2026-04-26 08:18:47 +02:00
|
|
|
| 'locale'
|
|
|
|
|
| 'sidebarCollapsed'
|
|
|
|
|
| 'searchVisible'
|
|
|
|
|
| 'search'
|
|
|
|
|
| 'isSearching'
|
|
|
|
|
| 'searchInputRef'
|
|
|
|
|
| 'propertyPicker'
|
|
|
|
|
| 'handleSortChange'
|
|
|
|
|
| 'handleCreateNote'
|
|
|
|
|
| 'onOpenType'
|
|
|
|
|
| 'toggleSearch'
|
|
|
|
|
| 'setSearch'
|
|
|
|
|
| 'handleSearchKeyDown'
|
|
|
|
|
>) {
|
|
|
|
|
return (
|
|
|
|
|
<NoteListHeader
|
|
|
|
|
title={title}
|
|
|
|
|
typeDocument={typeDocument}
|
|
|
|
|
isEntityView={isEntityView}
|
2026-05-11 16:37:06 +02:00
|
|
|
isChangesView={isChangesView}
|
2026-04-26 08:18:47 +02:00
|
|
|
listSort={listSort}
|
|
|
|
|
listDirection={listDirection}
|
|
|
|
|
customProperties={customProperties}
|
2026-05-11 16:37:06 +02:00
|
|
|
gitRepositories={gitRepositories}
|
|
|
|
|
selectedGitRepositoryPath={selectedGitRepositoryPath}
|
|
|
|
|
onGitRepositoryChange={onGitRepositoryChange}
|
2026-04-26 08:18:47 +02:00
|
|
|
locale={locale}
|
|
|
|
|
sidebarCollapsed={sidebarCollapsed}
|
|
|
|
|
searchVisible={searchVisible}
|
|
|
|
|
search={search}
|
|
|
|
|
isSearching={isSearching}
|
|
|
|
|
searchInputRef={searchInputRef}
|
|
|
|
|
propertyPicker={propertyPicker}
|
|
|
|
|
onSortChange={handleSortChange}
|
|
|
|
|
onCreateNote={handleCreateNote}
|
|
|
|
|
onOpenType={onOpenType}
|
|
|
|
|
onToggleSearch={toggleSearch}
|
|
|
|
|
onSearchChange={setSearch}
|
|
|
|
|
onSearchKeyDown={handleSearchKeyDown}
|
|
|
|
|
/>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function NoteListFooter({
|
2026-04-08 14:01:19 +02:00
|
|
|
multiSelect,
|
2026-04-26 08:18:47 +02:00
|
|
|
isArchivedView,
|
2026-04-16 00:38:43 +02:00
|
|
|
handleBulkOrganize,
|
2026-04-08 14:01:19 +02:00
|
|
|
handleBulkArchive,
|
|
|
|
|
handleBulkDeletePermanently,
|
|
|
|
|
handleBulkUnarchive,
|
|
|
|
|
contextMenuNode,
|
|
|
|
|
dialogNode,
|
2026-04-26 08:18:47 +02:00
|
|
|
}: Pick<
|
|
|
|
|
NoteListLayoutProps,
|
|
|
|
|
| 'multiSelect'
|
|
|
|
|
| 'isArchivedView'
|
|
|
|
|
| 'handleBulkOrganize'
|
|
|
|
|
| 'handleBulkArchive'
|
|
|
|
|
| 'handleBulkDeletePermanently'
|
|
|
|
|
| 'handleBulkUnarchive'
|
|
|
|
|
| 'contextMenuNode'
|
|
|
|
|
| 'dialogNode'
|
|
|
|
|
>) {
|
2026-04-08 14:01:19 +02:00
|
|
|
return (
|
2026-04-26 08:18:47 +02:00
|
|
|
<>
|
2026-04-16 00:38:43 +02:00
|
|
|
<MultiSelectBar
|
|
|
|
|
multiSelect={multiSelect}
|
|
|
|
|
isArchivedView={isArchivedView}
|
|
|
|
|
handleBulkOrganize={handleBulkOrganize}
|
|
|
|
|
handleBulkArchive={handleBulkArchive}
|
|
|
|
|
handleBulkDeletePermanently={handleBulkDeletePermanently}
|
|
|
|
|
handleBulkUnarchive={handleBulkUnarchive}
|
|
|
|
|
/>
|
2026-04-26 08:18:47 +02:00
|
|
|
{contextMenuNode}{dialogNode}
|
|
|
|
|
</>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function NoteListLayout({
|
|
|
|
|
noteListPanelRef,
|
|
|
|
|
handleNoteListPanelBlurCapture,
|
|
|
|
|
handleNoteListPanelFocusCapture,
|
|
|
|
|
...contentProps
|
|
|
|
|
}: NoteListLayoutProps) {
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
ref={noteListPanelRef}
|
|
|
|
|
className="flex flex-col select-none overflow-hidden border-r border-border bg-card text-foreground"
|
|
|
|
|
style={{ height: '100%' }}
|
|
|
|
|
onBlurCapture={handleNoteListPanelBlurCapture}
|
|
|
|
|
onFocusCapture={handleNoteListPanelFocusCapture}
|
|
|
|
|
>
|
|
|
|
|
<NoteListLayoutHeader {...contentProps} />
|
|
|
|
|
<NoteListBody {...contentProps} />
|
|
|
|
|
<NoteListFooter {...contentProps} />
|
2026-04-08 14:01:19 +02:00
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|