fix: keep vault loading shell visible
This commit is contained in:
@@ -517,10 +517,16 @@ describe('App', () => {
|
||||
|
||||
render(<App />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('vault-loading-skeleton')).not.toBeInTheDocument()
|
||||
})
|
||||
expect(screen.getByText('Select a note to start editing')).toBeInTheDocument()
|
||||
expect(await screen.findByTestId('sidebar-loading-favorites', {}, { timeout: 5000 })).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('vault-loading-skeleton')).not.toBeInTheDocument()
|
||||
expect(screen.getByTestId('sidebar-top-nav')).toHaveTextContent('Inbox')
|
||||
expect(screen.getByTestId('sidebar-loading-views')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('sidebar-loading-types')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('sidebar-loading-folders')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('note-list-loading-skeleton')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('breadcrumb-title-skeleton')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('editor-content-skeleton')).toBeInTheDocument()
|
||||
expect(screen.queryByText('Select a note to start editing')).not.toBeInTheDocument()
|
||||
expect(screen.getByTestId('status-vault-reloading')).toHaveAccessibleName('Reloading vault from disk')
|
||||
await act(async () => {
|
||||
fireEvent.keyDown(window, { key: 'p', code: 'KeyP', metaKey: true })
|
||||
@@ -535,6 +541,9 @@ describe('App', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('vault-loading-skeleton')).not.toBeInTheDocument()
|
||||
expect(screen.queryByTestId('note-list-loading-skeleton')).not.toBeInTheDocument()
|
||||
expect(screen.queryByTestId('breadcrumb-title-skeleton')).not.toBeInTheDocument()
|
||||
expect(screen.queryByTestId('editor-content-skeleton')).not.toBeInTheDocument()
|
||||
expect(screen.queryByTestId('status-vault-reloading')).not.toBeInTheDocument()
|
||||
expect(screen.getAllByText('Test Project').length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
17
src/App.tsx
17
src/App.tsx
@@ -1609,17 +1609,7 @@ function App() {
|
||||
)
|
||||
}
|
||||
|
||||
// Show the full skeleton only while app-level vault capabilities are unknown.
|
||||
if (!noteWindowParams && onboarding.state.status === 'ready' && gitRepoState === 'checking') {
|
||||
return (
|
||||
<AppLoadingSkeleton
|
||||
noteListWidth={layout.noteListWidth}
|
||||
showNoteList={noteListVisible}
|
||||
showSidebar={sidebarVisible}
|
||||
sidebarWidth={layout.sidebarWidth}
|
||||
/>
|
||||
)
|
||||
}
|
||||
const isVaultContentLoading = !noteWindowParams && onboarding.state.status === 'ready' && vault.isLoading
|
||||
|
||||
return (
|
||||
<div className="app-shell">
|
||||
@@ -1627,7 +1617,7 @@ function App() {
|
||||
{sidebarVisible && (
|
||||
<>
|
||||
<div className="app__sidebar" style={{ width: layout.sidebarWidth }}>
|
||||
<Sidebar entries={vault.entries} folders={vault.folders} views={vault.views} selection={effectiveSelection} onSelect={handleSetSelection} onSelectNote={notes.handleSelectNote} onSelectFavorite={handleOpenFavorite} onReorderFavorites={entryActions.handleReorderFavorites} onCreateType={notes.handleCreateNoteImmediate} onCreateNewType={dialogs.openCreateType} onCustomizeType={entryActions.handleCustomizeType} onUpdateTypeTemplate={entryActions.handleUpdateTypeTemplate} onReorderSections={entryActions.handleReorderSections} onRenameSection={entryActions.handleRenameSection} onToggleTypeVisibility={entryActions.handleToggleTypeVisibility} onCreateFolder={handleCreateFolder} onRenameFolder={folderActions.renameFolder} onDeleteFolder={folderActions.requestDeleteFolder} folderFileActions={fileActions.folderActions} renamingFolderPath={folderActions.renamingFolderPath} onStartRenameFolder={folderActions.startFolderRename} onCancelRenameFolder={folderActions.cancelFolderRename} onCreateView={dialogs.openCreateView} onEditView={handleEditView} onDeleteView={handleDeleteView} onReorderViews={viewOrdering.onReorderViews} showInbox={explicitOrganizationEnabled} inboxCount={inboxCount} locale={appLocale} />
|
||||
<Sidebar entries={vault.entries} folders={vault.folders} views={vault.views} selection={effectiveSelection} onSelect={handleSetSelection} onSelectNote={notes.handleSelectNote} onSelectFavorite={handleOpenFavorite} onReorderFavorites={entryActions.handleReorderFavorites} onCreateType={notes.handleCreateNoteImmediate} onCreateNewType={dialogs.openCreateType} onCustomizeType={entryActions.handleCustomizeType} onUpdateTypeTemplate={entryActions.handleUpdateTypeTemplate} onReorderSections={entryActions.handleReorderSections} onRenameSection={entryActions.handleRenameSection} onToggleTypeVisibility={entryActions.handleToggleTypeVisibility} onCreateFolder={handleCreateFolder} onRenameFolder={folderActions.renameFolder} onDeleteFolder={folderActions.requestDeleteFolder} folderFileActions={fileActions.folderActions} renamingFolderPath={folderActions.renamingFolderPath} onStartRenameFolder={folderActions.startFolderRename} onCancelRenameFolder={folderActions.cancelFolderRename} onCreateView={dialogs.openCreateView} onEditView={handleEditView} onDeleteView={handleDeleteView} onReorderViews={viewOrdering.onReorderViews} showInbox={explicitOrganizationEnabled} inboxCount={inboxCount} locale={appLocale} loading={isVaultContentLoading} />
|
||||
</div>
|
||||
<ResizeHandle onResize={layout.handleSidebarResize} />
|
||||
</>
|
||||
@@ -1638,7 +1628,7 @@ function App() {
|
||||
{effectiveSelection.kind === 'filter' && effectiveSelection.filter === 'pulse' ? (
|
||||
<PulseView vaultPath={resolvedPath} onOpenNote={handlePulseOpenNote} sidebarCollapsed={!sidebarVisible} onExpandSidebar={() => handleSetViewMode('all')} locale={appLocale} />
|
||||
) : (
|
||||
<NoteList entries={vault.entries} selection={effectiveSelection} selectedNote={activeTab?.entry ?? null} noteListFilter={noteListFilter} onNoteListFilterChange={setNoteListFilter} inboxPeriod={inboxPeriod} modifiedFiles={vault.modifiedFiles} modifiedFilesError={vault.modifiedFilesError} getNoteStatus={vault.getNoteStatus} sidebarCollapsed={!sidebarVisible} onSelectNote={notes.handleSelectNote} onReplaceActiveTab={handleReplaceActiveTabWithQueuedDiff} onEnterNeighborhood={handleEnterNeighborhood} onCreateNote={notes.handleCreateNoteImmediate} onBulkOrganize={explicitOrganizationEnabled ? bulkActions.handleBulkOrganize : undefined} onBulkArchive={bulkActions.handleBulkArchive} onBulkDeletePermanently={deleteActions.handleBulkDeletePermanently} onUpdateTypeSort={notes.handleUpdateFrontmatter} onUpdateViewDefinition={handleUpdateViewDefinition} updateEntry={vault.updateEntry} onOpenInNewWindow={handleOpenEntryInNewWindow} onDiscardFile={handleDiscardFile} onOpenDeletedNote={handleOpenDeletedNote} allNotesNoteListProperties={vaultConfig.allNotes?.noteListProperties ?? null} onUpdateAllNotesNoteListProperties={handleUpdateAllNotesNoteListProperties} inboxNoteListProperties={vaultConfig.inbox?.noteListProperties ?? null} onUpdateInboxNoteListProperties={handleUpdateInboxNoteListProperties} views={vault.views} visibleNotesRef={visibleNotesRef} multiSelectionCommandRef={multiSelectionCommandRef} locale={appLocale} />
|
||||
<NoteList entries={vault.entries} selection={effectiveSelection} selectedNote={activeTab?.entry ?? null} loading={isVaultContentLoading} noteListFilter={noteListFilter} onNoteListFilterChange={setNoteListFilter} inboxPeriod={inboxPeriod} modifiedFiles={vault.modifiedFiles} modifiedFilesError={vault.modifiedFilesError} getNoteStatus={vault.getNoteStatus} sidebarCollapsed={!sidebarVisible} onSelectNote={notes.handleSelectNote} onReplaceActiveTab={handleReplaceActiveTabWithQueuedDiff} onEnterNeighborhood={handleEnterNeighborhood} onCreateNote={notes.handleCreateNoteImmediate} onBulkOrganize={explicitOrganizationEnabled ? bulkActions.handleBulkOrganize : undefined} onBulkArchive={bulkActions.handleBulkArchive} onBulkDeletePermanently={deleteActions.handleBulkDeletePermanently} onUpdateTypeSort={notes.handleUpdateFrontmatter} onUpdateViewDefinition={handleUpdateViewDefinition} updateEntry={vault.updateEntry} onOpenInNewWindow={handleOpenEntryInNewWindow} onDiscardFile={handleDiscardFile} onOpenDeletedNote={handleOpenDeletedNote} allNotesNoteListProperties={vaultConfig.allNotes?.noteListProperties ?? null} onUpdateAllNotesNoteListProperties={handleUpdateAllNotesNoteListProperties} inboxNoteListProperties={vaultConfig.inbox?.noteListProperties ?? null} onUpdateInboxNoteListProperties={handleUpdateInboxNoteListProperties} views={vault.views} visibleNotesRef={visibleNotesRef} multiSelectionCommandRef={multiSelectionCommandRef} locale={appLocale} />
|
||||
)}
|
||||
</div>
|
||||
<ResizeHandle onResize={layout.handleNoteListResize} />
|
||||
@@ -1648,6 +1638,7 @@ function App() {
|
||||
<Editor
|
||||
tabs={notes.tabs}
|
||||
activeTabPath={notes.activeTabPath}
|
||||
isVaultLoading={isVaultContentLoading}
|
||||
entries={vault.entries}
|
||||
onNavigateWikilink={notes.handleNavigateWikilink}
|
||||
onLoadDiff={vault.loadDiff}
|
||||
|
||||
@@ -55,6 +55,7 @@ interface BreadcrumbBarProps {
|
||||
/** Ref for direct DOM manipulation — avoids re-render on scroll. */
|
||||
barRef?: React.Ref<HTMLDivElement>
|
||||
locale?: AppLocale
|
||||
loadingTitle?: boolean
|
||||
}
|
||||
|
||||
const DISABLED_ICON_STYLE = { opacity: 0.4, cursor: 'not-allowed' } as const
|
||||
@@ -615,6 +616,16 @@ function FilenameCrumb({ entry, locale = 'en', onRenameFilename }: Pick<Breadcru
|
||||
)
|
||||
}
|
||||
|
||||
function BreadcrumbTitleSkeleton() {
|
||||
return (
|
||||
<span
|
||||
aria-hidden="true"
|
||||
data-testid="breadcrumb-title-skeleton"
|
||||
className="h-4 w-36 animate-pulse rounded bg-muted"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function BreadcrumbActions({
|
||||
entry,
|
||||
showDiffToggle,
|
||||
@@ -664,15 +675,18 @@ function BreadcrumbActions({
|
||||
function BreadcrumbTitle({
|
||||
entry,
|
||||
locale,
|
||||
loadingTitle,
|
||||
onRenameFilename,
|
||||
}: Pick<BreadcrumbBarProps, 'entry' | 'locale' | 'onRenameFilename'>) {
|
||||
}: Pick<BreadcrumbBarProps, 'entry' | 'locale' | 'loadingTitle' | 'onRenameFilename'>) {
|
||||
const typeLabel = entry.isA ?? 'Note'
|
||||
return (
|
||||
<div className="flex items-center gap-1.5 min-w-0 text-sm text-muted-foreground">
|
||||
<span className="shrink-0">{typeLabel}</span>
|
||||
<span className="shrink-0 text-border">›</span>
|
||||
<div className="flex min-w-0 items-center gap-1 truncate">
|
||||
<FilenameCrumb entry={entry} locale={locale} onRenameFilename={onRenameFilename} />
|
||||
{loadingTitle
|
||||
? <BreadcrumbTitleSkeleton />
|
||||
: <FilenameCrumb entry={entry} locale={locale} onRenameFilename={onRenameFilename} />}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -682,6 +696,7 @@ export const BreadcrumbBar = memo(function BreadcrumbBar({
|
||||
entry,
|
||||
barRef,
|
||||
locale = 'en',
|
||||
loadingTitle = false,
|
||||
onRenameFilename,
|
||||
...actionProps
|
||||
}: BreadcrumbBarProps) {
|
||||
@@ -703,7 +718,12 @@ export const BreadcrumbBar = memo(function BreadcrumbBar({
|
||||
}}
|
||||
>
|
||||
<div className="breadcrumb-bar__title min-w-0">
|
||||
<BreadcrumbTitle entry={entry} locale={locale} onRenameFilename={onRenameFilename} />
|
||||
<BreadcrumbTitle
|
||||
entry={entry}
|
||||
locale={locale}
|
||||
loadingTitle={loadingTitle}
|
||||
onRenameFilename={onRenameFilename}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
|
||||
@@ -461,6 +461,26 @@ describe('Editor', () => {
|
||||
expect(screen.getByRole('button', { name: 'Delete this note' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('keeps editor chrome visible while active note content is loading', () => {
|
||||
renderEditor({
|
||||
tabs: [],
|
||||
activeTabPath: mockEntry.path,
|
||||
entries: [mockEntry],
|
||||
inspectorCollapsed: false,
|
||||
inspectorEntry: mockEntry,
|
||||
inspectorContent: mockContent,
|
||||
})
|
||||
|
||||
expect(screen.getByTestId('breadcrumb-filename-trigger')).toHaveTextContent('test')
|
||||
expect(screen.getAllByText('Properties').length).toBeGreaterThan(0)
|
||||
expect(screen.queryByText('Select a note to start editing')).not.toBeInTheDocument()
|
||||
expect(screen.queryByTestId('blocknote-view')).not.toBeInTheDocument()
|
||||
|
||||
const skeleton = screen.getByTestId('editor-content-skeleton')
|
||||
expect(skeleton.closest('.editor-content-wrapper')).not.toBeNull()
|
||||
expect(skeleton.closest('.editor-scroll-area')).not.toBeNull()
|
||||
})
|
||||
|
||||
it('hides the legacy title field for untitled draft notes', () => {
|
||||
const draftEntry: VaultEntry = {
|
||||
...mockEntry,
|
||||
|
||||
@@ -41,6 +41,7 @@ interface Tab {
|
||||
interface EditorProps {
|
||||
tabs: Tab[]
|
||||
activeTabPath: string | null
|
||||
isVaultLoading?: boolean
|
||||
entries: VaultEntry[]
|
||||
onNavigateWikilink: (target: string) => void
|
||||
onUnsupportedAiPaste?: (message: string) => void
|
||||
@@ -291,8 +292,10 @@ function useEditorFindCommand({
|
||||
|
||||
function EditorLayout({
|
||||
tabs,
|
||||
activeTabPath,
|
||||
activeTab,
|
||||
isLoadingNewTab,
|
||||
isVaultLoading,
|
||||
entries,
|
||||
editor,
|
||||
diffMode,
|
||||
@@ -353,8 +356,10 @@ function EditorLayout({
|
||||
locale,
|
||||
}: {
|
||||
tabs: Tab[]
|
||||
activeTabPath: string | null
|
||||
activeTab: Tab | null
|
||||
isLoadingNewTab: boolean
|
||||
isVaultLoading?: boolean
|
||||
entries: VaultEntry[]
|
||||
editor: ReturnType<typeof useCreateBlockNote>
|
||||
diffMode: boolean
|
||||
@@ -415,11 +420,12 @@ function EditorLayout({
|
||||
locale?: AppLocale
|
||||
}) {
|
||||
const activeBinaryTab = activeTab?.entry.fileKind === 'binary' ? activeTab : null
|
||||
const showEmptyState = tabs.length === 0 && activeTabPath === null && !isVaultLoading
|
||||
|
||||
return (
|
||||
<div className="editor flex flex-col min-h-0 overflow-hidden bg-background text-foreground">
|
||||
<div className="flex flex-1 min-h-0">
|
||||
{tabs.length === 0
|
||||
{showEmptyState
|
||||
? <EditorEmptyState locale={locale} />
|
||||
: activeBinaryTab
|
||||
? (
|
||||
@@ -432,7 +438,9 @@ function EditorLayout({
|
||||
)
|
||||
: <EditorContent
|
||||
activeTab={activeTab}
|
||||
activeTabPath={activeTabPath}
|
||||
isLoadingNewTab={isLoadingNewTab}
|
||||
isVaultLoading={isVaultLoading}
|
||||
entries={entries}
|
||||
editor={editor}
|
||||
diffMode={diffMode}
|
||||
@@ -512,6 +520,7 @@ function EditorLayout({
|
||||
export const Editor = memo(function Editor(props: EditorProps) {
|
||||
const {
|
||||
tabs, activeTabPath, entries, onNavigateWikilink,
|
||||
isVaultLoading,
|
||||
getNoteStatus,
|
||||
inspectorCollapsed, onToggleInspector, inspectorWidth,
|
||||
defaultAiAgent = DEFAULT_AI_AGENT, defaultAiAgentReadiness, defaultAiAgentReady = true,
|
||||
@@ -527,8 +536,7 @@ export const Editor = memo(function Editor(props: EditorProps) {
|
||||
noteWidth, onToggleNoteWidth,
|
||||
onFileCreated, onFileModified, onVaultChanged,
|
||||
isConflicted, onKeepMine, onKeepTheirs,
|
||||
flushPendingEditorContentRef, flushPendingRawContentRef, findInNoteRef,
|
||||
locale,
|
||||
flushPendingEditorContentRef, flushPendingRawContentRef, findInNoteRef, locale,
|
||||
} = props
|
||||
|
||||
const {
|
||||
@@ -566,8 +574,10 @@ export const Editor = memo(function Editor(props: EditorProps) {
|
||||
return (
|
||||
<EditorLayout
|
||||
tabs={tabs}
|
||||
activeTabPath={props.activeTabPath}
|
||||
activeTab={activeTab}
|
||||
isLoadingNewTab={isLoadingNewTab}
|
||||
isVaultLoading={isVaultLoading}
|
||||
entries={entries}
|
||||
editor={editor}
|
||||
diffMode={diffMode}
|
||||
|
||||
@@ -21,6 +21,11 @@ import {
|
||||
TypesSection,
|
||||
ViewsSection,
|
||||
} from './sidebar/SidebarSections'
|
||||
import {
|
||||
SidebarCreatableLoadingSection,
|
||||
SidebarFavoritesLoadingSection,
|
||||
SidebarTypesLoadingSection,
|
||||
} from './sidebar/SidebarLoadingSections'
|
||||
import { useSidebarTypeInteractions } from './sidebar/useSidebarTypeInteractions'
|
||||
import type { AppLocale } from '../lib/i18n'
|
||||
import type { FolderFileActions } from '../hooks/useFileActions'
|
||||
@@ -56,6 +61,7 @@ interface SidebarProps {
|
||||
inboxCount?: number
|
||||
locale?: AppLocale
|
||||
onCollapse?: () => void
|
||||
loading?: boolean
|
||||
}
|
||||
|
||||
interface SidebarNavigationProps extends Pick<
|
||||
@@ -82,6 +88,7 @@ interface SidebarNavigationProps extends Pick<
|
||||
| 'inboxCount'
|
||||
| 'onCreateNewType'
|
||||
| 'locale'
|
||||
| 'loading'
|
||||
> {
|
||||
activeCount: number
|
||||
archivedCount: number
|
||||
@@ -98,6 +105,248 @@ interface SidebarNavigationProps extends Pick<
|
||||
toggleVisibility: (type: string) => void
|
||||
}
|
||||
|
||||
type SidebarFavoritesNavigationProps = Pick<
|
||||
SidebarNavigationProps,
|
||||
| 'loading'
|
||||
| 'entries'
|
||||
| 'selection'
|
||||
| 'onSelect'
|
||||
| 'onSelectFavorite'
|
||||
| 'onReorderFavorites'
|
||||
| 'groupCollapsed'
|
||||
| 'toggleGroup'
|
||||
| 'locale'
|
||||
>
|
||||
|
||||
type SidebarViewsNavigationProps = Pick<
|
||||
SidebarNavigationProps,
|
||||
| 'loading'
|
||||
| 'views'
|
||||
| 'selection'
|
||||
| 'onSelect'
|
||||
| 'onCreateView'
|
||||
| 'onEditView'
|
||||
| 'onDeleteView'
|
||||
| 'onReorderViews'
|
||||
| 'groupCollapsed'
|
||||
| 'toggleGroup'
|
||||
| 'sensors'
|
||||
| 'entries'
|
||||
| 'locale'
|
||||
>
|
||||
|
||||
type SidebarTypesNavigationProps = Pick<
|
||||
SidebarNavigationProps,
|
||||
| 'loading'
|
||||
| 'visibleSections'
|
||||
| 'allSectionGroups'
|
||||
| 'sectionIds'
|
||||
| 'sensors'
|
||||
| 'handleDragEnd'
|
||||
| 'sectionProps'
|
||||
| 'groupCollapsed'
|
||||
| 'toggleGroup'
|
||||
| 'typeInteractions'
|
||||
| 'isSectionVisible'
|
||||
| 'toggleVisibility'
|
||||
| 'onCreateNewType'
|
||||
| 'locale'
|
||||
>
|
||||
|
||||
type SidebarFoldersNavigationProps = Pick<
|
||||
SidebarNavigationProps,
|
||||
| 'loading'
|
||||
| 'folders'
|
||||
| 'selection'
|
||||
| 'onSelect'
|
||||
| 'onCreateFolder'
|
||||
| 'onRenameFolder'
|
||||
| 'onDeleteFolder'
|
||||
| 'folderFileActions'
|
||||
| 'renamingFolderPath'
|
||||
| 'onStartRenameFolder'
|
||||
| 'onCancelRenameFolder'
|
||||
| 'groupCollapsed'
|
||||
| 'toggleGroup'
|
||||
| 'locale'
|
||||
>
|
||||
|
||||
function SidebarFavoritesNavigation({
|
||||
loading,
|
||||
entries,
|
||||
selection,
|
||||
onSelect,
|
||||
onSelectFavorite,
|
||||
onReorderFavorites,
|
||||
groupCollapsed,
|
||||
toggleGroup,
|
||||
locale,
|
||||
}: SidebarFavoritesNavigationProps) {
|
||||
if (loading) {
|
||||
return (
|
||||
<SidebarFavoritesLoadingSection
|
||||
collapsed={groupCollapsed.favorites}
|
||||
locale={locale}
|
||||
onToggle={() => toggleGroup('favorites')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="border-b border-border">
|
||||
<FavoritesSection
|
||||
entries={entries}
|
||||
selection={selection}
|
||||
onSelect={onSelect}
|
||||
onSelectNote={onSelectFavorite}
|
||||
onReorder={onReorderFavorites}
|
||||
collapsed={groupCollapsed.favorites}
|
||||
locale={locale}
|
||||
onToggle={() => toggleGroup('favorites')}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarViewsNavigation({
|
||||
loading,
|
||||
views,
|
||||
selection,
|
||||
onSelect,
|
||||
onCreateView,
|
||||
onEditView,
|
||||
onDeleteView,
|
||||
onReorderViews,
|
||||
groupCollapsed,
|
||||
toggleGroup,
|
||||
sensors,
|
||||
entries,
|
||||
locale,
|
||||
}: SidebarViewsNavigationProps) {
|
||||
if (loading) {
|
||||
return (
|
||||
<SidebarCreatableLoadingSection
|
||||
collapsed={groupCollapsed.views}
|
||||
kind="views"
|
||||
locale={locale}
|
||||
onCreate={onCreateView}
|
||||
onToggle={() => toggleGroup('views')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ViewsSection
|
||||
views={views ?? []}
|
||||
selection={selection}
|
||||
onSelect={onSelect}
|
||||
collapsed={groupCollapsed.views}
|
||||
onToggle={() => toggleGroup('views')}
|
||||
onCreateView={onCreateView}
|
||||
onEditView={onEditView}
|
||||
onDeleteView={onDeleteView}
|
||||
onReorderViews={onReorderViews}
|
||||
sensors={sensors}
|
||||
entries={entries}
|
||||
locale={locale}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarTypesNavigation({
|
||||
loading,
|
||||
visibleSections,
|
||||
allSectionGroups,
|
||||
sectionIds,
|
||||
sensors,
|
||||
handleDragEnd,
|
||||
sectionProps,
|
||||
groupCollapsed,
|
||||
toggleGroup,
|
||||
typeInteractions,
|
||||
isSectionVisible,
|
||||
toggleVisibility,
|
||||
onCreateNewType,
|
||||
locale,
|
||||
}: SidebarTypesNavigationProps) {
|
||||
if (loading) {
|
||||
return (
|
||||
<SidebarTypesLoadingSection
|
||||
collapsed={groupCollapsed.sections}
|
||||
locale={locale}
|
||||
onCreateNewType={onCreateNewType}
|
||||
onToggle={() => toggleGroup('sections')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<TypesSection
|
||||
visibleSections={visibleSections}
|
||||
allSectionGroups={allSectionGroups}
|
||||
sectionIds={sectionIds}
|
||||
sensors={sensors}
|
||||
handleDragEnd={handleDragEnd}
|
||||
sectionProps={sectionProps}
|
||||
collapsed={groupCollapsed.sections}
|
||||
onToggle={() => toggleGroup('sections')}
|
||||
showCustomize={typeInteractions.showCustomize}
|
||||
setShowCustomize={typeInteractions.setShowCustomize}
|
||||
isSectionVisible={isSectionVisible}
|
||||
toggleVisibility={toggleVisibility}
|
||||
onCreateNewType={onCreateNewType}
|
||||
customizeRef={typeInteractions.customizeRef}
|
||||
locale={locale}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarFoldersNavigation({
|
||||
loading,
|
||||
folders,
|
||||
selection,
|
||||
onSelect,
|
||||
onCreateFolder,
|
||||
onRenameFolder,
|
||||
onDeleteFolder,
|
||||
folderFileActions,
|
||||
renamingFolderPath,
|
||||
onStartRenameFolder,
|
||||
onCancelRenameFolder,
|
||||
groupCollapsed,
|
||||
toggleGroup,
|
||||
locale,
|
||||
}: SidebarFoldersNavigationProps) {
|
||||
if (loading) {
|
||||
return (
|
||||
<SidebarCreatableLoadingSection
|
||||
collapsed={groupCollapsed.folders}
|
||||
kind="folders"
|
||||
locale={locale}
|
||||
onToggle={() => toggleGroup('folders')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<FolderTree
|
||||
folders={folders ?? []}
|
||||
selection={selection}
|
||||
onSelect={onSelect}
|
||||
onCreateFolder={onCreateFolder}
|
||||
onRenameFolder={onRenameFolder}
|
||||
onDeleteFolder={onDeleteFolder}
|
||||
folderFileActions={folderFileActions}
|
||||
renamingFolderPath={renamingFolderPath}
|
||||
onStartRenameFolder={onStartRenameFolder}
|
||||
onCancelRenameFolder={onCancelRenameFolder}
|
||||
collapsed={groupCollapsed.folders}
|
||||
locale={locale}
|
||||
onToggle={() => toggleGroup('folders')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarNavigation({
|
||||
entries,
|
||||
selection,
|
||||
@@ -120,6 +369,7 @@ function SidebarNavigation({
|
||||
showInbox = true,
|
||||
inboxCount = 0,
|
||||
locale = 'en',
|
||||
loading = false,
|
||||
onCreateNewType,
|
||||
activeCount,
|
||||
archivedCount,
|
||||
@@ -135,8 +385,8 @@ function SidebarNavigation({
|
||||
isSectionVisible,
|
||||
toggleVisibility,
|
||||
}: SidebarNavigationProps) {
|
||||
const hasFavorites = entries.some((entry) => entry.favorite && !entry.archived)
|
||||
const hasViews = views.length > 0 || !!onCreateView
|
||||
const hasFavorites = loading || entries.some((entry) => entry.favorite && !entry.archived)
|
||||
const hasViews = loading || views.length > 0 || !!onCreateView
|
||||
|
||||
return (
|
||||
<nav className="flex-1 overflow-y-auto">
|
||||
@@ -148,55 +398,56 @@ function SidebarNavigation({
|
||||
activeCount={activeCount}
|
||||
archivedCount={archivedCount}
|
||||
locale={locale}
|
||||
loading={loading}
|
||||
/>
|
||||
{hasFavorites && (
|
||||
<div className="border-b border-border">
|
||||
<FavoritesSection
|
||||
entries={entries}
|
||||
selection={selection}
|
||||
onSelect={onSelect}
|
||||
onSelectNote={onSelectFavorite}
|
||||
onReorder={onReorderFavorites}
|
||||
collapsed={groupCollapsed.favorites}
|
||||
locale={locale}
|
||||
onToggle={() => toggleGroup('favorites')}
|
||||
/>
|
||||
</div>
|
||||
<SidebarFavoritesNavigation
|
||||
loading={loading}
|
||||
entries={entries}
|
||||
selection={selection}
|
||||
onSelect={onSelect}
|
||||
onSelectFavorite={onSelectFavorite}
|
||||
onReorderFavorites={onReorderFavorites}
|
||||
groupCollapsed={groupCollapsed}
|
||||
toggleGroup={toggleGroup}
|
||||
locale={locale}
|
||||
/>
|
||||
)}
|
||||
{hasViews && (
|
||||
<ViewsSection
|
||||
<SidebarViewsNavigation
|
||||
loading={loading}
|
||||
views={views}
|
||||
selection={selection}
|
||||
onSelect={onSelect}
|
||||
collapsed={groupCollapsed.views}
|
||||
onToggle={() => toggleGroup('views')}
|
||||
onCreateView={onCreateView}
|
||||
onEditView={onEditView}
|
||||
onDeleteView={onDeleteView}
|
||||
onReorderViews={onReorderViews}
|
||||
groupCollapsed={groupCollapsed}
|
||||
toggleGroup={toggleGroup}
|
||||
sensors={sensors}
|
||||
entries={entries}
|
||||
locale={locale}
|
||||
/>
|
||||
)}
|
||||
<TypesSection
|
||||
<SidebarTypesNavigation
|
||||
loading={loading}
|
||||
visibleSections={visibleSections}
|
||||
allSectionGroups={allSectionGroups}
|
||||
sectionIds={sectionIds}
|
||||
sensors={sensors}
|
||||
handleDragEnd={handleDragEnd}
|
||||
sectionProps={sectionProps}
|
||||
collapsed={groupCollapsed.sections}
|
||||
onToggle={() => toggleGroup('sections')}
|
||||
showCustomize={typeInteractions.showCustomize}
|
||||
setShowCustomize={typeInteractions.setShowCustomize}
|
||||
groupCollapsed={groupCollapsed}
|
||||
toggleGroup={toggleGroup}
|
||||
typeInteractions={typeInteractions}
|
||||
isSectionVisible={isSectionVisible}
|
||||
toggleVisibility={toggleVisibility}
|
||||
onCreateNewType={onCreateNewType}
|
||||
customizeRef={typeInteractions.customizeRef}
|
||||
locale={locale}
|
||||
/>
|
||||
<FolderTree
|
||||
<SidebarFoldersNavigation
|
||||
loading={loading}
|
||||
folders={folders}
|
||||
selection={selection}
|
||||
onSelect={onSelect}
|
||||
@@ -207,9 +458,9 @@ function SidebarNavigation({
|
||||
renamingFolderPath={renamingFolderPath}
|
||||
onStartRenameFolder={onStartRenameFolder}
|
||||
onCancelRenameFolder={onCancelRenameFolder}
|
||||
collapsed={groupCollapsed.folders}
|
||||
groupCollapsed={groupCollapsed}
|
||||
toggleGroup={toggleGroup}
|
||||
locale={locale}
|
||||
onToggle={() => toggleGroup('folders')}
|
||||
/>
|
||||
</nav>
|
||||
)
|
||||
@@ -251,6 +502,7 @@ export const Sidebar = memo(function Sidebar({
|
||||
locale = 'en',
|
||||
onCollapse,
|
||||
onCreateNewType,
|
||||
loading = false,
|
||||
}: SidebarProps) {
|
||||
const { typeEntryMap, allSectionGroups, visibleSections, sectionIds } = useSidebarSections(entries)
|
||||
const { activeCount, archivedCount } = useEntryCounts(entries)
|
||||
@@ -310,6 +562,7 @@ export const Sidebar = memo(function Sidebar({
|
||||
showInbox={showInbox}
|
||||
inboxCount={inboxCount}
|
||||
locale={locale}
|
||||
loading={loading}
|
||||
onCreateNewType={onCreateNewType}
|
||||
activeCount={activeCount}
|
||||
archivedCount={archivedCount}
|
||||
|
||||
@@ -118,21 +118,35 @@ export function SidebarCountPill({
|
||||
)
|
||||
}
|
||||
|
||||
export function SidebarLoadingCountPill({ compact, testId = 'sidebar-count-skeleton' }: { compact?: boolean; testId?: string }) {
|
||||
return (
|
||||
<span
|
||||
aria-hidden="true"
|
||||
data-testid={testId}
|
||||
className="inline-flex animate-pulse rounded-full bg-muted"
|
||||
style={{ width: compact ? 22 : 28, height: compact ? 18 : 20 }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function NavItemLabel({ label, compact }: { label: string; compact?: boolean }) {
|
||||
return <span className={cn("flex-1 font-medium", getNavItemTextClass(compact))}>{label}</span>
|
||||
}
|
||||
|
||||
function NavItemCount({
|
||||
count,
|
||||
countLoading,
|
||||
className,
|
||||
style,
|
||||
compact,
|
||||
}: {
|
||||
count?: number
|
||||
countLoading?: boolean
|
||||
className?: string
|
||||
style?: React.CSSProperties
|
||||
compact?: boolean
|
||||
}) {
|
||||
if (countLoading) return <SidebarLoadingCountPill compact={compact} />
|
||||
if (!hasSidebarCount(count)) return null
|
||||
return (
|
||||
<SidebarCountPill
|
||||
@@ -172,6 +186,7 @@ function ClickableNavItem({
|
||||
emoji,
|
||||
label,
|
||||
count,
|
||||
countLoading,
|
||||
isActive,
|
||||
activeClassName,
|
||||
badgeClassName,
|
||||
@@ -186,6 +201,7 @@ function ClickableNavItem({
|
||||
emoji?: string | null
|
||||
label: string
|
||||
count?: number
|
||||
countLoading?: boolean
|
||||
isActive?: boolean
|
||||
activeClassName: string
|
||||
badgeClassName?: string
|
||||
@@ -206,6 +222,7 @@ function ClickableNavItem({
|
||||
<NavItemLabel label={label} compact={compact} />
|
||||
<NavItemCount
|
||||
count={count}
|
||||
countLoading={countLoading}
|
||||
className={resolveBadgeClassName(isActive, activeBadgeClassName, badgeClassName)}
|
||||
style={resolveBadgeStyle(isActive, activeBadgeClassName, activeBadgeStyle, badgeStyle)}
|
||||
compact={compact}
|
||||
@@ -214,11 +231,12 @@ function ClickableNavItem({
|
||||
)
|
||||
}
|
||||
|
||||
export function NavItem({ icon: Icon, emoji, label, count, isActive, activeClassName = 'bg-primary/10 text-primary', badgeClassName, badgeStyle, activeBadgeClassName, activeBadgeStyle, onClick, disabled, disabledTooltip, compact }: {
|
||||
export function NavItem({ icon: Icon, emoji, label, count, countLoading, isActive, activeClassName = 'bg-primary/10 text-primary', badgeClassName, badgeStyle, activeBadgeClassName, activeBadgeStyle, onClick, disabled, disabledTooltip, compact }: {
|
||||
icon: ComponentType<IconProps>
|
||||
emoji?: string | null
|
||||
label: string
|
||||
count?: number
|
||||
countLoading?: boolean
|
||||
isActive?: boolean
|
||||
activeClassName?: string
|
||||
badgeClassName?: string
|
||||
@@ -230,7 +248,7 @@ export function NavItem({ icon: Icon, emoji, label, count, isActive, activeClass
|
||||
disabledTooltip?: string
|
||||
compact?: boolean
|
||||
}) {
|
||||
const padding = getNavItemPadding(compact, hasSidebarCount(count))
|
||||
const padding = getNavItemPadding(compact, countLoading || hasSidebarCount(count))
|
||||
if (disabled) {
|
||||
return (
|
||||
<DisabledNavItem
|
||||
@@ -250,6 +268,7 @@ export function NavItem({ icon: Icon, emoji, label, count, isActive, activeClass
|
||||
emoji={emoji}
|
||||
label={label}
|
||||
count={count}
|
||||
countLoading={countLoading}
|
||||
isActive={isActive}
|
||||
activeClassName={activeClassName}
|
||||
badgeClassName={badgeClassName}
|
||||
|
||||
@@ -2,6 +2,7 @@ import type React from 'react'
|
||||
import { useCallback, useEffect, useRef } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { translate, type AppLocale } from '../../lib/i18n'
|
||||
import type { VaultEntry } from '../../types'
|
||||
import { dispatchEditorFindAvailability } from '../../utils/editorFindEvents'
|
||||
import { DiffView } from '../DiffView'
|
||||
import { BreadcrumbBar } from '../BreadcrumbBar'
|
||||
@@ -38,9 +39,43 @@ type BreadcrumbActions = Pick<
|
||||
| 'onToggleNoteWidth'
|
||||
>
|
||||
|
||||
const LOADING_BREADCRUMB_ENTRY: VaultEntry = {
|
||||
path: '',
|
||||
filename: 'loading.md',
|
||||
title: '',
|
||||
isA: 'Note',
|
||||
aliases: [],
|
||||
belongsTo: [],
|
||||
relatedTo: [],
|
||||
status: null,
|
||||
archived: false,
|
||||
modifiedAt: null,
|
||||
createdAt: null,
|
||||
fileSize: 0,
|
||||
snippet: '',
|
||||
wordCount: 0,
|
||||
relationships: {},
|
||||
icon: null,
|
||||
color: null,
|
||||
order: null,
|
||||
sidebarLabel: null,
|
||||
template: null,
|
||||
sort: null,
|
||||
view: null,
|
||||
visible: true,
|
||||
organized: false,
|
||||
favorite: false,
|
||||
favoriteIndex: null,
|
||||
listPropertiesDisplay: [],
|
||||
outgoingLinks: [],
|
||||
properties: {},
|
||||
hasH1: false,
|
||||
fileKind: 'markdown',
|
||||
}
|
||||
|
||||
function EditorLoadingSkeleton() {
|
||||
return (
|
||||
<div className="flex flex-1 flex-col gap-3 p-8 animate-pulse" style={{ minHeight: 0 }}>
|
||||
<div data-testid="editor-content-skeleton" className="flex flex-1 flex-col gap-3 py-5 animate-pulse" style={{ minHeight: 0 }}>
|
||||
<div className="h-6 w-2/5 rounded bg-muted" />
|
||||
<div className="h-4 w-4/5 rounded bg-muted" />
|
||||
<div className="h-4 w-3/5 rounded bg-muted" />
|
||||
@@ -50,6 +85,16 @@ function EditorLoadingSkeleton() {
|
||||
)
|
||||
}
|
||||
|
||||
function EditorLoadingCanvas({ cssVars }: Pick<EditorContentModel, 'cssVars'>) {
|
||||
return (
|
||||
<div className="editor-scroll-area" style={cssVars as React.CSSProperties}>
|
||||
<div className="editor-content-wrapper">
|
||||
<EditorLoadingSkeleton />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function DiffModeView({ diffContent, locale = 'en', onToggleDiff }: { diffContent: string | null; locale?: AppLocale; onToggleDiff: () => void }) {
|
||||
const label = translate(locale, 'editor.toolbar.rawReturn')
|
||||
|
||||
@@ -117,6 +162,7 @@ function ActiveTabBreadcrumb({
|
||||
path,
|
||||
actions,
|
||||
locale,
|
||||
loadingTitle,
|
||||
}: {
|
||||
activeTab: NonNullable<EditorContentModel['activeTab']>
|
||||
barRef: React.RefObject<HTMLDivElement | null>
|
||||
@@ -124,12 +170,14 @@ function ActiveTabBreadcrumb({
|
||||
path: string
|
||||
actions: BreadcrumbActions
|
||||
locale?: AppLocale
|
||||
loadingTitle?: boolean
|
||||
}) {
|
||||
return (
|
||||
<BreadcrumbBar
|
||||
entry={activeTab.entry}
|
||||
wordCount={wordCount}
|
||||
barRef={barRef}
|
||||
loadingTitle={loadingTitle}
|
||||
showDiffToggle={actions.showDiffToggle}
|
||||
diffMode={actions.diffMode}
|
||||
diffLoading={actions.diffLoading}
|
||||
@@ -156,6 +204,106 @@ function ActiveTabBreadcrumb({
|
||||
)
|
||||
}
|
||||
|
||||
function EditorLoadingBreadcrumb({
|
||||
actions,
|
||||
barRef,
|
||||
locale,
|
||||
}: {
|
||||
actions: BreadcrumbActions
|
||||
barRef: React.RefObject<HTMLDivElement | null>
|
||||
locale?: AppLocale
|
||||
}) {
|
||||
return (
|
||||
<BreadcrumbBar
|
||||
entry={LOADING_BREADCRUMB_ENTRY}
|
||||
wordCount={0}
|
||||
barRef={barRef}
|
||||
loadingTitle
|
||||
showDiffToggle={false}
|
||||
diffMode={false}
|
||||
diffLoading={false}
|
||||
onToggleDiff={actions.onToggleDiff}
|
||||
rawMode={false}
|
||||
forceRawMode={false}
|
||||
showAIChat={actions.showAIChat}
|
||||
onToggleAIChat={actions.onToggleAIChat}
|
||||
inspectorCollapsed={actions.inspectorCollapsed}
|
||||
onToggleInspector={actions.onToggleInspector}
|
||||
noteWidth={actions.noteWidth}
|
||||
onToggleNoteWidth={actions.onToggleNoteWidth}
|
||||
locale={locale}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function buildBreadcrumbActions(model: EditorContentModel): BreadcrumbActions {
|
||||
return {
|
||||
diffMode: model.diffMode,
|
||||
diffLoading: model.diffLoading,
|
||||
onToggleDiff: model.onToggleDiff,
|
||||
effectiveRawMode: model.effectiveRawMode,
|
||||
onToggleRaw: model.onToggleRaw,
|
||||
forceRawMode: model.forceRawMode,
|
||||
showAIChat: model.showAIChat,
|
||||
onToggleAIChat: model.onToggleAIChat,
|
||||
inspectorCollapsed: model.inspectorCollapsed,
|
||||
onToggleInspector: model.onToggleInspector,
|
||||
showDiffToggle: model.showDiffToggle,
|
||||
onToggleFavorite: model.onToggleFavorite,
|
||||
onToggleOrganized: model.onToggleOrganized,
|
||||
onRevealFile: model.onRevealFile,
|
||||
onCopyFilePath: model.onCopyFilePath,
|
||||
onDeleteNote: model.onDeleteNote,
|
||||
onArchiveNote: model.onArchiveNote,
|
||||
onUnarchiveNote: model.onUnarchiveNote,
|
||||
onRenameFilename: model.onRenameFilename,
|
||||
noteWidth: model.noteWidth,
|
||||
onToggleNoteWidth: model.onToggleNoteWidth,
|
||||
}
|
||||
}
|
||||
|
||||
function EditorBreadcrumbArea({
|
||||
actions,
|
||||
barRef,
|
||||
chromePath,
|
||||
chromeTab,
|
||||
chromeWordCount,
|
||||
isVaultLoading,
|
||||
locale,
|
||||
}: {
|
||||
actions: BreadcrumbActions
|
||||
barRef: React.RefObject<HTMLDivElement | null>
|
||||
chromePath: string
|
||||
chromeTab: EditorContentModel['activeTab'] | EditorContentModel['loadingTab']
|
||||
chromeWordCount: number
|
||||
isVaultLoading?: boolean
|
||||
locale?: AppLocale
|
||||
}) {
|
||||
if (chromeTab) {
|
||||
return (
|
||||
<ActiveTabBreadcrumb
|
||||
activeTab={chromeTab}
|
||||
barRef={barRef}
|
||||
wordCount={chromeWordCount}
|
||||
path={chromePath}
|
||||
locale={locale}
|
||||
loadingTitle={isVaultLoading}
|
||||
actions={actions}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (!isVaultLoading) return null
|
||||
|
||||
return (
|
||||
<EditorLoadingBreadcrumb
|
||||
actions={actions}
|
||||
barRef={barRef}
|
||||
locale={locale}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function EditorChrome({
|
||||
isArchived,
|
||||
onUnarchiveNote,
|
||||
@@ -262,6 +410,7 @@ function EditorFindScope({
|
||||
export function EditorContentLayout(model: EditorContentModel) {
|
||||
const {
|
||||
activeTab,
|
||||
loadingTab,
|
||||
isLoadingNewTab,
|
||||
entries,
|
||||
editor,
|
||||
@@ -290,87 +439,69 @@ export function EditorContentLayout(model: EditorContentModel) {
|
||||
noteWidth,
|
||||
findRequest,
|
||||
locale,
|
||||
isVaultLoading,
|
||||
} = model
|
||||
const rootClassName = cn(
|
||||
'flex flex-1 flex-col min-w-0 min-h-0',
|
||||
noteWidth === 'wide' ? 'editor-content-width--wide' : 'editor-content-width--normal',
|
||||
)
|
||||
|
||||
if (!activeTab) {
|
||||
return (
|
||||
<div className={rootClassName}>
|
||||
{isLoadingNewTab && showEditor && <EditorLoadingSkeleton />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const chromeTab = activeTab ?? loadingTab
|
||||
const chromePath = chromeTab?.entry.path ?? path
|
||||
const chromeWordCount = activeTab ? wordCount : 0
|
||||
const showActiveContent = activeTab && !isVaultLoading
|
||||
const showLoadingContent = isVaultLoading || (isLoadingNewTab && showEditor)
|
||||
const breadcrumbActions = buildBreadcrumbActions(model)
|
||||
|
||||
return (
|
||||
<div className={rootClassName}>
|
||||
<ActiveTabBreadcrumb
|
||||
activeTab={activeTab}
|
||||
<EditorBreadcrumbArea
|
||||
actions={breadcrumbActions}
|
||||
barRef={breadcrumbBarRef}
|
||||
wordCount={wordCount}
|
||||
path={path}
|
||||
locale={locale}
|
||||
actions={{
|
||||
diffMode: model.diffMode,
|
||||
diffLoading: model.diffLoading,
|
||||
onToggleDiff: model.onToggleDiff,
|
||||
effectiveRawMode: model.effectiveRawMode,
|
||||
onToggleRaw: model.onToggleRaw,
|
||||
forceRawMode: model.forceRawMode,
|
||||
showAIChat: model.showAIChat,
|
||||
onToggleAIChat: model.onToggleAIChat,
|
||||
inspectorCollapsed: model.inspectorCollapsed,
|
||||
onToggleInspector: model.onToggleInspector,
|
||||
showDiffToggle: model.showDiffToggle,
|
||||
onToggleFavorite: model.onToggleFavorite,
|
||||
onToggleOrganized: model.onToggleOrganized,
|
||||
onRevealFile: model.onRevealFile,
|
||||
onCopyFilePath: model.onCopyFilePath,
|
||||
onDeleteNote: model.onDeleteNote,
|
||||
onArchiveNote: model.onArchiveNote,
|
||||
onUnarchiveNote: model.onUnarchiveNote,
|
||||
onRenameFilename: model.onRenameFilename,
|
||||
noteWidth: model.noteWidth,
|
||||
onToggleNoteWidth: model.onToggleNoteWidth,
|
||||
}}
|
||||
/>
|
||||
<EditorChrome
|
||||
isArchived={isArchived}
|
||||
onUnarchiveNote={onUnarchiveNote}
|
||||
path={path}
|
||||
isConflicted={isConflicted}
|
||||
onKeepMine={onKeepMine}
|
||||
onKeepTheirs={onKeepTheirs}
|
||||
diffMode={diffMode}
|
||||
diffContent={diffContent}
|
||||
onToggleDiff={onToggleDiff}
|
||||
chromePath={chromePath}
|
||||
chromeTab={chromeTab}
|
||||
chromeWordCount={chromeWordCount}
|
||||
isVaultLoading={isVaultLoading}
|
||||
locale={locale}
|
||||
/>
|
||||
<RawModeEditorSection
|
||||
activeTab={activeTab}
|
||||
entries={entries}
|
||||
findRequest={findRequest}
|
||||
rawMode={effectiveRawMode}
|
||||
rawModeContent={rawModeContent}
|
||||
onRawContentChange={onRawContentChange}
|
||||
onSave={onSave}
|
||||
rawLatestContentRef={rawLatestContentRef}
|
||||
vaultPath={vaultPath}
|
||||
locale={locale}
|
||||
/>
|
||||
<EditorCanvas
|
||||
showEditor={showEditor}
|
||||
cssVars={cssVars}
|
||||
vaultPath={vaultPath}
|
||||
editor={editor}
|
||||
entries={entries}
|
||||
onNavigateWikilink={onNavigateWikilink}
|
||||
onEditorChange={onEditorChange}
|
||||
isDeletedPreview={isDeletedPreview}
|
||||
/>
|
||||
{isLoadingNewTab && showEditor && <EditorLoadingSkeleton />}
|
||||
{showActiveContent && (
|
||||
<>
|
||||
<EditorChrome
|
||||
isArchived={isArchived}
|
||||
onUnarchiveNote={onUnarchiveNote}
|
||||
path={path}
|
||||
isConflicted={isConflicted}
|
||||
onKeepMine={onKeepMine}
|
||||
onKeepTheirs={onKeepTheirs}
|
||||
diffMode={diffMode}
|
||||
diffContent={diffContent}
|
||||
onToggleDiff={onToggleDiff}
|
||||
locale={locale}
|
||||
/>
|
||||
<RawModeEditorSection
|
||||
activeTab={activeTab}
|
||||
entries={entries}
|
||||
findRequest={findRequest}
|
||||
rawMode={effectiveRawMode}
|
||||
rawModeContent={rawModeContent}
|
||||
onRawContentChange={onRawContentChange}
|
||||
onSave={onSave}
|
||||
rawLatestContentRef={rawLatestContentRef}
|
||||
vaultPath={vaultPath}
|
||||
locale={locale}
|
||||
/>
|
||||
<EditorCanvas
|
||||
showEditor={showEditor}
|
||||
cssVars={cssVars}
|
||||
vaultPath={vaultPath}
|
||||
editor={editor}
|
||||
entries={entries}
|
||||
onNavigateWikilink={onNavigateWikilink}
|
||||
onEditorChange={onEditorChange}
|
||||
isDeletedPreview={isDeletedPreview}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{showLoadingContent && <EditorLoadingCanvas cssVars={cssVars} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -14,7 +14,9 @@ export interface Tab {
|
||||
|
||||
export interface EditorContentProps {
|
||||
activeTab: Tab | null
|
||||
activeTabPath: string | null
|
||||
isLoadingNewTab: boolean
|
||||
isVaultLoading?: boolean
|
||||
entries: VaultEntry[]
|
||||
editor: ReturnType<typeof useCreateBlockNote>
|
||||
diffMode: boolean
|
||||
@@ -56,6 +58,7 @@ export interface EditorContentProps {
|
||||
export function useEditorContentModel(props: EditorContentProps) {
|
||||
const {
|
||||
activeTab,
|
||||
activeTabPath,
|
||||
entries,
|
||||
rawMode,
|
||||
diffMode,
|
||||
@@ -77,6 +80,10 @@ export function useEditorContentModel(props: EditorContentProps) {
|
||||
activeStatus: props.activeStatus,
|
||||
})
|
||||
const showEditor = !diffMode && showContentEditor
|
||||
const loadingEntry = !activeTab && activeTabPath
|
||||
? entries.find((entry) => entry.path === activeTabPath) ?? null
|
||||
: null
|
||||
const loadingTab = loadingEntry ? { entry: loadingEntry, content: '' } : null
|
||||
|
||||
const breadcrumbBarRef = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
@@ -88,6 +95,7 @@ export function useEditorContentModel(props: EditorContentProps) {
|
||||
effectiveRawMode,
|
||||
forceRawMode: isNonMarkdownText || isDeletedPreview,
|
||||
showEditor,
|
||||
loadingTab,
|
||||
path,
|
||||
breadcrumbBarRef,
|
||||
wordCount,
|
||||
|
||||
@@ -8,6 +8,57 @@ type NoteListLayoutProps = ReturnType<typeof useNoteListModel> & {
|
||||
handleBulkOrganize?: () => void
|
||||
}
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
function MultiSelectBar({
|
||||
multiSelect,
|
||||
isArchivedView,
|
||||
@@ -47,6 +98,7 @@ function NoteListContent({
|
||||
searched,
|
||||
noteListVirtuosoRef,
|
||||
locale,
|
||||
loading,
|
||||
}: Pick<
|
||||
NoteListLayoutProps,
|
||||
| 'entitySelection'
|
||||
@@ -64,10 +116,13 @@ function NoteListContent({
|
||||
| 'searched'
|
||||
| 'noteListVirtuosoRef'
|
||||
| 'locale'
|
||||
| 'loading'
|
||||
>) {
|
||||
return (
|
||||
<div className="flex-1 overflow-hidden" style={{ minHeight: 0 }}>
|
||||
{entitySelection ? (
|
||||
{loading ? (
|
||||
<NoteListLoadingSkeleton />
|
||||
) : entitySelection ? (
|
||||
<EntityView
|
||||
entity={entitySelection.entry}
|
||||
groups={searchedGroups}
|
||||
@@ -121,6 +176,7 @@ function NoteListBody({
|
||||
noteListFilter,
|
||||
filterCounts,
|
||||
onNoteListFilterChange,
|
||||
loading,
|
||||
}: Pick<
|
||||
NoteListLayoutProps,
|
||||
| 'handleListKeyDown'
|
||||
@@ -147,6 +203,7 @@ function NoteListBody({
|
||||
| 'noteListFilter'
|
||||
| 'filterCounts'
|
||||
| 'onNoteListFilterChange'
|
||||
| 'loading'
|
||||
>) {
|
||||
return (
|
||||
<div
|
||||
@@ -176,6 +233,7 @@ function NoteListBody({
|
||||
searched={searched}
|
||||
noteListVirtuosoRef={noteListVirtuosoRef}
|
||||
locale={locale}
|
||||
loading={loading}
|
||||
/>
|
||||
{showFilterPills && (
|
||||
<FilterPills
|
||||
|
||||
@@ -437,6 +437,7 @@ export interface NoteListProps {
|
||||
entries: VaultEntry[]
|
||||
selection: SidebarSelection
|
||||
selectedNote: VaultEntry | null
|
||||
loading?: boolean
|
||||
noteListFilter: NoteListFilter
|
||||
onNoteListFilterChange: (filter: NoteListFilter) => void
|
||||
inboxPeriod?: InboxPeriod
|
||||
@@ -471,6 +472,7 @@ function buildNoteListLayoutModel(params: {
|
||||
selection: SidebarSelection
|
||||
views?: ViewFile[]
|
||||
sidebarCollapsed?: boolean
|
||||
loading: boolean
|
||||
modifiedFilesError?: string | null
|
||||
noteListFilter: NoteListFilter
|
||||
filterCounts: ReturnType<typeof useFilterCounts>
|
||||
@@ -487,6 +489,7 @@ function buildNoteListLayoutModel(params: {
|
||||
}) {
|
||||
return {
|
||||
title: resolveHeaderTitle(params.selection, params.content.typeDocument, params.views, params.locale),
|
||||
loading: params.loading,
|
||||
locale: params.locale,
|
||||
typeDocument: params.content.typeDocument,
|
||||
isEntityView: params.content.isEntityView,
|
||||
@@ -545,6 +548,7 @@ export function useNoteListModel({
|
||||
entries,
|
||||
selection,
|
||||
selectedNote,
|
||||
loading = false,
|
||||
noteListFilter,
|
||||
onNoteListFilterChange,
|
||||
inboxPeriod = 'all',
|
||||
@@ -663,6 +667,7 @@ export function useNoteListModel({
|
||||
selection,
|
||||
views,
|
||||
sidebarCollapsed,
|
||||
loading,
|
||||
onOpenType: onReplaceActiveTab,
|
||||
modifiedFilesError,
|
||||
noteListFilter,
|
||||
|
||||
277
src/components/sidebar/SidebarLoadingSections.tsx
Normal file
277
src/components/sidebar/SidebarLoadingSections.tsx
Normal file
@@ -0,0 +1,277 @@
|
||||
import type { MouseEvent, ReactNode } from 'react'
|
||||
import { Folder, Funnel, Plus } from '@phosphor-icons/react'
|
||||
import { SlidersHorizontal } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { translate, type AppLocale } from '../../lib/i18n'
|
||||
import { SidebarGroupHeader } from './SidebarGroupHeader'
|
||||
|
||||
interface SidebarLoadingSectionsProps {
|
||||
collapsed: boolean
|
||||
locale?: AppLocale
|
||||
onToggle: () => void
|
||||
}
|
||||
|
||||
interface SidebarLoadingActionProps {
|
||||
label: string
|
||||
onClick?: () => void
|
||||
testId?: string
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
interface SidebarLoadingRowProps {
|
||||
icon?: ReactNode
|
||||
iconColor?: string
|
||||
labelWidth: number
|
||||
showCount?: boolean
|
||||
}
|
||||
|
||||
interface SidebarLoadingSectionProps extends SidebarLoadingSectionsProps {
|
||||
label: string
|
||||
rows: SidebarLoadingRowProps[]
|
||||
testId: string
|
||||
children?: ReactNode
|
||||
}
|
||||
|
||||
interface CreatableLoadingSectionProps extends SidebarLoadingSectionsProps {
|
||||
actionLabel: string
|
||||
actionTestId?: string
|
||||
label: string
|
||||
onCreate?: () => void
|
||||
rows: SidebarLoadingRowProps[]
|
||||
testId: string
|
||||
}
|
||||
|
||||
interface CreatableLoadingSectionConfig {
|
||||
actionLabelKey: Parameters<typeof translate>[1]
|
||||
actionTestId?: string
|
||||
labelKey: Parameters<typeof translate>[1]
|
||||
rows: SidebarLoadingRowProps[]
|
||||
testId: string
|
||||
}
|
||||
|
||||
const FAVORITE_ROWS = [
|
||||
{ iconColor: 'var(--accent-yellow)', labelWidth: 132 },
|
||||
{ iconColor: 'var(--accent-red)', labelWidth: 118 },
|
||||
]
|
||||
|
||||
const VIEW_ROWS = [
|
||||
{ icon: <Funnel size={16} />, labelWidth: 118 },
|
||||
{ icon: <Funnel size={16} />, labelWidth: 146, showCount: true },
|
||||
]
|
||||
|
||||
const TYPE_ROWS = [
|
||||
{ iconColor: 'var(--accent-red)', labelWidth: 72, showCount: true },
|
||||
{ iconColor: 'var(--accent-orange)', labelWidth: 92, showCount: true },
|
||||
{ iconColor: 'var(--accent-purple)', labelWidth: 104, showCount: true },
|
||||
{ iconColor: 'var(--accent-blue)', labelWidth: 126, showCount: true },
|
||||
{ iconColor: 'var(--accent-green)', labelWidth: 112, showCount: true },
|
||||
{ iconColor: 'var(--accent-yellow)', labelWidth: 96, showCount: true },
|
||||
]
|
||||
|
||||
const FOLDER_ROWS = [
|
||||
{ icon: <Folder size={16} />, labelWidth: 118 },
|
||||
{ icon: <Folder size={16} />, labelWidth: 92 },
|
||||
]
|
||||
|
||||
type CreatableLoadingSectionKind = 'views' | 'folders'
|
||||
|
||||
const CREATABLE_LOADING_SECTIONS: Record<CreatableLoadingSectionKind, CreatableLoadingSectionConfig> = {
|
||||
views: {
|
||||
actionLabelKey: 'sidebar.action.createView',
|
||||
labelKey: 'sidebar.group.views',
|
||||
rows: VIEW_ROWS,
|
||||
testId: 'sidebar-loading-views',
|
||||
},
|
||||
folders: {
|
||||
actionLabelKey: 'sidebar.action.createFolder',
|
||||
actionTestId: 'create-folder-btn',
|
||||
labelKey: 'sidebar.group.folders',
|
||||
rows: FOLDER_ROWS,
|
||||
testId: 'sidebar-loading-folders',
|
||||
},
|
||||
}
|
||||
|
||||
interface ConfiguredCreatableLoadingSectionProps extends SidebarLoadingSectionsProps {
|
||||
kind: CreatableLoadingSectionKind
|
||||
onCreate?: () => void
|
||||
}
|
||||
|
||||
function SidebarLoadingAction({
|
||||
label,
|
||||
onClick,
|
||||
testId,
|
||||
children,
|
||||
}: SidebarLoadingActionProps) {
|
||||
const handleClick = onClick
|
||||
? (event: MouseEvent<HTMLButtonElement>) => {
|
||||
event.stopPropagation()
|
||||
onClick()
|
||||
}
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
className="h-auto w-auto min-w-0 rounded-none p-0 text-muted-foreground hover:bg-transparent hover:text-foreground"
|
||||
data-testid={testId}
|
||||
title={label}
|
||||
aria-label={label}
|
||||
aria-disabled={onClick ? undefined : true}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarLoadingIcon({ icon, iconColor }: Pick<SidebarLoadingRowProps, 'icon' | 'iconColor'>) {
|
||||
if (icon) return <span className="shrink-0 text-muted-foreground">{icon}</span>
|
||||
|
||||
return (
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="h-4 w-4 shrink-0 rounded-sm"
|
||||
style={{ background: iconColor ?? 'var(--muted)' }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarLoadingBar({ width }: { width: number }) {
|
||||
return (
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="h-3.5 rounded bg-muted"
|
||||
style={{ width }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarLoadingRow({
|
||||
icon,
|
||||
iconColor,
|
||||
labelWidth,
|
||||
showCount,
|
||||
}: SidebarLoadingRowProps) {
|
||||
return (
|
||||
<div
|
||||
className="flex select-none items-center gap-2 rounded"
|
||||
style={{ padding: '6px 8px 6px 16px', borderRadius: 4 }}
|
||||
>
|
||||
<SidebarLoadingIcon icon={icon} iconColor={iconColor} />
|
||||
<div className="flex min-w-0 flex-1 items-center">
|
||||
<SidebarLoadingBar width={labelWidth} />
|
||||
</div>
|
||||
{showCount && <span aria-hidden="true" className="h-5 w-7 rounded-full bg-muted" />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarLoadingSection({
|
||||
label,
|
||||
collapsed,
|
||||
onToggle,
|
||||
rows,
|
||||
testId,
|
||||
children,
|
||||
}: SidebarLoadingSectionProps) {
|
||||
return (
|
||||
<div className="border-b border-border" data-testid={testId} style={{ padding: '0 6px' }}>
|
||||
<SidebarGroupHeader label={label} collapsed={collapsed} onToggle={onToggle}>
|
||||
{children}
|
||||
</SidebarGroupHeader>
|
||||
{!collapsed && (
|
||||
<div className="flex flex-col gap-0.5 pb-2 animate-pulse" aria-hidden="true">
|
||||
{rows.map((row, index) => (
|
||||
<SidebarLoadingRow key={`${testId}-${index}`} {...row} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function SidebarFavoritesLoadingSection(props: SidebarLoadingSectionsProps) {
|
||||
return (
|
||||
<SidebarLoadingSection
|
||||
{...props}
|
||||
label={translate(props.locale ?? 'en', 'sidebar.group.favorites')}
|
||||
rows={FAVORITE_ROWS}
|
||||
testId="sidebar-loading-favorites"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function SidebarCreatableLoadingSection({
|
||||
kind,
|
||||
onCreate,
|
||||
...props
|
||||
}: ConfiguredCreatableLoadingSectionProps) {
|
||||
const config = CREATABLE_LOADING_SECTIONS[kind]
|
||||
const locale = props.locale ?? 'en'
|
||||
return (
|
||||
<CreatableLoadingSection
|
||||
{...props}
|
||||
actionLabel={translate(locale, config.actionLabelKey)}
|
||||
actionTestId={config.actionTestId}
|
||||
label={translate(locale, config.labelKey)}
|
||||
onCreate={onCreate}
|
||||
rows={config.rows}
|
||||
testId={config.testId}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CreatableLoadingSection({
|
||||
actionLabel,
|
||||
actionTestId,
|
||||
label,
|
||||
onCreate,
|
||||
rows,
|
||||
testId,
|
||||
...props
|
||||
}: CreatableLoadingSectionProps) {
|
||||
return (
|
||||
<SidebarLoadingSection
|
||||
{...props}
|
||||
label={label}
|
||||
rows={rows}
|
||||
testId={testId}
|
||||
>
|
||||
{onCreate && (
|
||||
<SidebarLoadingAction label={actionLabel} onClick={onCreate} testId={actionTestId}>
|
||||
<Plus size={12} className="text-muted-foreground hover:text-foreground" />
|
||||
</SidebarLoadingAction>
|
||||
)}
|
||||
</SidebarLoadingSection>
|
||||
)
|
||||
}
|
||||
|
||||
export function SidebarTypesLoadingSection({
|
||||
onCreateNewType,
|
||||
...props
|
||||
}: SidebarLoadingSectionsProps & {
|
||||
onCreateNewType?: () => void
|
||||
}) {
|
||||
const locale = props.locale ?? 'en'
|
||||
return (
|
||||
<SidebarLoadingSection
|
||||
{...props}
|
||||
label={translate(locale, 'sidebar.group.types')}
|
||||
rows={TYPE_ROWS}
|
||||
testId="sidebar-loading-types"
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<SidebarLoadingAction label={translate(locale, 'sidebar.action.customizeSections')}>
|
||||
<SlidersHorizontal size={12} className="text-muted-foreground" />
|
||||
</SidebarLoadingAction>
|
||||
{onCreateNewType && (
|
||||
<SidebarLoadingAction label={translate(locale, 'sidebar.action.createType')} onClick={onCreateNewType} testId="create-type-btn">
|
||||
<Plus size={12} className="text-muted-foreground hover:text-foreground" />
|
||||
</SidebarLoadingAction>
|
||||
)}
|
||||
</div>
|
||||
</SidebarLoadingSection>
|
||||
)
|
||||
}
|
||||
@@ -11,6 +11,7 @@ interface SidebarTopNavProps {
|
||||
activeCount: number
|
||||
archivedCount: number
|
||||
locale?: AppLocale
|
||||
loading?: boolean
|
||||
}
|
||||
|
||||
export function SidebarTopNav({
|
||||
@@ -21,6 +22,7 @@ export function SidebarTopNav({
|
||||
activeCount,
|
||||
archivedCount,
|
||||
locale = 'en',
|
||||
loading = false,
|
||||
}: SidebarTopNavProps) {
|
||||
return (
|
||||
<div className="border-b border-border" data-testid="sidebar-top-nav" style={{ padding: '4px 6px' }}>
|
||||
@@ -29,6 +31,7 @@ export function SidebarTopNav({
|
||||
icon={Tray}
|
||||
label={translate(locale, 'sidebar.nav.inbox')}
|
||||
count={inboxCount}
|
||||
countLoading={loading}
|
||||
isActive={isSelectionActive(selection, { kind: 'filter', filter: 'inbox' })}
|
||||
badgeClassName="text-muted-foreground"
|
||||
badgeStyle={{ background: 'var(--muted)' }}
|
||||
@@ -40,6 +43,7 @@ export function SidebarTopNav({
|
||||
icon={FileText}
|
||||
label={translate(locale, 'sidebar.nav.allNotes')}
|
||||
count={activeCount}
|
||||
countLoading={loading}
|
||||
isActive={isSelectionActive(selection, { kind: 'filter', filter: 'all' })}
|
||||
badgeClassName="text-muted-foreground"
|
||||
badgeStyle={{ background: 'var(--muted)' }}
|
||||
@@ -50,6 +54,7 @@ export function SidebarTopNav({
|
||||
icon={Archive}
|
||||
label={translate(locale, 'sidebar.nav.archive')}
|
||||
count={archivedCount}
|
||||
countLoading={loading}
|
||||
isActive={isSelectionActive(selection, { kind: 'filter', filter: 'archived' })}
|
||||
badgeClassName="text-muted-foreground"
|
||||
badgeStyle={{ background: 'var(--muted)' }}
|
||||
|
||||
@@ -99,8 +99,16 @@ async function installSlowVaultMock(page: Page): Promise<void> {
|
||||
}
|
||||
|
||||
async function expectResponsiveShellWhileVaultLoads(page: Page): Promise<void> {
|
||||
await expect(page.getByTestId('sidebar-loading-favorites')).toBeVisible({ timeout: 5_000 })
|
||||
await expect(page.getByTestId('vault-loading-skeleton')).not.toBeVisible()
|
||||
await expect(page.getByText('Select a note to start editing')).toBeVisible()
|
||||
await expect(page.getByTestId('sidebar-top-nav')).toContainText('Inbox')
|
||||
await expect(page.getByTestId('sidebar-loading-views')).toBeVisible()
|
||||
await expect(page.getByTestId('sidebar-loading-types')).toBeVisible()
|
||||
await expect(page.getByTestId('sidebar-loading-folders')).toBeVisible()
|
||||
await expect(page.getByTestId('note-list-loading-skeleton')).toBeVisible()
|
||||
await expect(page.getByTestId('breadcrumb-title-skeleton')).toBeVisible()
|
||||
await expect(page.getByTestId('editor-content-skeleton')).toBeVisible()
|
||||
await expect(page.getByText('Select a note to start editing')).not.toBeVisible()
|
||||
await expect(page.getByTestId('status-vault-reloading')).toHaveAccessibleName('Reloading vault from disk')
|
||||
|
||||
await sendShortcut(page, 'p', ['Control'])
|
||||
|
||||
Reference in New Issue
Block a user