feat: dedicated files layout (#1928)

This commit is contained in:
Mo
2022-11-02 11:06:19 -05:00
committed by GitHub
parent 1498cce37f
commit dd821c95e6
18 changed files with 226 additions and 98 deletions

View File

@@ -17,4 +17,5 @@ export interface TagPreferences {
customNoteTitleFormat?: string customNoteTitleFormat?: string
editorIdentifier?: FeatureIdentifier | string editorIdentifier?: FeatureIdentifier | string
entryMode?: 'normal' | 'daily' entryMode?: 'normal' | 'daily'
panelWidth?: number
} }

View File

@@ -12,7 +12,7 @@ import { NavigationController } from '@/Controllers/Navigation/NavigationControl
import { NotesController } from '@/Controllers/NotesController' import { NotesController } from '@/Controllers/NotesController'
import { ElementIds } from '@/Constants/ElementIDs' import { ElementIds } from '@/Constants/ElementIDs'
import { classNames } from '@/Utils/ConcatenateClassNames' import { classNames } from '@/Utils/ConcatenateClassNames'
import { SNTag } from '@standardnotes/snjs' import { ContentType, SNTag } from '@standardnotes/snjs'
type Props = { type Props = {
application: WebApplication application: WebApplication
@@ -95,35 +95,44 @@ const ContentList: FunctionComponent<Props> = ({
[hideTags, selectedTag, application], [hideTags, selectedTag, application],
) )
const hasNotes = items.some((item) => item.content_type === ContentType.Note)
return ( return (
<div <div
className={classNames( className={classNames(
'infinite-scroll overflow-y-auto overflow-x-hidden focus:shadow-none focus:outline-none', 'infinite-scroll overflow-y-auto overflow-x-hidden focus:shadow-none focus:outline-none',
'md:max-h-full md:overflow-y-hidden md:hover:overflow-y-auto pointer-coarse:md:overflow-y-auto', 'md:max-h-full md:overflow-y-hidden md:hover:overflow-y-auto pointer-coarse:md:overflow-y-auto',
'md:hover:[overflow-y:_overlay]', 'flex flex-wrap pb-2 md:hover:[overflow-y:_overlay]',
hasNotes ? 'justify-center' : 'justify-center md:justify-start md:pl-1',
)} )}
id={ElementIds.ContentList} id={ElementIds.ContentList}
onScroll={onScroll} onScroll={onScroll}
onKeyDown={onKeyDown} onKeyDown={onKeyDown}
tabIndex={FOCUSABLE_BUT_NOT_TABBABLE} tabIndex={FOCUSABLE_BUT_NOT_TABBABLE}
> >
{items.map((item) => ( {items.map((item, index) => {
<ContentListItem const previousItem = items[index - 1]
key={item.uuid} const nextItem = items[index + 1]
application={application} return (
item={item} <ContentListItem
selected={selectedUuids.has(item.uuid)} key={item.uuid}
hideDate={hideDate} application={application}
hidePreview={hideNotePreview} item={item}
hideTags={hideTags} selected={selectedUuids.has(item.uuid)}
hideIcon={hideEditorIcon} hideDate={hideDate}
sortBy={sortBy} hidePreview={hideNotePreview}
filesController={filesController} hideTags={hideTags}
onSelect={selectItem} hideIcon={hideEditorIcon}
tags={getTagsForItem(item)} sortBy={sortBy}
notesController={notesController} filesController={filesController}
/> onSelect={selectItem}
))} tags={getTagsForItem(item)}
notesController={notesController}
isPreviousItemTiled={previousItem?.content_type === ContentType.File}
isNextItemTiled={nextItem?.content_type === ContentType.File}
/>
)
})}
</div> </div>
) )
} }

View File

@@ -1,15 +1,16 @@
import { ContentType } from '@standardnotes/snjs' import { ContentType, FileItem, SNNote } from '@standardnotes/snjs'
import React, { FunctionComponent } from 'react' import React, { FunctionComponent } from 'react'
import FileListItem from './FileListItem' import FileListItem from './FileListItem'
import NoteListItem from './NoteListItem' import NoteListItem from './NoteListItem'
import { AbstractListItemProps, doListItemPropsMeritRerender } from './Types/AbstractListItemProps' import { AbstractListItemProps, doListItemPropsMeritRerender } from './Types/AbstractListItemProps'
import { ListableContentItem } from './Types/ListableContentItem'
const ContentListItem: FunctionComponent<AbstractListItemProps> = (props) => { const ContentListItem: FunctionComponent<AbstractListItemProps<ListableContentItem>> = (props) => {
switch (props.item.content_type) { switch (props.item.content_type) {
case ContentType.Note: case ContentType.Note:
return <NoteListItem {...props} /> return <NoteListItem {...props} item={props.item as SNNote} />
case ContentType.File: case ContentType.File:
return <FileListItem {...props} /> return <FileListItem {...props} item={props.item as FileItem} />
default: default:
return null return null
} }

View File

@@ -1,7 +1,7 @@
import { KeyboardKey, KeyboardModifier } from '@standardnotes/ui-services' import { KeyboardKey, KeyboardModifier } from '@standardnotes/ui-services'
import { WebApplication } from '@/Application/Application' import { WebApplication } from '@/Application/Application'
import { PANEL_NAME_NOTES } from '@/Constants/Constants' import { PANEL_NAME_NOTES } from '@/Constants/Constants'
import { FileItem, PrefKey, SystemViewId } from '@standardnotes/snjs' import { FileItem, PrefKey } from '@standardnotes/snjs'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { FunctionComponent, useCallback, useEffect, useMemo, useRef } from 'react' import { FunctionComponent, useCallback, useEffect, useMemo, useRef } from 'react'
import ContentList from '@/Components/ContentListView/ContentList' import ContentList from '@/Components/ContentListView/ContentList'
@@ -118,10 +118,7 @@ const ContentListView: FunctionComponent<Props> = ({
const icon = selectedTag?.iconString const icon = selectedTag?.iconString
const isFilesSmartView = useMemo( const isFilesSmartView = useMemo(() => navigationController.isInFilesView, [navigationController.isInFilesView])
() => navigationController.selected?.uuid === SystemViewId.Files,
[navigationController.selected?.uuid],
)
const addNewItem = useCallback(async () => { const addNewItem = useCallback(async () => {
if (isFilesSmartView) { if (isFilesSmartView) {
@@ -215,10 +212,14 @@ const ContentListView: FunctionComponent<Props> = ({
const panelResizeFinishCallback: ResizeFinishCallback = useCallback( const panelResizeFinishCallback: ResizeFinishCallback = useCallback(
(width, _lastLeft, _isMaxWidth, isCollapsed) => { (width, _lastLeft, _isMaxWidth, isCollapsed) => {
application.setPreference(PrefKey.NotesPanelWidth, width).catch(console.error) if (selectedAsTag) {
navigationController.setPanelWidthForTag(selectedAsTag, width)
} else {
application.setPreference(PrefKey.NotesPanelWidth, width).catch(console.error)
}
application.publishPanelDidResizeEvent(PANEL_NAME_NOTES, isCollapsed) application.publishPanelDidResizeEvent(PANEL_NAME_NOTES, isCollapsed)
}, },
[application], [application, selectedAsTag, navigationController],
) )
const addButtonLabel = useMemo( const addButtonLabel = useMemo(
@@ -243,15 +244,26 @@ const ContentListView: FunctionComponent<Props> = ({
[selectionController], [selectionController],
) )
useEffect(() => {
const hasEditorPane = selectedUuids.size > 0
if (!hasEditorPane) {
itemsViewPanelRef.current?.style.removeProperty('width')
}
}, [selectedUuids, itemsViewPanelRef])
const hasEditorPane = selectedUuids.size > 0 || renderedItems.length === 0
return ( return (
<div <div
id="items-column" id="items-column"
className={classNames( className={classNames(
'sn-component section app-column flex h-full flex-col overflow-hidden pt-safe-top', 'sn-component section app-column flex h-full flex-col overflow-hidden pt-safe-top',
'xl:w-87.5 xsm-only:!w-full sm-only:!w-full', hasEditorPane ? 'xl:w-[24rem] xsm-only:!w-full sm-only:!w-full' : 'w-full md:min-w-[400px]',
isTabletScreenSize && !isNotesListVisibleOnTablets hasEditorPane
? 'pointer-coarse:md-only:!w-0 pointer-coarse:lg-only:!w-0' ? isTabletScreenSize && !isNotesListVisibleOnTablets
: 'pointer-coarse:md-only:!w-60 pointer-coarse:lg-only:!w-60', ? 'pointer-coarse:md-only:!w-0 pointer-coarse:lg-only:!w-0'
: 'pointer-coarse:md-only:!w-60 pointer-coarse:lg-only:!w-60'
: '',
)} )}
aria-label={'Notes & Files'} aria-label={'Notes & Files'}
ref={itemsViewPanelRef} ref={itemsViewPanelRef}
@@ -327,7 +339,7 @@ const ContentListView: FunctionComponent<Props> = ({
) : null} ) : null}
<div className="absolute bottom-0 h-safe-bottom w-full" /> <div className="absolute bottom-0 h-safe-bottom w-full" />
</ResponsivePaneContent> </ResponsivePaneContent>
{itemsViewPanelRef.current && ( {hasEditorPane && itemsViewPanelRef.current && (
<PanelResizer <PanelResizer
collapsable={true} collapsable={true}
hoverable={true} hoverable={true}

View File

@@ -1,5 +1,5 @@
import { formatDateAndTimeForNote } from '@/Utils/DateUtils' import { formatDateAndTimeForNote } from '@/Utils/DateUtils'
import { SNTag } from '@standardnotes/snjs' import { isNote, SNTag } from '@standardnotes/snjs'
import { ComponentPropsWithoutRef, forwardRef, FunctionComponent, Ref } from 'react' import { ComponentPropsWithoutRef, forwardRef, FunctionComponent, Ref } from 'react'
import ListItemFlagIcons from '../ListItemFlagIcons' import ListItemFlagIcons from '../ListItemFlagIcons'
import ListItemMetadata from '../ListItemMetadata' import ListItemMetadata from '../ListItemMetadata'
@@ -65,7 +65,7 @@ export const DailyItemCell = forwardRef(
{item && ( {item && (
<> <>
<ListItemTitle item={item} /> <ListItemTitle item={item} />
<ListItemNotePreviewText hidePreview={hidePreview} item={item} lineLimit={5} /> {isNote(item) && <ListItemNotePreviewText hidePreview={hidePreview} item={item} lineLimit={5} />}
<ListItemMetadata item={item} hideDate={hideDate} sortBy={'created_at'} /> <ListItemMetadata item={item} hideDate={hideDate} sortBy={'created_at'} />
<ListItemTags hideTags={hideTags} tags={tags} /> <ListItemTags hideTags={hideTags} tags={tags} />
</> </>

View File

@@ -3,15 +3,16 @@ import { observer } from 'mobx-react-lite'
import { FunctionComponent, useCallback, useRef } from 'react' import { FunctionComponent, useCallback, useRef } from 'react'
import { getFileIconComponent } from '../FilePreview/getFileIconComponent' import { getFileIconComponent } from '../FilePreview/getFileIconComponent'
import ListItemConflictIndicator from './ListItemConflictIndicator' import ListItemConflictIndicator from './ListItemConflictIndicator'
import ListItemFlagIcons from './ListItemFlagIcons'
import ListItemTags from './ListItemTags' import ListItemTags from './ListItemTags'
import ListItemMetadata from './ListItemMetadata' import ListItemMetadata from './ListItemMetadata'
import { DisplayableListItemProps } from './Types/DisplayableListItemProps' import { DisplayableListItemProps } from './Types/DisplayableListItemProps'
import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider' import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
import { AppPaneId } from '../ResponsivePane/AppPaneMetadata' import { AppPaneId } from '../ResponsivePane/AppPaneMetadata'
import { useContextMenuEvent } from '@/Hooks/useContextMenuEvent' import { useContextMenuEvent } from '@/Hooks/useContextMenuEvent'
import { classNames } from '@/Utils/ConcatenateClassNames'
import { formatSizeToReadableString } from '@standardnotes/filepicker'
const FileListItem: FunctionComponent<DisplayableListItemProps> = ({ const FileListItem: FunctionComponent<DisplayableListItemProps<FileItem>> = ({
application, application,
filesController, filesController,
hideDate, hideDate,
@@ -67,7 +68,7 @@ const FileListItem: FunctionComponent<DisplayableListItemProps> = ({
const IconComponent = () => const IconComponent = () =>
getFileIconComponent( getFileIconComponent(
application.iconsController.getIconForFileType((item as FileItem).mimeType), application.iconsController.getIconForFileType((item as FileItem).mimeType),
'w-5 h-5 flex-shrink-0', 'w-10 h-10 flex-shrink-0',
) )
useContextMenuEvent(listItemRef, openContextMenu) useContextMenuEvent(listItemRef, openContextMenu)
@@ -75,28 +76,41 @@ const FileListItem: FunctionComponent<DisplayableListItemProps> = ({
return ( return (
<div <div
ref={listItemRef} ref={listItemRef}
className={`content-list-item flex w-full cursor-pointer items-stretch text-text ${ className={classNames('flex max-h-[300px] w-[190px] cursor-pointer px-1 pt-2 text-text md:w-[200px]')}
selected && 'selected border-l-2px border-solid border-info'
}`}
id={item.uuid} id={item.uuid}
onClick={onClick} onClick={onClick}
> >
{!hideIcon ? ( <div
<div className="mr-0 flex flex-col items-center justify-between p-4.5 pr-3"> className={`flex flex-col justify-between overflow-hidden rounded bg-passive-5 pt-5 transition-all hover:bg-passive-4 ${
<IconComponent /> selected ? 'border-[1px] border-solid border-info' : 'border-[1px] border-solid border-border'
}`}
>
<div className={'px-5'}>
{!hideIcon ? (
<div className="mr-0">
<IconComponent />
</div>
) : (
<div className="pr-4" />
)}
<div className="min-w-0 flex-grow py-4 px-0">
<div className="line-clamp-2 overflow-hidden text-editor font-semibold">
<div className="break-word line-clamp-2 mr-2 overflow-hidden">{item.title}</div>
</div>
<ListItemMetadata item={item} hideDate={hideDate} sortBy={sortBy} />
<ListItemTags hideTags={hideTags} tags={tags} />
<ListItemConflictIndicator item={item} />
</div>
</div> </div>
) : ( <div
<div className="pr-4" /> className={classNames(
)} 'border-t-[1px] border-solid border-border p-3 text-xs font-bold',
<div className="min-w-0 flex-grow border-b border-solid border-border py-4 px-0"> selected ? 'bg-info text-info-contrast' : 'bg-passive-4 text-neutral',
<div className="flex items-start justify-between overflow-hidden text-base font-semibold leading-[1.3]"> )}
<div className="break-word mr-2">{item.title}</div> >
{formatSizeToReadableString(item.decryptedSize)}
</div> </div>
<ListItemMetadata item={item} hideDate={hideDate} sortBy={sortBy} />
<ListItemTags hideTags={hideTags} tags={tags} />
<ListItemConflictIndicator item={item} />
</div> </div>
<ListItemFlagIcons item={item} />
</div> </div>
) )
} }

View File

@@ -11,11 +11,13 @@ type Props = {
starred: ListableContentItem['starred'] starred: ListableContentItem['starred']
} }
hasFiles?: boolean hasFiles?: boolean
hasBorder?: boolean
className?: string
} }
const ListItemFlagIcons: FunctionComponent<Props> = ({ item, hasFiles = false }) => { const ListItemFlagIcons: FunctionComponent<Props> = ({ item, hasFiles = false, hasBorder = true, className }) => {
return ( return (
<div className="flex items-start border-b border-solid border-border p-4 pl-0"> <div className={`flex items-start ${hasBorder && 'border-b border-solid border-border'} ${className} pl-0`}>
{item.locked && ( {item.locked && (
<span className="flex items-center" title="Editing Disabled"> <span className="flex items-center" title="Editing Disabled">
<Icon ariaLabel="Editing Disabled" type="pencil-off" className="text-info" size="medium" /> <Icon ariaLabel="Editing Disabled" type="pencil-off" className="text-info" size="medium" />

View File

@@ -1,9 +1,8 @@
import { sanitizeHtmlString } from '@standardnotes/snjs' import { sanitizeHtmlString, SNNote } from '@standardnotes/snjs'
import { FunctionComponent } from 'react' import { FunctionComponent } from 'react'
import { ListableContentItem } from './Types/ListableContentItem'
type Props = { type Props = {
item: ListableContentItem item: SNNote
hidePreview: boolean hidePreview: boolean
lineLimit?: number lineLimit?: number
} }

View File

@@ -1,10 +1,11 @@
import { FunctionComponent } from 'react' import { FunctionComponent } from 'react'
import Icon from '@/Components/Icon/Icon' import Icon from '@/Components/Icon/Icon'
import { DisplayableListItemProps } from './Types/DisplayableListItemProps' import { DisplayableListItemProps } from './Types/DisplayableListItemProps'
import { ListableContentItem } from './Types/ListableContentItem'
type Props = { type Props = {
hideTags: boolean hideTags: boolean
tags: DisplayableListItemProps['tags'] tags: DisplayableListItemProps<ListableContentItem>['tags']
} }
const ListItemTags: FunctionComponent<Props> = ({ hideTags, tags }) => { const ListItemTags: FunctionComponent<Props> = ({ hideTags, tags }) => {

View File

@@ -14,8 +14,9 @@ import { useContextMenuEvent } from '@/Hooks/useContextMenuEvent'
import ListItemNotePreviewText from './ListItemNotePreviewText' import ListItemNotePreviewText from './ListItemNotePreviewText'
import { ListItemTitle } from './ListItemTitle' import { ListItemTitle } from './ListItemTitle'
import { log, LoggingDomain } from '@/Logging' import { log, LoggingDomain } from '@/Logging'
import { classNames } from '@/Utils/ConcatenateClassNames'
const NoteListItem: FunctionComponent<DisplayableListItemProps> = ({ const NoteListItem: FunctionComponent<DisplayableListItemProps<SNNote>> = ({
application, application,
notesController, notesController,
onSelect, onSelect,
@@ -27,6 +28,8 @@ const NoteListItem: FunctionComponent<DisplayableListItemProps> = ({
selected, selected,
sortBy, sortBy,
tags, tags,
isPreviousItemTiled,
isNextItemTiled,
}) => { }) => {
const { toggleAppPane } = useResponsiveAppPane() const { toggleAppPane } = useResponsiveAppPane()
@@ -73,12 +76,17 @@ const NoteListItem: FunctionComponent<DisplayableListItemProps> = ({
log(LoggingDomain.ItemsList, 'Rendering note list item', item.title) log(LoggingDomain.ItemsList, 'Rendering note list item', item.title)
const hasOffsetBorder = !isNextItemTiled
return ( return (
<div <div
ref={listItemRef} ref={listItemRef}
className={`content-list-item flex w-full cursor-pointer items-stretch text-text ${ className={classNames(
selected && `selected border-l-2 border-solid border-accessory-tint-${tint}` 'content-list-item text-tex flex w-full cursor-pointer items-stretch',
}`} selected && `selected border-l-2 border-solid border-accessory-tint-${tint}`,
isPreviousItemTiled && 'mt-3 border-t border-solid border-t-border',
isNextItemTiled && 'mb-3 border-b border-solid border-b-border',
)}
id={item.uuid} id={item.uuid}
onClick={onClick} onClick={onClick}
> >
@@ -89,14 +97,14 @@ const NoteListItem: FunctionComponent<DisplayableListItemProps> = ({
) : ( ) : (
<div className="pr-4" /> <div className="pr-4" />
)} )}
<div className="min-w-0 flex-grow border-b border-solid border-border py-4 px-0"> <div className={`min-w-0 flex-grow ${hasOffsetBorder && 'border-b border-solid border-border'} py-4 px-0`}>
<ListItemTitle item={item} /> <ListItemTitle item={item} />
<ListItemNotePreviewText item={item} hidePreview={hidePreview} /> <ListItemNotePreviewText item={item} hidePreview={hidePreview} />
<ListItemMetadata item={item} hideDate={hideDate} sortBy={sortBy} /> <ListItemMetadata item={item} hideDate={hideDate} sortBy={sortBy} />
<ListItemTags hideTags={hideTags} tags={tags} /> <ListItemTags hideTags={hideTags} tags={tags} />
<ListItemConflictIndicator item={item} /> <ListItemConflictIndicator item={item} />
</div> </div>
<ListItemFlagIcons item={item} hasFiles={hasFiles} /> <ListItemFlagIcons className="p-4" item={item} hasFiles={hasFiles} hasBorder={hasOffsetBorder} />
</div> </div>
) )
} }

View File

@@ -4,7 +4,9 @@ import { NotesController } from '@/Controllers/NotesController'
import { SortableItem, SNTag, Uuids } from '@standardnotes/snjs' import { SortableItem, SNTag, Uuids } from '@standardnotes/snjs'
import { ListableContentItem } from './ListableContentItem' import { ListableContentItem } from './ListableContentItem'
export type AbstractListItemProps = { type KeysOfUnion<T> = T extends T ? keyof T : never
export type AbstractListItemProps<I extends ListableContentItem> = {
application: WebApplication application: WebApplication
filesController: FilesController filesController: FilesController
notesController: NotesController notesController: NotesController
@@ -13,14 +15,19 @@ export type AbstractListItemProps = {
hideIcon: boolean hideIcon: boolean
hideTags: boolean hideTags: boolean
hidePreview: boolean hidePreview: boolean
item: ListableContentItem item: I
selected: boolean selected: boolean
sortBy: keyof SortableItem | undefined sortBy: keyof SortableItem | undefined
tags: SNTag[] tags: SNTag[]
isPreviousItemTiled?: boolean
isNextItemTiled?: boolean
} }
export function doListItemPropsMeritRerender(previous: AbstractListItemProps, next: AbstractListItemProps): boolean { export function doListItemPropsMeritRerender(
const simpleComparison: (keyof AbstractListItemProps)[] = [ previous: AbstractListItemProps<ListableContentItem>,
next: AbstractListItemProps<ListableContentItem>,
): boolean {
const simpleComparison: (keyof AbstractListItemProps<ListableContentItem>)[] = [
'onSelect', 'onSelect',
'hideDate', 'hideDate',
'hideIcon', 'hideIcon',
@@ -28,6 +35,8 @@ export function doListItemPropsMeritRerender(previous: AbstractListItemProps, ne
'hidePreview', 'hidePreview',
'selected', 'selected',
'sortBy', 'sortBy',
'isPreviousItemTiled',
'isNextItemTiled',
] ]
for (const key of simpleComparison) { for (const key of simpleComparison) {
@@ -83,7 +92,7 @@ function doesItemChangeMeritRerender(previous: ListableContentItem, next: Listab
return true return true
} }
const propertiesMeritingRerender: (keyof ListableContentItem)[] = [ const propertiesMeritingRerender: KeysOfUnion<ListableContentItem>[] = [
'title', 'title',
'protected', 'protected',
'updatedAtString', 'updatedAtString',
@@ -97,7 +106,7 @@ function doesItemChangeMeritRerender(previous: ListableContentItem, next: Listab
] ]
for (const key of propertiesMeritingRerender) { for (const key of propertiesMeritingRerender) {
if (previous[key] !== next[key]) { if (previous[key as keyof ListableContentItem] !== next[key as keyof ListableContentItem]) {
return true return true
} }
} }

View File

@@ -1,7 +1,8 @@
import { SNTag } from '@standardnotes/snjs' import { SNTag } from '@standardnotes/snjs'
import { AbstractListItemProps } from './AbstractListItemProps' import { AbstractListItemProps } from './AbstractListItemProps'
import { ListableContentItem } from './ListableContentItem'
export type DisplayableListItemProps = AbstractListItemProps & { export type DisplayableListItemProps<I extends ListableContentItem> = AbstractListItemProps<I> & {
tags: { tags: {
uuid: SNTag['uuid'] uuid: SNTag['uuid']
title: SNTag['title'] title: SNTag['title']

View File

@@ -1,12 +1,3 @@
import { DecryptedItem, ItemContent } from '@standardnotes/snjs' import { FileItem, SNNote } from '@standardnotes/snjs'
export type ListableContentItem = DecryptedItem<ItemContent> & { export type ListableContentItem = SNNote | FileItem
title: string
protected: boolean
updatedAtString?: string
createdAtString?: string
hidePreview?: boolean
preview_html?: string
preview_plain?: string
text?: string
}

View File

@@ -22,6 +22,7 @@ import {
useBoolean, useBoolean,
TemplateNoteViewAutofocusBehavior, TemplateNoteViewAutofocusBehavior,
isTag, isTag,
isFile,
} from '@standardnotes/snjs' } from '@standardnotes/snjs'
import { action, computed, makeObservable, observable, reaction, runInAction } from 'mobx' import { action, computed, makeObservable, observable, reaction, runInAction } from 'mobx'
import { WebApplication } from '../../Application/Application' import { WebApplication } from '../../Application/Application'
@@ -37,6 +38,7 @@ import dayjs from 'dayjs'
import { LinkingController } from '../LinkingController' import { LinkingController } from '../LinkingController'
import { AbstractViewController } from '../Abstract/AbstractViewController' import { AbstractViewController } from '../Abstract/AbstractViewController'
import { Persistable } from '../Abstract/Persistable' import { Persistable } from '../Abstract/Persistable'
import { log, LoggingDomain } from '@/Logging'
const MinNoteCellHeight = 51.0 const MinNoteCellHeight = 51.0
const DefaultListNumNotes = 20 const DefaultListNumNotes = 20
@@ -380,11 +382,42 @@ export class ItemListController
) )
} }
/**
* In some cases we want to keep the selected item open even if it doesn't appear in results,
* for example if you are inside tag Foo and remove tag Foo from the note, we want to keep the note open.
*/
private shouldCloseActiveItem = (activeItem: SNNote | FileItem | undefined) => { private shouldCloseActiveItem = (activeItem: SNNote | FileItem | undefined) => {
const isSearching = this.noteFilterText.length > 0 const activeItemExistsInUpdatedResults = this.items.find((item) => item.uuid === activeItem?.uuid)
const itemExistsInUpdatedResults = this.items.find((item) => item.uuid === activeItem?.uuid)
return !itemExistsInUpdatedResults && !isSearching && this.navigationController.isInAnySystemView() const closeBecauseActiveItemIsFileAndDoesntExistInUpdatedResults =
activeItem && isFile(activeItem) && !activeItemExistsInUpdatedResults
if (closeBecauseActiveItemIsFileAndDoesntExistInUpdatedResults) {
log(LoggingDomain.Selection, 'shouldCloseActiveItem closeBecauseActiveItemIsFileAndDoesntExistInUpdatedResults')
return true
}
const firstItemInNewResults = this.getFirstNonProtectedItem()
const closePreviousItemWhenSwitchingToFilesBasedView =
firstItemInNewResults && isFile(firstItemInNewResults) && !activeItemExistsInUpdatedResults
if (closePreviousItemWhenSwitchingToFilesBasedView) {
log(LoggingDomain.Selection, 'shouldCloseActiveItem closePreviousItemWhenSwitchingToFilesBasedView')
return true
}
const isSearching = this.noteFilterText.length > 0
const closeBecauseActiveItemDoesntExistInCurrentSystemView =
!activeItemExistsInUpdatedResults && !isSearching && this.navigationController.isInAnySystemView()
if (closeBecauseActiveItemDoesntExistInCurrentSystemView) {
log(LoggingDomain.Selection, 'shouldCloseActiveItem closePreviousItemWhenSwitchingToFilesBasedView')
return true
}
return false
} }
private shouldSelectNextItemOrCreateNewNote = (activeItem: SNNote | FileItem | undefined) => { private shouldSelectNextItemOrCreateNewNote = (activeItem: SNNote | FileItem | undefined) => {
@@ -404,6 +437,11 @@ export class ItemListController
} }
private shouldSelectFirstItem = (itemsReloadSource: ItemsReloadSource) => { private shouldSelectFirstItem = (itemsReloadSource: ItemsReloadSource) => {
const item = this.getFirstNonProtectedItem()
if (item && isFile(item)) {
return false
}
const selectedTag = this.navigationController.selected const selectedTag = this.navigationController.selected
const isDailyEntry = selectedTag && isTag(selectedTag) && selectedTag.isDailyEntry const isDailyEntry = selectedTag && isTag(selectedTag) && selectedTag.isDailyEntry
if (isDailyEntry) { if (isDailyEntry) {
@@ -424,10 +462,17 @@ export class ItemListController
const activeItem = activeController?.item const activeItem = activeController?.item
if (this.shouldCloseActiveItem(activeItem) && activeController) { if (activeController && activeItem && this.shouldCloseActiveItem(activeItem)) {
this.closeItemController(activeController) this.closeItemController(activeController)
this.selectionController.selectNextItem()
this.selectionController.deselectItem(activeItem)
if (this.shouldSelectFirstItem(itemsReloadSource)) {
log(LoggingDomain.Selection, 'Selecting next item after closing active one')
this.selectionController.selectNextItem()
}
} else if (this.shouldSelectActiveItem(activeItem) && activeItem) { } else if (this.shouldSelectActiveItem(activeItem) && activeItem) {
log(LoggingDomain.Selection, 'Selecting active item')
await this.selectionController.selectItem(activeItem.uuid).catch(console.error) await this.selectionController.selectItem(activeItem.uuid).catch(console.error)
} else if (this.shouldSelectFirstItem(itemsReloadSource)) { } else if (this.shouldSelectFirstItem(itemsReloadSource)) {
await this.selectFirstItem() await this.selectFirstItem()
@@ -547,9 +592,11 @@ export class ItemListController
this.displayOptions = newDisplayOptions this.displayOptions = newDisplayOptions
this.webDisplayOptions = newWebDisplayOptions this.webDisplayOptions = newWebDisplayOptions
const newWidth = this.application.getPreference(PrefKey.NotesPanelWidth) const listColumnWidth =
if (newWidth && newWidth !== this.panelWidth) { selectedTag?.preferences?.panelWidth || this.application.getPreference(PrefKey.NotesPanelWidth)
this.panelWidth = newWidth
if (listColumnWidth && listColumnWidth !== this.panelWidth) {
this.panelWidth = listColumnWidth
} }
if (!displayOptionsChanged) { if (!displayOptionsChanged) {
@@ -560,7 +607,10 @@ export class ItemListController
await this.reloadItems(ItemsReloadSource.DisplayOptionsChange) await this.reloadItems(ItemsReloadSource.DisplayOptionsChange)
if (newDisplayOptions.sortBy !== currentSortBy) { if (
newDisplayOptions.sortBy !== currentSortBy &&
this.shouldSelectFirstItem(ItemsReloadSource.DisplayOptionsChange)
) {
await this.selectFirstItem() await this.selectFirstItem()
} }
@@ -689,6 +739,8 @@ export class ItemListController
const item = this.getFirstNonProtectedItem() const item = this.getFirstNonProtectedItem()
if (item) { if (item) {
log(LoggingDomain.Selection, 'Selecting first item', item.uuid)
await this.selectionController.selectItemWithScrollHandling(item, { await this.selectionController.selectItemWithScrollHandling(item, {
userTriggered: false, userTriggered: false,
scrollIntoView: false, scrollIntoView: false,
@@ -702,6 +754,7 @@ export class ItemListController
const item = this.getFirstNonProtectedItem() const item = this.getFirstNonProtectedItem()
if (item) { if (item) {
log(LoggingDomain.Selection, 'selectNextItemOrCreateNewNote')
await this.selectionController await this.selectionController
.selectItemWithScrollHandling(item, { .selectItemWithScrollHandling(item, {
userTriggered: false, userTriggered: false,
@@ -746,6 +799,7 @@ export class ItemListController
} }
private closeItemController(controller: NoteViewController | FileViewController): void { private closeItemController(controller: NoteViewController | FileViewController): void {
log(LoggingDomain.Selection, 'Closing item controller', controller.runtimeId)
this.application.itemControllerGroup.closeItemController(controller) this.application.itemControllerGroup.closeItemController(controller)
} }

View File

@@ -431,6 +431,15 @@ export class NavigationController
return this.selected_ return this.selected_
} }
public async setPanelWidthForTag(tag: SNTag, width: number): Promise<void> {
await this.application.mutator.changeAndSaveItem<TagMutator>(tag, (mutator) => {
mutator.preferences = {
...mutator.preferences,
panelWidth: width,
}
})
}
public async setSelectedTag(tag: AnyTag | undefined, { userTriggered } = { userTriggered: false }) { public async setSelectedTag(tag: AnyTag | undefined, { userTriggered } = { userTriggered: false }) {
if (tag && tag.conflictOf) { if (tag && tag.conflictOf) {
this.application.mutator this.application.mutator

View File

@@ -1,4 +1,5 @@
import { ListableContentItem } from '@/Components/ContentListView/Types/ListableContentItem' import { ListableContentItem } from '@/Components/ContentListView/Types/ListableContentItem'
import { log, LoggingDomain } from '@/Logging'
import { import {
ChallengeReason, ChallengeReason,
ContentType, ContentType,
@@ -8,6 +9,7 @@ import {
UuidString, UuidString,
InternalEventBus, InternalEventBus,
isFile, isFile,
Uuids,
} from '@standardnotes/snjs' } from '@standardnotes/snjs'
import { action, computed, makeObservable, observable, reaction, runInAction } from 'mobx' import { action, computed, makeObservable, observable, reaction, runInAction } from 'mobx'
import { WebApplication } from '../Application/Application' import { WebApplication } from '../Application/Application'
@@ -248,12 +250,15 @@ export class SelectedItemsController
didSelect: boolean didSelect: boolean
}> => { }> => {
const item = this.application.items.findItem<ListableContentItem>(uuid) const item = this.application.items.findItem<ListableContentItem>(uuid)
if (!item) { if (!item) {
return { return {
didSelect: false, didSelect: false,
} }
} }
log(LoggingDomain.Selection, 'selectItem', item.uuid)
const hasMeta = this.io.activeModifiers.has(KeyboardModifier.Meta) const hasMeta = this.io.activeModifiers.has(KeyboardModifier.Meta)
const hasCtrl = this.io.activeModifiers.has(KeyboardModifier.Ctrl) const hasCtrl = this.io.activeModifiers.has(KeyboardModifier.Ctrl)
const hasShift = this.io.activeModifiers.has(KeyboardModifier.Shift) const hasShift = this.io.activeModifiers.has(KeyboardModifier.Shift)
@@ -304,14 +309,18 @@ export class SelectedItemsController
} }
selectUuids = async (uuids: UuidString[], userTriggered = false) => { selectUuids = async (uuids: UuidString[], userTriggered = false) => {
const itemsForUuids = this.application.items.findItems(uuids) const itemsForUuids = this.application.items.findItems(uuids).filter((item) => !isFile(item))
if (itemsForUuids.length < 1) { if (itemsForUuids.length < 1) {
return return
} }
if (!userTriggered && itemsForUuids.some((item) => item.protected && isFile(item))) { if (!userTriggered && itemsForUuids.some((item) => item.protected && isFile(item))) {
return return
} }
this.setSelectedUuids(new Set(uuids))
this.setSelectedUuids(new Set(Uuids(itemsForUuids)))
if (itemsForUuids.length === 1) { if (itemsForUuids.length === 1) {
void this.openSingleSelectedItem() void this.openSingleSelectedItem()
} }

View File

@@ -7,6 +7,7 @@ export enum LoggingDomain {
ItemsList, ItemsList,
NavigationList, NavigationList,
Viewport, Viewport,
Selection,
} }
const LoggingStatus: Record<LoggingDomain, boolean> = { const LoggingStatus: Record<LoggingDomain, boolean> = {
@@ -14,7 +15,8 @@ const LoggingStatus: Record<LoggingDomain, boolean> = {
[LoggingDomain.NoteView]: false, [LoggingDomain.NoteView]: false,
[LoggingDomain.ItemsList]: false, [LoggingDomain.ItemsList]: false,
[LoggingDomain.NavigationList]: false, [LoggingDomain.NavigationList]: false,
[LoggingDomain.Viewport]: true, [LoggingDomain.Viewport]: false,
[LoggingDomain.Selection]: false,
} }
export function log(domain: LoggingDomain, ...args: any[]): void { export function log(domain: LoggingDomain, ...args: any[]): void {

View File

@@ -109,6 +109,12 @@
-webkit-line-clamp: 1; -webkit-line-clamp: 1;
} }
.line-clamp-2 {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
@mixin DimmedBackground($color, $opacity) { @mixin DimmedBackground($color, $opacity) {
content: ''; content: '';
width: 100%; width: 100%;