refactor: repo (#1070)
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { KeyboardKey } from '@/Services/IOService'
|
||||
import { ViewControllerManager } from '@/Services/ViewControllerManager'
|
||||
import { UuidString } from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent, KeyboardEventHandler, UIEventHandler, useCallback } from 'react'
|
||||
import { FOCUSABLE_BUT_NOT_TABBABLE, NOTES_LIST_SCROLL_THRESHOLD } from '@/Constants/Constants'
|
||||
import { ListableContentItem } from './Types/ListableContentItem'
|
||||
import ContentListItem from './ContentListItem'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
viewControllerManager: ViewControllerManager
|
||||
items: ListableContentItem[]
|
||||
selectedItems: Record<UuidString, ListableContentItem>
|
||||
paginate: () => void
|
||||
}
|
||||
|
||||
const ContentList: FunctionComponent<Props> = ({
|
||||
application,
|
||||
viewControllerManager,
|
||||
items,
|
||||
selectedItems,
|
||||
paginate,
|
||||
}) => {
|
||||
const { selectPreviousItem, selectNextItem } = viewControllerManager.itemListController
|
||||
const { hideTags, hideDate, hideNotePreview, hideEditorIcon } =
|
||||
viewControllerManager.itemListController.webDisplayOptions
|
||||
const { sortBy } = viewControllerManager.itemListController.displayOptions
|
||||
|
||||
const onScroll: UIEventHandler = useCallback(
|
||||
(e) => {
|
||||
const offset = NOTES_LIST_SCROLL_THRESHOLD
|
||||
const element = e.target as HTMLElement
|
||||
if (element.scrollTop + element.offsetHeight >= element.scrollHeight - offset) {
|
||||
paginate()
|
||||
}
|
||||
},
|
||||
[paginate],
|
||||
)
|
||||
|
||||
const onKeyDown: KeyboardEventHandler = useCallback(
|
||||
(e) => {
|
||||
if (e.key === KeyboardKey.Up) {
|
||||
e.preventDefault()
|
||||
selectPreviousItem()
|
||||
} else if (e.key === KeyboardKey.Down) {
|
||||
e.preventDefault()
|
||||
selectNextItem()
|
||||
}
|
||||
},
|
||||
[selectNextItem, selectPreviousItem],
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
className="infinite-scroll focus:shadow-none focus:outline-none"
|
||||
id="notes-scrollable"
|
||||
onScroll={onScroll}
|
||||
onKeyDown={onKeyDown}
|
||||
tabIndex={FOCUSABLE_BUT_NOT_TABBABLE}
|
||||
>
|
||||
{items.map((item) => (
|
||||
<ContentListItem
|
||||
key={item.uuid}
|
||||
application={application}
|
||||
item={item}
|
||||
selected={!!selectedItems[item.uuid]}
|
||||
hideDate={hideDate}
|
||||
hidePreview={hideNotePreview}
|
||||
hideTags={hideTags}
|
||||
hideIcon={hideEditorIcon}
|
||||
sortBy={sortBy}
|
||||
filesController={viewControllerManager.filesController}
|
||||
selectionController={viewControllerManager.selectionController}
|
||||
navigationController={viewControllerManager.navigationController}
|
||||
notesController={viewControllerManager.notesController}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(ContentList)
|
||||
@@ -0,0 +1,38 @@
|
||||
import { ContentType, SNTag } from '@standardnotes/snjs'
|
||||
import { FunctionComponent } from 'react'
|
||||
import FileListItem from './FileListItem'
|
||||
import NoteListItem from './NoteListItem'
|
||||
import { AbstractListItemProps } from './Types/AbstractListItemProps'
|
||||
|
||||
const ContentListItem: FunctionComponent<AbstractListItemProps> = (props) => {
|
||||
const getTags = () => {
|
||||
if (props.hideTags) {
|
||||
return []
|
||||
}
|
||||
|
||||
const selectedTag = props.navigationController.selected
|
||||
if (!selectedTag) {
|
||||
return []
|
||||
}
|
||||
|
||||
const tags = props.application.getItemTags(props.item)
|
||||
|
||||
const isNavigatingOnlyTag = selectedTag instanceof SNTag && tags.length === 1
|
||||
if (isNavigatingOnlyTag) {
|
||||
return []
|
||||
}
|
||||
|
||||
return tags
|
||||
}
|
||||
|
||||
switch (props.item.content_type) {
|
||||
case ContentType.Note:
|
||||
return <NoteListItem tags={getTags()} {...props} />
|
||||
case ContentType.File:
|
||||
return <FileListItem tags={getTags()} {...props} />
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export default ContentListItem
|
||||
@@ -0,0 +1,257 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { CollectionSort, CollectionSortProperty, PrefKey, SystemViewId } from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent, useCallback, useState } from 'react'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import Menu from '@/Components/Menu/Menu'
|
||||
import MenuItem from '@/Components/Menu/MenuItem'
|
||||
import MenuItemSeparator from '@/Components/Menu/MenuItemSeparator'
|
||||
import { MenuItemType } from '@/Components/Menu/MenuItemType'
|
||||
import { ViewControllerManager } from '@/Services/ViewControllerManager'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
viewControllerManager: ViewControllerManager
|
||||
closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void
|
||||
closeDisplayOptionsMenu: () => void
|
||||
isOpen: boolean
|
||||
}
|
||||
|
||||
const ContentListOptionsMenu: FunctionComponent<Props> = ({
|
||||
closeDisplayOptionsMenu,
|
||||
closeOnBlur,
|
||||
application,
|
||||
viewControllerManager,
|
||||
isOpen,
|
||||
}) => {
|
||||
const [sortBy, setSortBy] = useState(() => application.getPreference(PrefKey.SortNotesBy, CollectionSort.CreatedAt))
|
||||
const [sortReverse, setSortReverse] = useState(() => application.getPreference(PrefKey.SortNotesReverse, false))
|
||||
const [hidePreview, setHidePreview] = useState(() => application.getPreference(PrefKey.NotesHideNotePreview, false))
|
||||
const [hideDate, setHideDate] = useState(() => application.getPreference(PrefKey.NotesHideDate, false))
|
||||
const [hideTags, setHideTags] = useState(() => application.getPreference(PrefKey.NotesHideTags, true))
|
||||
const [hidePinned, setHidePinned] = useState(() => application.getPreference(PrefKey.NotesHidePinned, false))
|
||||
const [showArchived, setShowArchived] = useState(() => application.getPreference(PrefKey.NotesShowArchived, false))
|
||||
const [showTrashed, setShowTrashed] = useState(() => application.getPreference(PrefKey.NotesShowTrashed, false))
|
||||
const [hideProtected, setHideProtected] = useState(() => application.getPreference(PrefKey.NotesHideProtected, false))
|
||||
const [hideEditorIcon, setHideEditorIcon] = useState(() =>
|
||||
application.getPreference(PrefKey.NotesHideEditorIcon, false),
|
||||
)
|
||||
|
||||
const toggleSortReverse = useCallback(() => {
|
||||
application.setPreference(PrefKey.SortNotesReverse, !sortReverse).catch(console.error)
|
||||
setSortReverse(!sortReverse)
|
||||
}, [application, sortReverse])
|
||||
|
||||
const toggleSortBy = useCallback(
|
||||
(sort: CollectionSortProperty) => {
|
||||
if (sortBy === sort) {
|
||||
toggleSortReverse()
|
||||
} else {
|
||||
setSortBy(sort)
|
||||
application.setPreference(PrefKey.SortNotesBy, sort).catch(console.error)
|
||||
}
|
||||
},
|
||||
[application, sortBy, toggleSortReverse],
|
||||
)
|
||||
|
||||
const toggleSortByDateModified = useCallback(() => {
|
||||
toggleSortBy(CollectionSort.UpdatedAt)
|
||||
}, [toggleSortBy])
|
||||
|
||||
const toggleSortByCreationDate = useCallback(() => {
|
||||
toggleSortBy(CollectionSort.CreatedAt)
|
||||
}, [toggleSortBy])
|
||||
|
||||
const toggleSortByTitle = useCallback(() => {
|
||||
toggleSortBy(CollectionSort.Title)
|
||||
}, [toggleSortBy])
|
||||
|
||||
const toggleHidePreview = useCallback(() => {
|
||||
setHidePreview(!hidePreview)
|
||||
application.setPreference(PrefKey.NotesHideNotePreview, !hidePreview).catch(console.error)
|
||||
}, [application, hidePreview])
|
||||
|
||||
const toggleHideDate = useCallback(() => {
|
||||
setHideDate(!hideDate)
|
||||
application.setPreference(PrefKey.NotesHideDate, !hideDate).catch(console.error)
|
||||
}, [application, hideDate])
|
||||
|
||||
const toggleHideTags = useCallback(() => {
|
||||
setHideTags(!hideTags)
|
||||
application.setPreference(PrefKey.NotesHideTags, !hideTags).catch(console.error)
|
||||
}, [application, hideTags])
|
||||
|
||||
const toggleHidePinned = useCallback(() => {
|
||||
setHidePinned(!hidePinned)
|
||||
application.setPreference(PrefKey.NotesHidePinned, !hidePinned).catch(console.error)
|
||||
}, [application, hidePinned])
|
||||
|
||||
const toggleShowArchived = useCallback(() => {
|
||||
setShowArchived(!showArchived)
|
||||
application.setPreference(PrefKey.NotesShowArchived, !showArchived).catch(console.error)
|
||||
}, [application, showArchived])
|
||||
|
||||
const toggleShowTrashed = useCallback(() => {
|
||||
setShowTrashed(!showTrashed)
|
||||
application.setPreference(PrefKey.NotesShowTrashed, !showTrashed).catch(console.error)
|
||||
}, [application, showTrashed])
|
||||
|
||||
const toggleHideProtected = useCallback(() => {
|
||||
setHideProtected(!hideProtected)
|
||||
application.setPreference(PrefKey.NotesHideProtected, !hideProtected).catch(console.error)
|
||||
}, [application, hideProtected])
|
||||
|
||||
const toggleEditorIcon = useCallback(() => {
|
||||
setHideEditorIcon(!hideEditorIcon)
|
||||
application.setPreference(PrefKey.NotesHideEditorIcon, !hideEditorIcon).catch(console.error)
|
||||
}, [application, hideEditorIcon])
|
||||
|
||||
return (
|
||||
<Menu
|
||||
className={
|
||||
'sn-dropdown sn-dropdown--animated min-w-70 overflow-y-auto \
|
||||
border-1 border-solid border-main text-sm z-index-dropdown-menu \
|
||||
flex flex-col py-2 top-full left-2 absolute'
|
||||
}
|
||||
a11yLabel="Notes list options menu"
|
||||
closeMenu={closeDisplayOptionsMenu}
|
||||
isOpen={isOpen}
|
||||
>
|
||||
<div className="px-3 my-1 text-xs font-semibold color-text uppercase">Sort by</div>
|
||||
<MenuItem
|
||||
className="py-2"
|
||||
type={MenuItemType.RadioButton}
|
||||
onClick={toggleSortByDateModified}
|
||||
checked={sortBy === CollectionSort.UpdatedAt}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
<div className="flex flex-grow items-center justify-between ml-2">
|
||||
<span>Date modified</span>
|
||||
{sortBy === CollectionSort.UpdatedAt ? (
|
||||
sortReverse ? (
|
||||
<Icon type="arrows-sort-up" className="color-neutral" />
|
||||
) : (
|
||||
<Icon type="arrows-sort-down" className="color-neutral" />
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
className="py-2"
|
||||
type={MenuItemType.RadioButton}
|
||||
onClick={toggleSortByCreationDate}
|
||||
checked={sortBy === CollectionSort.CreatedAt}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
<div className="flex flex-grow items-center justify-between ml-2">
|
||||
<span>Creation date</span>
|
||||
{sortBy === CollectionSort.CreatedAt ? (
|
||||
sortReverse ? (
|
||||
<Icon type="arrows-sort-up" className="color-neutral" />
|
||||
) : (
|
||||
<Icon type="arrows-sort-down" className="color-neutral" />
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
className="py-2"
|
||||
type={MenuItemType.RadioButton}
|
||||
onClick={toggleSortByTitle}
|
||||
checked={sortBy === CollectionSort.Title}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
<div className="flex flex-grow items-center justify-between ml-2">
|
||||
<span>Title</span>
|
||||
{sortBy === CollectionSort.Title ? (
|
||||
sortReverse ? (
|
||||
<Icon type="arrows-sort-up" className="color-neutral" />
|
||||
) : (
|
||||
<Icon type="arrows-sort-down" className="color-neutral" />
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
</MenuItem>
|
||||
<MenuItemSeparator />
|
||||
<div className="px-3 py-1 text-xs font-semibold color-text uppercase">View</div>
|
||||
{viewControllerManager.navigationController.selectedUuid !== SystemViewId.Files && (
|
||||
<MenuItem
|
||||
type={MenuItemType.SwitchButton}
|
||||
className="py-1 hover:bg-contrast focus:bg-info-backdrop"
|
||||
checked={!hidePreview}
|
||||
onChange={toggleHidePreview}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
<div className="flex flex-col max-w-3/4">Show note preview</div>
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem
|
||||
type={MenuItemType.SwitchButton}
|
||||
className="py-1 hover:bg-contrast focus:bg-info-backdrop"
|
||||
checked={!hideDate}
|
||||
onChange={toggleHideDate}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
Show date
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
type={MenuItemType.SwitchButton}
|
||||
className="py-1 hover:bg-contrast focus:bg-info-backdrop"
|
||||
checked={!hideTags}
|
||||
onChange={toggleHideTags}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
Show tags
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
type={MenuItemType.SwitchButton}
|
||||
className="py-1 hover:bg-contrast focus:bg-info-backdrop"
|
||||
checked={!hideEditorIcon}
|
||||
onChange={toggleEditorIcon}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
Show icon
|
||||
</MenuItem>
|
||||
<div className="h-1px my-2 bg-border"></div>
|
||||
<div className="px-3 py-1 text-xs font-semibold color-text uppercase">Other</div>
|
||||
<MenuItem
|
||||
type={MenuItemType.SwitchButton}
|
||||
className="py-1 hover:bg-contrast focus:bg-info-backdrop"
|
||||
checked={!hidePinned}
|
||||
onChange={toggleHidePinned}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
Show pinned
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
type={MenuItemType.SwitchButton}
|
||||
className="py-1 hover:bg-contrast focus:bg-info-backdrop"
|
||||
checked={!hideProtected}
|
||||
onChange={toggleHideProtected}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
Show protected
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
type={MenuItemType.SwitchButton}
|
||||
className="py-1 hover:bg-contrast focus:bg-info-backdrop"
|
||||
checked={showArchived}
|
||||
onChange={toggleShowArchived}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
Show archived
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
type={MenuItemType.SwitchButton}
|
||||
className="py-1 hover:bg-contrast focus:bg-info-backdrop"
|
||||
checked={showTrashed}
|
||||
onChange={toggleShowTrashed}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
Show trashed
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(ContentListOptionsMenu)
|
||||
@@ -0,0 +1,279 @@
|
||||
import { KeyboardKey, KeyboardModifier } from '@/Services/IOService'
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { ViewControllerManager } from '@/Services/ViewControllerManager'
|
||||
import { PANEL_NAME_NOTES } from '@/Constants/Constants'
|
||||
import { PrefKey, SystemViewId } from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import {
|
||||
ChangeEventHandler,
|
||||
FunctionComponent,
|
||||
KeyboardEventHandler,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import ContentList from '@/Components/ContentListView/ContentList'
|
||||
import NoAccountWarningWrapper from '@/Components/NoAccountWarning/NoAccountWarning'
|
||||
import SearchOptions from '@/Components/SearchOptions/SearchOptions'
|
||||
import PanelResizer, { PanelSide, ResizeFinishCallback, PanelResizeType } from '@/Components/PanelResizer/PanelResizer'
|
||||
import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure'
|
||||
import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur'
|
||||
import ContentListOptionsMenu from './ContentListOptionsMenu'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
viewControllerManager: ViewControllerManager
|
||||
}
|
||||
|
||||
const ContentListView: FunctionComponent<Props> = ({ application, viewControllerManager }) => {
|
||||
const itemsViewPanelRef = useRef<HTMLDivElement>(null)
|
||||
const displayOptionsMenuRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const {
|
||||
completedFullSync,
|
||||
noteFilterText,
|
||||
optionsSubtitle,
|
||||
panelTitle,
|
||||
renderedItems,
|
||||
setNoteFilterText,
|
||||
searchBarElement,
|
||||
selectNextItem,
|
||||
selectPreviousItem,
|
||||
onFilterEnter,
|
||||
clearFilterText,
|
||||
paginate,
|
||||
panelWidth,
|
||||
createNewNote,
|
||||
} = viewControllerManager.itemListController
|
||||
|
||||
const { selectedItems } = viewControllerManager.selectionController
|
||||
|
||||
const [showDisplayOptionsMenu, setShowDisplayOptionsMenu] = useState(false)
|
||||
const [focusedSearch, setFocusedSearch] = useState(false)
|
||||
|
||||
const [closeDisplayOptMenuOnBlur] = useCloseOnBlur(displayOptionsMenuRef, setShowDisplayOptionsMenu)
|
||||
|
||||
const isFilesSmartView = useMemo(
|
||||
() => viewControllerManager.navigationController.selected?.uuid === SystemViewId.Files,
|
||||
[viewControllerManager.navigationController.selected?.uuid],
|
||||
)
|
||||
|
||||
const addNewItem = useCallback(() => {
|
||||
if (isFilesSmartView) {
|
||||
void viewControllerManager.filesController.uploadNewFile()
|
||||
} else {
|
||||
void createNewNote()
|
||||
}
|
||||
}, [viewControllerManager.filesController, createNewNote, isFilesSmartView])
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
* In the browser we're not allowed to override cmd/ctrl + n, so we have to
|
||||
* use Control modifier as well. These rules don't apply to desktop, but
|
||||
* probably better to be consistent.
|
||||
*/
|
||||
const newNoteKeyObserver = application.io.addKeyObserver({
|
||||
key: 'n',
|
||||
modifiers: [KeyboardModifier.Meta, KeyboardModifier.Ctrl],
|
||||
onKeyDown: (event) => {
|
||||
event.preventDefault()
|
||||
addNewItem()
|
||||
},
|
||||
})
|
||||
|
||||
const nextNoteKeyObserver = application.io.addKeyObserver({
|
||||
key: KeyboardKey.Down,
|
||||
elements: [document.body, ...(searchBarElement ? [searchBarElement] : [])],
|
||||
onKeyDown: () => {
|
||||
if (searchBarElement === document.activeElement) {
|
||||
searchBarElement?.blur()
|
||||
}
|
||||
selectNextItem()
|
||||
},
|
||||
})
|
||||
|
||||
const previousNoteKeyObserver = application.io.addKeyObserver({
|
||||
key: KeyboardKey.Up,
|
||||
element: document.body,
|
||||
onKeyDown: () => {
|
||||
selectPreviousItem()
|
||||
},
|
||||
})
|
||||
|
||||
const searchKeyObserver = application.io.addKeyObserver({
|
||||
key: 'f',
|
||||
modifiers: [KeyboardModifier.Meta, KeyboardModifier.Shift],
|
||||
onKeyDown: () => {
|
||||
if (searchBarElement) {
|
||||
searchBarElement.focus()
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return () => {
|
||||
newNoteKeyObserver()
|
||||
nextNoteKeyObserver()
|
||||
previousNoteKeyObserver()
|
||||
searchKeyObserver()
|
||||
}
|
||||
}, [addNewItem, application.io, createNewNote, searchBarElement, selectNextItem, selectPreviousItem])
|
||||
|
||||
const onNoteFilterTextChange: ChangeEventHandler<HTMLInputElement> = useCallback(
|
||||
(e) => {
|
||||
setNoteFilterText(e.target.value)
|
||||
},
|
||||
[setNoteFilterText],
|
||||
)
|
||||
|
||||
const onSearchFocused = useCallback(() => setFocusedSearch(true), [])
|
||||
const onSearchBlurred = useCallback(() => setFocusedSearch(false), [])
|
||||
|
||||
const onNoteFilterKeyUp: KeyboardEventHandler = useCallback(
|
||||
(e) => {
|
||||
if (e.key === KeyboardKey.Enter) {
|
||||
onFilterEnter()
|
||||
}
|
||||
},
|
||||
[onFilterEnter],
|
||||
)
|
||||
|
||||
const panelResizeFinishCallback: ResizeFinishCallback = useCallback(
|
||||
(width, _lastLeft, _isMaxWidth, isCollapsed) => {
|
||||
application.setPreference(PrefKey.NotesPanelWidth, width).catch(console.error)
|
||||
viewControllerManager.noteTagsController.reloadTagsContainerMaxWidth()
|
||||
application.publishPanelDidResizeEvent(PANEL_NAME_NOTES, isCollapsed)
|
||||
},
|
||||
[viewControllerManager, application],
|
||||
)
|
||||
|
||||
const panelWidthEventCallback = useCallback(() => {
|
||||
viewControllerManager.noteTagsController.reloadTagsContainerMaxWidth()
|
||||
}, [viewControllerManager])
|
||||
|
||||
const toggleDisplayOptionsMenu = useCallback(() => {
|
||||
setShowDisplayOptionsMenu(!showDisplayOptionsMenu)
|
||||
}, [showDisplayOptionsMenu])
|
||||
|
||||
const addButtonLabel = useMemo(
|
||||
() => (isFilesSmartView ? 'Upload file' : 'Create a new note in the selected tag'),
|
||||
[isFilesSmartView],
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
id="items-column"
|
||||
className="sn-component section app-column app-column-second"
|
||||
aria-label={'Notes & Files'}
|
||||
ref={itemsViewPanelRef}
|
||||
>
|
||||
<div className="content">
|
||||
<div id="items-title-bar" className="section-title-bar">
|
||||
<div id="items-title-bar-container">
|
||||
<div className="section-title-bar-header">
|
||||
<div className="sk-h2 font-semibold title">{panelTitle}</div>
|
||||
<button
|
||||
className="flex items-center px-5 py-1 bg-contrast hover:brightness-130 color-text border-0 cursor-pointer"
|
||||
title={addButtonLabel}
|
||||
aria-label={addButtonLabel}
|
||||
onClick={addNewItem}
|
||||
>
|
||||
<Icon type="add" className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="filter-section" role="search">
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
id="search-bar"
|
||||
className="filter-bar"
|
||||
placeholder="Search"
|
||||
title="Searches notes and files in the currently selected tag"
|
||||
value={noteFilterText}
|
||||
onChange={onNoteFilterTextChange}
|
||||
onKeyUp={onNoteFilterKeyUp}
|
||||
onFocus={onSearchFocused}
|
||||
onBlur={onSearchBlurred}
|
||||
autoComplete="off"
|
||||
/>
|
||||
{noteFilterText && (
|
||||
<button onClick={clearFilterText} id="search-clear-button">
|
||||
✕
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{(focusedSearch || noteFilterText) && (
|
||||
<div className="animate-fade-from-top">
|
||||
<SearchOptions application={application} viewControllerManager={viewControllerManager} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<NoAccountWarningWrapper viewControllerManager={viewControllerManager} />
|
||||
</div>
|
||||
<div id="items-menu-bar" className="sn-component" ref={displayOptionsMenuRef}>
|
||||
<div className="sk-app-bar no-edges">
|
||||
<div className="left">
|
||||
<Disclosure open={showDisplayOptionsMenu} onChange={toggleDisplayOptionsMenu}>
|
||||
<DisclosureButton
|
||||
className={`sk-app-bar-item bg-contrast color-text border-0 focus:shadow-none ${
|
||||
showDisplayOptionsMenu ? 'selected' : ''
|
||||
}`}
|
||||
onBlur={closeDisplayOptMenuOnBlur}
|
||||
>
|
||||
<div className="sk-app-bar-item-column">
|
||||
<div className="sk-label">Options</div>
|
||||
</div>
|
||||
<div className="sk-app-bar-item-column">
|
||||
<div className="sk-sublabel">{optionsSubtitle}</div>
|
||||
</div>
|
||||
</DisclosureButton>
|
||||
<DisclosurePanel onBlur={closeDisplayOptMenuOnBlur}>
|
||||
{showDisplayOptionsMenu && (
|
||||
<ContentListOptionsMenu
|
||||
application={application}
|
||||
viewControllerManager={viewControllerManager}
|
||||
closeDisplayOptionsMenu={toggleDisplayOptionsMenu}
|
||||
closeOnBlur={closeDisplayOptMenuOnBlur}
|
||||
isOpen={showDisplayOptionsMenu}
|
||||
/>
|
||||
)}
|
||||
</DisclosurePanel>
|
||||
</Disclosure>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{completedFullSync && !renderedItems.length ? <p className="empty-items-list faded">No items.</p> : null}
|
||||
{!completedFullSync && !renderedItems.length ? <p className="empty-items-list faded">Loading...</p> : null}
|
||||
{renderedItems.length ? (
|
||||
<ContentList
|
||||
items={renderedItems}
|
||||
selectedItems={selectedItems}
|
||||
application={application}
|
||||
viewControllerManager={viewControllerManager}
|
||||
paginate={paginate}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
{itemsViewPanelRef.current && (
|
||||
<PanelResizer
|
||||
collapsable={true}
|
||||
hoverable={true}
|
||||
defaultWidth={300}
|
||||
panel={itemsViewPanelRef.current}
|
||||
side={PanelSide.Right}
|
||||
type={PanelResizeType.WidthOnly}
|
||||
resizeFinishCallback={panelResizeFinishCallback}
|
||||
widthEventCallback={panelWidthEventCallback}
|
||||
width={panelWidth}
|
||||
left={0}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(ContentListView)
|
||||
@@ -0,0 +1,86 @@
|
||||
import { FileItem } from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent, useCallback } from 'react'
|
||||
import { getFileIconComponent } from '../AttachedFilesPopover/getFileIconComponent'
|
||||
import ListItemConflictIndicator from './ListItemConflictIndicator'
|
||||
import ListItemFlagIcons from './ListItemFlagIcons'
|
||||
import ListItemTags from './ListItemTags'
|
||||
import ListItemMetadata from './ListItemMetadata'
|
||||
import { DisplayableListItemProps } from './Types/DisplayableListItemProps'
|
||||
|
||||
const FileListItem: FunctionComponent<DisplayableListItemProps> = ({
|
||||
application,
|
||||
filesController,
|
||||
selectionController,
|
||||
hideDate,
|
||||
hideIcon,
|
||||
hideTags,
|
||||
item,
|
||||
selected,
|
||||
sortBy,
|
||||
tags,
|
||||
}) => {
|
||||
const openFileContextMenu = useCallback(
|
||||
(posX: number, posY: number) => {
|
||||
filesController.setFileContextMenuLocation({
|
||||
x: posX,
|
||||
y: posY,
|
||||
})
|
||||
filesController.setShowFileContextMenu(true)
|
||||
},
|
||||
[filesController],
|
||||
)
|
||||
|
||||
const openContextMenu = useCallback(
|
||||
async (posX: number, posY: number) => {
|
||||
const { didSelect } = await selectionController.selectItem(item.uuid)
|
||||
if (didSelect) {
|
||||
openFileContextMenu(posX, posY)
|
||||
}
|
||||
},
|
||||
[selectionController, item.uuid, openFileContextMenu],
|
||||
)
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
void selectionController.selectItem(item.uuid, true)
|
||||
}, [item.uuid, selectionController])
|
||||
|
||||
const IconComponent = () =>
|
||||
getFileIconComponent(
|
||||
application.iconsController.getIconForFileType((item as FileItem).mimeType),
|
||||
'w-5 h-5 flex-shrink-0',
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`content-list-item flex items-stretch w-full cursor-pointer ${
|
||||
selected && 'selected border-0 border-l-2px border-solid border-info'
|
||||
}`}
|
||||
id={item.uuid}
|
||||
onClick={onClick}
|
||||
onContextMenu={(event) => {
|
||||
event.preventDefault()
|
||||
void openContextMenu(event.clientX, event.clientY)
|
||||
}}
|
||||
>
|
||||
{!hideIcon ? (
|
||||
<div className="flex flex-col items-center justify-between p-4.5 pr-3 mr-0">
|
||||
<IconComponent />
|
||||
</div>
|
||||
) : (
|
||||
<div className="pr-4" />
|
||||
)}
|
||||
<div className="flex-grow min-w-0 py-4 px-0 border-0 border-b-1 border-solid border-main">
|
||||
<div className="flex items-start justify-between font-semibold text-base leading-1.3 overflow-hidden">
|
||||
<div className="break-word mr-2">{item.title}</div>
|
||||
</div>
|
||||
<ListItemMetadata item={item} hideDate={hideDate} sortBy={sortBy} />
|
||||
<ListItemTags hideTags={hideTags} tags={tags} />
|
||||
<ListItemConflictIndicator item={item} />
|
||||
</div>
|
||||
<ListItemFlagIcons item={item} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(FileListItem)
|
||||
@@ -0,0 +1,20 @@
|
||||
import { FunctionComponent } from 'react'
|
||||
import { ListableContentItem } from './Types/ListableContentItem'
|
||||
|
||||
type Props = {
|
||||
item: {
|
||||
conflictOf?: ListableContentItem['conflictOf']
|
||||
}
|
||||
}
|
||||
|
||||
const ListItemConflictIndicator: FunctionComponent<Props> = ({ item }) => {
|
||||
return item.conflictOf ? (
|
||||
<div className="flex flex-wrap items-center mt-0.5">
|
||||
<div className={'py-1 px-1.5 rounded mr-1 mt-2 bg-danger color-danger-contrast'}>
|
||||
<div className="text-xs font-bold text-center">Conflicted Copy</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
|
||||
export default ListItemConflictIndicator
|
||||
@@ -0,0 +1,47 @@
|
||||
import { FunctionComponent } from 'react'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import { ListableContentItem } from './Types/ListableContentItem'
|
||||
|
||||
type Props = {
|
||||
item: {
|
||||
locked: ListableContentItem['locked']
|
||||
trashed: ListableContentItem['trashed']
|
||||
archived: ListableContentItem['archived']
|
||||
pinned: ListableContentItem['pinned']
|
||||
}
|
||||
hasFiles?: boolean
|
||||
}
|
||||
|
||||
const ListItemFlagIcons: FunctionComponent<Props> = ({ item, hasFiles = false }) => {
|
||||
return (
|
||||
<div className="flex items-start p-4 pl-0 border-0 border-b-1 border-solid border-main">
|
||||
{item.locked && (
|
||||
<span className="flex items-center" title="Editing Disabled">
|
||||
<Icon ariaLabel="Editing Disabled" type="pencil-off" className="sn-icon--small color-info" />
|
||||
</span>
|
||||
)}
|
||||
{item.trashed && (
|
||||
<span className="flex items-center ml-1.5" title="Trashed">
|
||||
<Icon ariaLabel="Trashed" type="trash-filled" className="sn-icon--small color-danger" />
|
||||
</span>
|
||||
)}
|
||||
{item.archived && (
|
||||
<span className="flex items-center ml-1.5" title="Archived">
|
||||
<Icon ariaLabel="Archived" type="archive" className="sn-icon--mid color-accessory-tint-3" />
|
||||
</span>
|
||||
)}
|
||||
{item.pinned && (
|
||||
<span className="flex items-center ml-1.5" title="Pinned">
|
||||
<Icon ariaLabel="Pinned" type="pin-filled" className="sn-icon--small color-info" />
|
||||
</span>
|
||||
)}
|
||||
{hasFiles && (
|
||||
<span className="flex items-center ml-1.5" title="Files">
|
||||
<Icon ariaLabel="Files" type="attachment-file" className="sn-icon--small color-info" />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ListItemFlagIcons
|
||||
@@ -0,0 +1,31 @@
|
||||
import { CollectionSort, SortableItem } from '@standardnotes/snjs'
|
||||
import { FunctionComponent } from 'react'
|
||||
import { ListableContentItem } from './Types/ListableContentItem'
|
||||
|
||||
type Props = {
|
||||
item: {
|
||||
protected: ListableContentItem['protected']
|
||||
updatedAtString?: ListableContentItem['updatedAtString']
|
||||
createdAtString?: ListableContentItem['createdAtString']
|
||||
}
|
||||
hideDate: boolean
|
||||
sortBy: keyof SortableItem | undefined
|
||||
}
|
||||
|
||||
const ListItemMetadata: FunctionComponent<Props> = ({ item, hideDate, sortBy }) => {
|
||||
const showModifiedDate = sortBy === CollectionSort.UpdatedAt
|
||||
|
||||
if (hideDate && !item.protected) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="text-xs leading-1.4 mt-1 faded">
|
||||
{item.protected && <span>Protected {hideDate ? '' : ' • '}</span>}
|
||||
{!hideDate && showModifiedDate && <span>Modified {item.updatedAtString || 'Now'}</span>}
|
||||
{!hideDate && !showModifiedDate && <span>{item.createdAtString || 'Now'}</span>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ListItemMetadata
|
||||
@@ -0,0 +1,30 @@
|
||||
import { FunctionComponent } from 'react'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import { DisplayableListItemProps } from './Types/DisplayableListItemProps'
|
||||
|
||||
type Props = {
|
||||
hideTags: boolean
|
||||
tags: DisplayableListItemProps['tags']
|
||||
}
|
||||
|
||||
const ListItemTags: FunctionComponent<Props> = ({ hideTags, tags }) => {
|
||||
if (hideTags || !tags.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap mt-1.5 text-xs gap-2">
|
||||
{tags.map((tag) => (
|
||||
<span
|
||||
className="inline-flex items-center py-1 px-1.5 bg-passive-4-opacity-variant color-foreground rounded-0.5"
|
||||
key={tag.uuid}
|
||||
>
|
||||
<Icon type="hashtag" className="sn-icon--small color-passive-1 mr-1" />
|
||||
<span>{tag.title}</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ListItemTags
|
||||
@@ -0,0 +1,98 @@
|
||||
import { PLAIN_EDITOR_NAME } from '@/Constants/Constants'
|
||||
import { sanitizeHtmlString, SNNote } from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent } from 'react'
|
||||
import Icon from '@/Components/Icon/Icon'
|
||||
import ListItemConflictIndicator from './ListItemConflictIndicator'
|
||||
import ListItemFlagIcons from './ListItemFlagIcons'
|
||||
import ListItemTags from './ListItemTags'
|
||||
import ListItemMetadata from './ListItemMetadata'
|
||||
import { DisplayableListItemProps } from './Types/DisplayableListItemProps'
|
||||
|
||||
const NoteListItem: FunctionComponent<DisplayableListItemProps> = ({
|
||||
application,
|
||||
notesController,
|
||||
selectionController,
|
||||
hideDate,
|
||||
hideIcon,
|
||||
hideTags,
|
||||
hidePreview,
|
||||
item,
|
||||
selected,
|
||||
sortBy,
|
||||
tags,
|
||||
}) => {
|
||||
const editorForNote = application.componentManager.editorForNote(item as SNNote)
|
||||
const editorName = editorForNote?.name ?? PLAIN_EDITOR_NAME
|
||||
const [icon, tint] = application.iconsController.getIconAndTintForNoteType(editorForNote?.package_info.note_type)
|
||||
const hasFiles = application.items.getFilesForNote(item as SNNote).length > 0
|
||||
|
||||
const openNoteContextMenu = (posX: number, posY: number) => {
|
||||
notesController.setContextMenuClickLocation({
|
||||
x: posX,
|
||||
y: posY,
|
||||
})
|
||||
notesController.reloadContextMenuLayout()
|
||||
notesController.setContextMenuOpen(true)
|
||||
}
|
||||
|
||||
const openContextMenu = async (posX: number, posY: number) => {
|
||||
const { didSelect } = await selectionController.selectItem(item.uuid, true)
|
||||
if (didSelect) {
|
||||
openNoteContextMenu(posX, posY)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`content-list-item flex items-stretch w-full cursor-pointer ${
|
||||
selected && 'selected border-0 border-l-2px border-solid border-info'
|
||||
}`}
|
||||
id={item.uuid}
|
||||
onClick={() => {
|
||||
void selectionController.selectItem(item.uuid, true)
|
||||
}}
|
||||
onContextMenu={(event) => {
|
||||
event.preventDefault()
|
||||
void openContextMenu(event.clientX, event.clientY)
|
||||
}}
|
||||
>
|
||||
{!hideIcon ? (
|
||||
<div className="flex flex-col items-center justify-between p-4 pr-3 mr-0">
|
||||
<Icon ariaLabel={`Icon for ${editorName}`} type={icon} className={`color-accessory-tint-${tint}`} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="pr-4" />
|
||||
)}
|
||||
<div className="flex-grow min-w-0 py-4 px-0 border-0 border-b-1 border-solid border-main">
|
||||
<div className="flex items-start justify-between font-semibold text-base leading-1.3 overflow-hidden">
|
||||
<div className="break-word mr-2">{item.title}</div>
|
||||
</div>
|
||||
{!hidePreview && !item.hidePreview && !item.protected && (
|
||||
<div className="overflow-hidden overflow-ellipsis text-sm">
|
||||
{item.preview_html && (
|
||||
<div
|
||||
className="my-1"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: sanitizeHtmlString(item.preview_html),
|
||||
}}
|
||||
></div>
|
||||
)}
|
||||
{!item.preview_html && item.preview_plain && (
|
||||
<div className="leading-1.3 overflow-hidden line-clamp-1 mt-1">{item.preview_plain}</div>
|
||||
)}
|
||||
{!item.preview_html && !item.preview_plain && item.text && (
|
||||
<div className="leading-1.3 overflow-hidden line-clamp-1 mt-1">{item.text}</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<ListItemMetadata item={item} hideDate={hideDate} sortBy={sortBy} />
|
||||
<ListItemTags hideTags={hideTags} tags={tags} />
|
||||
<ListItemConflictIndicator item={item} />
|
||||
</div>
|
||||
<ListItemFlagIcons item={item} hasFiles={hasFiles} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(NoteListItem)
|
||||
@@ -0,0 +1,22 @@
|
||||
import { WebApplication } from '@/Application/Application'
|
||||
import { FilesController } from '@/Controllers/FilesController'
|
||||
import { NavigationController } from '@/Controllers/Navigation/NavigationController'
|
||||
import { NotesController } from '@/Controllers/NotesController'
|
||||
import { SelectedItemsController } from '@/Controllers/SelectedItemsController'
|
||||
import { SortableItem } from '@standardnotes/snjs'
|
||||
import { ListableContentItem } from './ListableContentItem'
|
||||
|
||||
export type AbstractListItemProps = {
|
||||
application: WebApplication
|
||||
filesController: FilesController
|
||||
selectionController: SelectedItemsController
|
||||
navigationController: NavigationController
|
||||
notesController: NotesController
|
||||
hideDate: boolean
|
||||
hideIcon: boolean
|
||||
hideTags: boolean
|
||||
hidePreview: boolean
|
||||
item: ListableContentItem
|
||||
selected: boolean
|
||||
sortBy: keyof SortableItem | undefined
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { SNTag } from '@standardnotes/snjs'
|
||||
import { AbstractListItemProps } from './AbstractListItemProps'
|
||||
|
||||
export type DisplayableListItemProps = AbstractListItemProps & {
|
||||
tags: {
|
||||
uuid: SNTag['uuid']
|
||||
title: SNTag['title']
|
||||
}[]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { ContentType, DecryptedItem, ItemContent } from '@standardnotes/snjs'
|
||||
|
||||
export type ListableContentItem = DecryptedItem<ItemContent> & {
|
||||
title: string
|
||||
protected: boolean
|
||||
uuid: string
|
||||
content_type: ContentType
|
||||
updatedAtString?: string
|
||||
createdAtString?: string
|
||||
hidePreview?: boolean
|
||||
preview_html?: string
|
||||
preview_plain?: string
|
||||
text?: string
|
||||
}
|
||||
Reference in New Issue
Block a user