feat: generic items list (#1035)
This commit is contained in:
@@ -118,7 +118,7 @@ export const AccountMenu: FunctionComponent<Props> = observer(
|
||||
return (
|
||||
<div ref={ref} id="account-menu" className="sn-component">
|
||||
<div
|
||||
className={`sn-menu-border sn-account-menu sn-dropdown ${
|
||||
className={`sn-account-menu sn-dropdown ${
|
||||
shouldAnimateCloseMenu ? 'slide-up-animation' : 'sn-dropdown--animated'
|
||||
} min-w-80 max-h-120 max-w-xs flex flex-col py-2 overflow-y-auto absolute`}
|
||||
onKeyDown={handleKeyDown}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { WebApplication } from '@/UIModels/Application'
|
||||
import { Component } from 'preact'
|
||||
import { ApplicationView } from '@/Components/ApplicationView/ApplicationView'
|
||||
import { WebOrDesktopDevice } from '@/Device/WebOrDesktopDevice'
|
||||
import { ApplicationGroupEvent, Runtime, ApplicationGroupEventData, DeinitSource } from '@standardnotes/snjs'
|
||||
import { ApplicationGroupEvent, ApplicationGroupEventData, DeinitSource } from '@standardnotes/snjs'
|
||||
import { unmountComponentAtNode, findDOMNode } from 'preact/compat'
|
||||
import { DialogContent, DialogOverlay } from '@reach/dialog'
|
||||
import { isDesktopApplication } from '@/Utils'
|
||||
@@ -39,12 +39,7 @@ export class ApplicationGroupView extends Component<Props, State> {
|
||||
return
|
||||
}
|
||||
|
||||
this.group = new ApplicationGroup(
|
||||
props.server,
|
||||
props.device,
|
||||
props.enableUnfinished ? Runtime.Dev : Runtime.Prod,
|
||||
props.websocketUrl,
|
||||
)
|
||||
this.group = new ApplicationGroup(props.server, props.device, props.websocketUrl)
|
||||
|
||||
window.mainApplicationGroup = this.group
|
||||
|
||||
|
||||
@@ -6,10 +6,9 @@ import { PANEL_NAME_NOTES, PANEL_NAME_NAVIGATION } from '@/Constants'
|
||||
import { alertDialog } from '@/Services/AlertService'
|
||||
import { WebApplication } from '@/UIModels/Application'
|
||||
import { Navigation } from '@/Components/Navigation/Navigation'
|
||||
import { NotesView } from '@/Components/NotesView/NotesView'
|
||||
import { NoteGroupView } from '@/Components/NoteGroupView/NoteGroupView'
|
||||
import { Footer } from '@/Components/Footer/Footer'
|
||||
import { SessionsModal } from '@/Components/SessionsModal'
|
||||
import { SessionsModal } from '@/Components/SessionsModal/SessionsModal'
|
||||
import { PreferencesViewWrapper } from '@/Components/Preferences/PreferencesViewWrapper'
|
||||
import { ChallengeModal } from '@/Components/ChallengeModal/ChallengeModal'
|
||||
import { NotesContextMenu } from '@/Components/NotesContextMenu/NotesContextMenu'
|
||||
@@ -21,9 +20,11 @@ import { PremiumModalProvider } from '@/Hooks/usePremiumModal'
|
||||
import { ConfirmSignoutContainer } from '@/Components/ConfirmSignoutModal/ConfirmSignoutModal'
|
||||
import { TagsContextMenu } from '@/Components/Tags/TagContextMenu'
|
||||
import { ToastContainer } from '@standardnotes/stylekit'
|
||||
import { FilePreviewModal } from '../Files/FilePreviewModal'
|
||||
import { FilePreviewModal } from '@/Components/Files/FilePreviewModal'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'preact/hooks'
|
||||
import { isStateDealloced } from '@/UIModels/AppState/AbstractState'
|
||||
import { ContentListView } from '@/Components/ContentListView/ContentListView'
|
||||
import { FileContextMenu } from '@/Components/FileContextMenu/FileContextMenu'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
@@ -211,7 +212,7 @@ export const ApplicationView: FunctionComponent<Props> = ({ application, mainApp
|
||||
<div className={platformString + ' main-ui-view sn-component'}>
|
||||
<div id="app" className={appClass + ' app app-column-container'}>
|
||||
<Navigation application={application} />
|
||||
<NotesView application={application} appState={appState} />
|
||||
<ContentListView application={application} appState={appState} />
|
||||
<NoteGroupView application={application} />
|
||||
</div>
|
||||
|
||||
@@ -227,6 +228,7 @@ export const ApplicationView: FunctionComponent<Props> = ({ application, mainApp
|
||||
<>
|
||||
<NotesContextMenu application={application} appState={appState} />
|
||||
<TagsContextMenu appState={appState} />
|
||||
<FileContextMenu appState={appState} />
|
||||
<PurchaseFlowWrapper application={application} appState={appState} />
|
||||
<ConfirmSignoutContainer
|
||||
applicationGroup={mainApplicationGroup}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { FunctionComponent } from 'preact'
|
||||
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
|
||||
import { Icon } from '@/Components/Icon/Icon'
|
||||
import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur'
|
||||
import { ChallengeReason, CollectionSort, ContentType, FileItem, SNNote } from '@standardnotes/snjs'
|
||||
import { ChallengeReason, ContentType, FileItem, SNNote } from '@standardnotes/snjs'
|
||||
import { confirmDialog } from '@/Services/AlertService'
|
||||
import { addToast, dismissToast, ToastType } from '@standardnotes/stylekit'
|
||||
import { StreamingFileReader } from '@standardnotes/filepicker'
|
||||
@@ -32,7 +32,7 @@ export const AttachedFilesButton: FunctionComponent<Props> = observer(
|
||||
}
|
||||
|
||||
const premiumModal = usePremiumModal()
|
||||
const note: SNNote | undefined = Object.values(appState.notes.selectedNotes)[0]
|
||||
const note: SNNote | undefined = appState.notes.firstSelectedNote
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
const [position, setPosition] = useState({
|
||||
@@ -59,10 +59,8 @@ export const AttachedFilesButton: FunctionComponent<Props> = observer(
|
||||
const attachedFilesCount = attachedFiles.length
|
||||
|
||||
useEffect(() => {
|
||||
application.items.setDisplayOptions(ContentType.File, CollectionSort.Title, 'dsc')
|
||||
|
||||
const unregisterFileStream = application.streamItems(ContentType.File, () => {
|
||||
setAllFiles(application.items.getDisplayableItems<FileItem>(ContentType.File))
|
||||
setAllFiles(application.items.getDisplayableFiles())
|
||||
if (note) {
|
||||
setAttachedFiles(application.items.getFilesForNote(note))
|
||||
}
|
||||
@@ -174,7 +172,7 @@ export const AttachedFilesButton: FunctionComponent<Props> = observer(
|
||||
}
|
||||
|
||||
const authorizeProtectedActionForFile = async (file: FileItem, challengeReason: ChallengeReason) => {
|
||||
const authorizedFiles = await application.protections.authorizeProtectedActionForFiles([file], challengeReason)
|
||||
const authorizedFiles = await application.protections.authorizeProtectedActionForItems([file], challengeReason)
|
||||
const isAuthorized = authorizedFiles.length > 0 && authorizedFiles.includes(file)
|
||||
return isAuthorized
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { StateUpdater, useCallback, useEffect, useRef, useState } from 'preact/hooks'
|
||||
import { Icon } from '@/Components/Icon/Icon'
|
||||
import { Switch } from '@/Components/Switch'
|
||||
import { Switch } from '@/Components/Switch/Switch'
|
||||
import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur'
|
||||
import { PopoverFileItemProps } from './PopoverFileItem'
|
||||
import { PopoverFileItemActionType } from './PopoverFileItemAction'
|
||||
|
||||
@@ -23,7 +23,7 @@ export const ChangeEditorButton: FunctionComponent<Props> = observer(
|
||||
return null
|
||||
}
|
||||
|
||||
const note = Object.values(appState.notes.selectedNotes)[0]
|
||||
const note = appState.notes.firstSelectedNote
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [isVisible, setIsVisible] = useState(false)
|
||||
const [position, setPosition] = useState({
|
||||
|
||||
@@ -29,7 +29,7 @@ type ChangeEditorMenuProps = {
|
||||
closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void
|
||||
closeMenu: () => void
|
||||
isVisible: boolean
|
||||
note: SNNote
|
||||
note: SNNote | undefined
|
||||
}
|
||||
|
||||
const getGroupId = (group: EditorMenuGroup) => group.title.toLowerCase().replace(/\s/, '-')
|
||||
@@ -75,97 +75,103 @@ export const ChangeEditorMenu: FunctionComponent<ChangeEditorMenuProps> = ({
|
||||
[currentEditor],
|
||||
)
|
||||
|
||||
const selectComponent = async (component: SNComponent | null, note: SNNote) => {
|
||||
if (component) {
|
||||
if (component.conflictOf) {
|
||||
application.mutator
|
||||
.changeAndSaveItem(component, (mutator) => {
|
||||
mutator.conflictOf = undefined
|
||||
const selectComponent = useCallback(
|
||||
async (component: SNComponent | null, note: SNNote) => {
|
||||
if (component) {
|
||||
if (component.conflictOf) {
|
||||
application.mutator
|
||||
.changeAndSaveItem(component, (mutator) => {
|
||||
mutator.conflictOf = undefined
|
||||
})
|
||||
.catch(console.error)
|
||||
}
|
||||
}
|
||||
|
||||
const transactions: TransactionalMutation[] = []
|
||||
|
||||
await application.getAppState().contentListView.insertCurrentIfTemplate()
|
||||
|
||||
if (note.locked) {
|
||||
application.alertService.alert(STRING_EDIT_LOCKED_ATTEMPT).catch(console.error)
|
||||
return
|
||||
}
|
||||
|
||||
if (!component) {
|
||||
if (!note.prefersPlainEditor) {
|
||||
transactions.push({
|
||||
itemUuid: note.uuid,
|
||||
mutate: (m: ItemMutator) => {
|
||||
const noteMutator = m as NoteMutator
|
||||
noteMutator.prefersPlainEditor = true
|
||||
},
|
||||
})
|
||||
.catch(console.error)
|
||||
}
|
||||
const currentEditor = application.componentManager.editorForNote(note)
|
||||
if (currentEditor?.isExplicitlyEnabledForItem(note.uuid)) {
|
||||
transactions.push(transactionForDisassociateComponentWithCurrentNote(currentEditor, note))
|
||||
}
|
||||
reloadFont(application.getPreference(PrefKey.EditorMonospaceEnabled))
|
||||
} else if (component.area === ComponentArea.Editor) {
|
||||
const currentEditor = application.componentManager.editorForNote(note)
|
||||
if (currentEditor && component.uuid !== currentEditor.uuid) {
|
||||
transactions.push(transactionForDisassociateComponentWithCurrentNote(currentEditor, note))
|
||||
}
|
||||
const prefersPlain = note.prefersPlainEditor
|
||||
if (prefersPlain) {
|
||||
transactions.push({
|
||||
itemUuid: note.uuid,
|
||||
mutate: (m: ItemMutator) => {
|
||||
const noteMutator = m as NoteMutator
|
||||
noteMutator.prefersPlainEditor = false
|
||||
},
|
||||
})
|
||||
}
|
||||
transactions.push(transactionForAssociateComponentWithCurrentNote(component, note))
|
||||
}
|
||||
}
|
||||
|
||||
const transactions: TransactionalMutation[] = []
|
||||
await application.mutator.runTransactionalMutations(transactions)
|
||||
/** Dirtying can happen above */
|
||||
application.sync.sync().catch(console.error)
|
||||
|
||||
await application.getAppState().notesView.insertCurrentIfTemplate()
|
||||
setCurrentEditor(application.componentManager.editorForNote(note))
|
||||
},
|
||||
[application],
|
||||
)
|
||||
|
||||
if (note.locked) {
|
||||
application.alertService.alert(STRING_EDIT_LOCKED_ATTEMPT).catch(console.error)
|
||||
return
|
||||
}
|
||||
|
||||
if (!component) {
|
||||
if (!note.prefersPlainEditor) {
|
||||
transactions.push({
|
||||
itemUuid: note.uuid,
|
||||
mutate: (m: ItemMutator) => {
|
||||
const noteMutator = m as NoteMutator
|
||||
noteMutator.prefersPlainEditor = true
|
||||
},
|
||||
})
|
||||
const selectEditor = useCallback(
|
||||
async (itemToBeSelected: EditorMenuItem) => {
|
||||
if (!itemToBeSelected.isEntitled) {
|
||||
premiumModal.activate(itemToBeSelected.name)
|
||||
return
|
||||
}
|
||||
const currentEditor = application.componentManager.editorForNote(note)
|
||||
if (currentEditor?.isExplicitlyEnabledForItem(note.uuid)) {
|
||||
transactions.push(transactionForDisassociateComponentWithCurrentNote(currentEditor, note))
|
||||
|
||||
const areBothEditorsPlain = !currentEditor && !itemToBeSelected.component
|
||||
|
||||
if (areBothEditorsPlain) {
|
||||
return
|
||||
}
|
||||
reloadFont(application.getPreference(PrefKey.EditorMonospaceEnabled))
|
||||
} else if (component.area === ComponentArea.Editor) {
|
||||
const currentEditor = application.componentManager.editorForNote(note)
|
||||
if (currentEditor && component.uuid !== currentEditor.uuid) {
|
||||
transactions.push(transactionForDisassociateComponentWithCurrentNote(currentEditor, note))
|
||||
|
||||
let shouldSelectEditor = true
|
||||
|
||||
if (itemToBeSelected.component) {
|
||||
const changeRequiresAlert = application.componentManager.doesEditorChangeRequireAlert(
|
||||
currentEditor,
|
||||
itemToBeSelected.component,
|
||||
)
|
||||
|
||||
if (changeRequiresAlert) {
|
||||
shouldSelectEditor = await application.componentManager.showEditorChangeAlert()
|
||||
}
|
||||
}
|
||||
const prefersPlain = note.prefersPlainEditor
|
||||
if (prefersPlain) {
|
||||
transactions.push({
|
||||
itemUuid: note.uuid,
|
||||
mutate: (m: ItemMutator) => {
|
||||
const noteMutator = m as NoteMutator
|
||||
noteMutator.prefersPlainEditor = false
|
||||
},
|
||||
})
|
||||
|
||||
if (shouldSelectEditor && note) {
|
||||
selectComponent(itemToBeSelected.component ?? null, note).catch(console.error)
|
||||
}
|
||||
transactions.push(transactionForAssociateComponentWithCurrentNote(component, note))
|
||||
}
|
||||
|
||||
await application.mutator.runTransactionalMutations(transactions)
|
||||
/** Dirtying can happen above */
|
||||
application.sync.sync().catch(console.error)
|
||||
|
||||
setCurrentEditor(application.componentManager.editorForNote(note))
|
||||
}
|
||||
|
||||
const selectEditor = async (itemToBeSelected: EditorMenuItem) => {
|
||||
if (!itemToBeSelected.isEntitled) {
|
||||
premiumModal.activate(itemToBeSelected.name)
|
||||
return
|
||||
}
|
||||
|
||||
const areBothEditorsPlain = !currentEditor && !itemToBeSelected.component
|
||||
|
||||
if (areBothEditorsPlain) {
|
||||
return
|
||||
}
|
||||
|
||||
let shouldSelectEditor = true
|
||||
|
||||
if (itemToBeSelected.component) {
|
||||
const changeRequiresAlert = application.componentManager.doesEditorChangeRequireAlert(
|
||||
currentEditor,
|
||||
itemToBeSelected.component,
|
||||
)
|
||||
|
||||
if (changeRequiresAlert) {
|
||||
shouldSelectEditor = await application.componentManager.showEditorChangeAlert()
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldSelectEditor) {
|
||||
selectComponent(itemToBeSelected.component ?? null, note).catch(console.error)
|
||||
}
|
||||
|
||||
closeMenu()
|
||||
}
|
||||
closeMenu()
|
||||
},
|
||||
[application.componentManager, closeMenu, currentEditor, note, premiumModal, selectComponent],
|
||||
)
|
||||
|
||||
return (
|
||||
<Menu className="pt-0.5 pb-1" a11yLabel="Change note type menu" isOpen={isVisible}>
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { WebApplication } from '@/UIModels/Application'
|
||||
import { KeyboardKey } from '@/Services/IOService'
|
||||
import { AppState } from '@/UIModels/AppState'
|
||||
import { UuidString } from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { FOCUSABLE_BUT_NOT_TABBABLE, NOTES_LIST_SCROLL_THRESHOLD } from '@/Constants'
|
||||
import { ListableContentItem } from './Types/ListableContentItem'
|
||||
import { ContentListItem } from './ContentListItem'
|
||||
import { useCallback } from 'preact/hooks'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
appState: AppState
|
||||
items: ListableContentItem[]
|
||||
selectedItems: Record<UuidString, ListableContentItem>
|
||||
paginate: () => void
|
||||
}
|
||||
|
||||
export const ContentList: FunctionComponent<Props> = observer(
|
||||
({ application, appState, items, selectedItems, paginate }) => {
|
||||
const { selectPreviousItem, selectNextItem } = appState.contentListView
|
||||
const { hideTags, hideDate, hideNotePreview, hideEditorIcon } = appState.contentListView.webDisplayOptions
|
||||
const { sortBy } = appState.contentListView.displayOptions
|
||||
|
||||
const onScroll = useCallback(
|
||||
(e: Event) => {
|
||||
const offset = NOTES_LIST_SCROLL_THRESHOLD
|
||||
const element = e.target as HTMLElement
|
||||
if (element.scrollTop + element.offsetHeight >= element.scrollHeight - offset) {
|
||||
paginate()
|
||||
}
|
||||
},
|
||||
[paginate],
|
||||
)
|
||||
|
||||
const onKeyDown = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
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}
|
||||
appState={appState}
|
||||
item={item}
|
||||
selected={!!selectedItems[item.uuid]}
|
||||
hideDate={hideDate}
|
||||
hidePreview={hideNotePreview}
|
||||
hideTags={hideTags}
|
||||
hideIcon={hideEditorIcon}
|
||||
sortBy={sortBy}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,36 @@
|
||||
import { ContentType, SNTag } from '@standardnotes/snjs'
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { FileListItem } from './FileListItem'
|
||||
import { NoteListItem } from './NoteListItem'
|
||||
import { AbstractListItemProps } from './Types/AbstractListItemProps'
|
||||
|
||||
export const ContentListItem: FunctionComponent<AbstractListItemProps> = (props) => {
|
||||
const getTags = () => {
|
||||
if (props.hideTags) {
|
||||
return []
|
||||
}
|
||||
|
||||
const selectedTag = props.appState.tags.selected
|
||||
if (!selectedTag) {
|
||||
return []
|
||||
}
|
||||
|
||||
const tags = props.appState.getItemTags(props.item)
|
||||
|
||||
const isNavigatingOnlyTag = selectedTag instanceof SNTag && tags.length === 1
|
||||
if (isNavigatingOnlyTag) {
|
||||
return []
|
||||
}
|
||||
|
||||
return tags.map((tag) => tag.title).sort()
|
||||
}
|
||||
|
||||
switch (props.item.content_type) {
|
||||
case ContentType.Note:
|
||||
return <NoteListItem tags={getTags()} {...props} />
|
||||
case ContentType.File:
|
||||
return <FileListItem tags={getTags()} {...props} />
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,9 @@ import { PrefKey } from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
|
||||
import { ContentList } from '@/Components/ContentListView/ContentList'
|
||||
import { NotesListOptionsMenu } from '@/Components/ContentListView/NotesListOptionsMenu'
|
||||
import { NoAccountWarning } from '@/Components/NoAccountWarning/NoAccountWarning'
|
||||
import { NotesList } from '@/Components/NotesList/NotesList'
|
||||
import { NotesListOptionsMenu } from '@/Components/NotesList/NotesListOptionsMenu'
|
||||
import { SearchOptions } from '@/Components/SearchOptions/SearchOptions'
|
||||
import { PanelSide, ResizeFinishCallback, PanelResizer, PanelResizeType } from '@/Components/PanelResizer/PanelResizer'
|
||||
import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure'
|
||||
@@ -20,34 +20,32 @@ type Props = {
|
||||
appState: AppState
|
||||
}
|
||||
|
||||
export const NotesView: FunctionComponent<Props> = observer(({ application, appState }: Props) => {
|
||||
export const ContentListView: FunctionComponent<Props> = observer(({ application, appState }) => {
|
||||
if (isStateDealloced(appState)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const notesViewPanelRef = useRef<HTMLDivElement>(null)
|
||||
const itemsViewPanelRef = useRef<HTMLDivElement>(null)
|
||||
const displayOptionsMenuRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const {
|
||||
completedFullSync,
|
||||
displayOptions,
|
||||
noteFilterText,
|
||||
optionsSubtitle,
|
||||
panelTitle,
|
||||
renderedNotes,
|
||||
renderedItems,
|
||||
setNoteFilterText,
|
||||
searchBarElement,
|
||||
selectNextItem,
|
||||
selectPreviousItem,
|
||||
onFilterEnter,
|
||||
clearFilterText,
|
||||
paginate,
|
||||
panelWidth,
|
||||
} = appState.notesView
|
||||
createNewNote,
|
||||
} = appState.contentListView
|
||||
|
||||
const { selectedNotes } = appState.notes
|
||||
|
||||
const createNewNote = useCallback(() => appState.notesView.createNewNote(), [appState])
|
||||
const onFilterEnter = useCallback(() => appState.notesView.onFilterEnter(), [appState])
|
||||
const clearFilterText = useCallback(() => appState.notesView.clearFilterText(), [appState])
|
||||
const setNoteFilterText = useCallback((text: string) => appState.notesView.setNoteFilterText(text), [appState])
|
||||
const selectNextNote = useCallback(() => appState.notesView.selectNextNote(), [appState])
|
||||
const selectPreviousNote = useCallback(() => appState.notesView.selectPreviousNote(), [appState])
|
||||
const { selectedItems } = appState.selectedItems
|
||||
|
||||
const [showDisplayOptionsMenu, setShowDisplayOptionsMenu] = useState(false)
|
||||
const [focusedSearch, setFocusedSearch] = useState(false)
|
||||
@@ -76,7 +74,7 @@ export const NotesView: FunctionComponent<Props> = observer(({ application, appS
|
||||
if (searchBarElement === document.activeElement) {
|
||||
searchBarElement?.blur()
|
||||
}
|
||||
selectNextNote()
|
||||
selectNextItem()
|
||||
},
|
||||
})
|
||||
|
||||
@@ -84,7 +82,7 @@ export const NotesView: FunctionComponent<Props> = observer(({ application, appS
|
||||
key: KeyboardKey.Up,
|
||||
element: document.body,
|
||||
onKeyDown: () => {
|
||||
selectPreviousNote()
|
||||
selectPreviousItem()
|
||||
},
|
||||
})
|
||||
|
||||
@@ -104,7 +102,7 @@ export const NotesView: FunctionComponent<Props> = observer(({ application, appS
|
||||
previousNoteKeyObserver()
|
||||
searchKeyObserver()
|
||||
}
|
||||
}, [application, createNewNote, selectPreviousNote, searchBarElement, selectNextNote])
|
||||
}, [application.io, createNewNote, searchBarElement, selectNextItem, selectPreviousItem])
|
||||
|
||||
const onNoteFilterTextChange = useCallback(
|
||||
(e: Event) => {
|
||||
@@ -144,14 +142,14 @@ export const NotesView: FunctionComponent<Props> = observer(({ application, appS
|
||||
|
||||
return (
|
||||
<div
|
||||
id="notes-column"
|
||||
className="sn-component section notes app-column app-column-second"
|
||||
aria-label="Notes"
|
||||
ref={notesViewPanelRef}
|
||||
id="items-column"
|
||||
className="sn-component section app-column app-column-second"
|
||||
aria-label={'Notes & Files'}
|
||||
ref={itemsViewPanelRef}
|
||||
>
|
||||
<div className="content">
|
||||
<div id="notes-title-bar" className="section-title-bar">
|
||||
<div id="notes-title-bar-container">
|
||||
<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
|
||||
@@ -172,7 +170,7 @@ export const NotesView: FunctionComponent<Props> = observer(({ application, appS
|
||||
id="search-bar"
|
||||
className="filter-bar"
|
||||
placeholder="Search"
|
||||
title="Searches notes in the currently selected tag"
|
||||
title="Searches notes and files in the currently selected tag"
|
||||
value={noteFilterText}
|
||||
onChange={onNoteFilterTextChange}
|
||||
onKeyUp={onNoteFilterKeyUp}
|
||||
@@ -195,7 +193,7 @@ export const NotesView: FunctionComponent<Props> = observer(({ application, appS
|
||||
</div>
|
||||
<NoAccountWarning appState={appState} />
|
||||
</div>
|
||||
<div id="notes-menu-bar" className="sn-component" ref={displayOptionsMenuRef}>
|
||||
<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}>
|
||||
@@ -227,27 +225,24 @@ export const NotesView: FunctionComponent<Props> = observer(({ application, appS
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{completedFullSync && !renderedNotes.length ? <p className="empty-notes-list faded">No notes.</p> : null}
|
||||
{!completedFullSync && !renderedNotes.length ? (
|
||||
<p className="empty-notes-list faded">Loading notes...</p>
|
||||
) : null}
|
||||
{renderedNotes.length ? (
|
||||
<NotesList
|
||||
notes={renderedNotes}
|
||||
selectedNotes={selectedNotes}
|
||||
{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}
|
||||
appState={appState}
|
||||
displayOptions={displayOptions}
|
||||
paginate={paginate}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
{notesViewPanelRef.current && (
|
||||
{itemsViewPanelRef.current && (
|
||||
<PanelResizer
|
||||
collapsable={true}
|
||||
hoverable={true}
|
||||
defaultWidth={300}
|
||||
panel={notesViewPanelRef.current}
|
||||
panel={itemsViewPanelRef.current}
|
||||
side={PanelSide.Right}
|
||||
type={PanelResizeType.WidthOnly}
|
||||
resizeFinishCallback={panelResizeFinishCallback}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { FileItem } from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { useCallback } from 'preact/hooks'
|
||||
import { getFileIconComponent } from '../AttachedFilesPopover/PopoverFileItem'
|
||||
import { ListItemConflictIndicator } from './ListItemConflictIndicator'
|
||||
import { ListItemFlagIcons } from './ListItemFlagIcons'
|
||||
import { ListItemTags } from './ListItemTags'
|
||||
import { ListItemMetadata } from './ListItemMetadata'
|
||||
import { DisplayableListItemProps } from './Types/DisplayableListItemProps'
|
||||
|
||||
export const FileListItem: FunctionComponent<DisplayableListItemProps> = observer(
|
||||
({ application, appState, hideDate, hideIcon, hideTags, item, selected, sortBy, tags }) => {
|
||||
const openFileContextMenu = useCallback(
|
||||
(posX: number, posY: number) => {
|
||||
appState.files.setFileContextMenuLocation({
|
||||
x: posX,
|
||||
y: posY,
|
||||
})
|
||||
appState.files.setShowFileContextMenu(true)
|
||||
},
|
||||
[appState.files],
|
||||
)
|
||||
|
||||
const openContextMenu = useCallback(
|
||||
(posX: number, posY: number) => {
|
||||
void appState.contentListView.selectItemWithScrollHandling(item, {
|
||||
userTriggered: true,
|
||||
scrollIntoView: false,
|
||||
})
|
||||
openFileContextMenu(posX, posY)
|
||||
},
|
||||
[appState.contentListView, item, openFileContextMenu],
|
||||
)
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
void appState.selectedItems.selectItem(item.uuid, true).then(({ didSelect }) => {
|
||||
if (didSelect && appState.selectedItems.selectedItemsCount < 2) {
|
||||
appState.filePreviewModal.activate(item as FileItem, appState.files.allFiles)
|
||||
}
|
||||
})
|
||||
}, [appState.filePreviewModal, appState.files.allFiles, appState.selectedItems, item])
|
||||
|
||||
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()
|
||||
openContextMenu(event.clientX, event.clientY)
|
||||
}}
|
||||
>
|
||||
{!hideIcon ? (
|
||||
<div className="flex flex-col items-center justify-between p-4 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>
|
||||
)
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,16 @@
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { ListableContentItem } from './Types/ListableContentItem'
|
||||
|
||||
export const ListItemConflictIndicator: FunctionComponent<{
|
||||
item: {
|
||||
conflictOf?: ListableContentItem['conflictOf']
|
||||
}
|
||||
}> = ({ 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
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { FunctionComponent } from 'preact'
|
||||
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
|
||||
}
|
||||
|
||||
export 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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { CollectionSort, SortableItem } from '@standardnotes/snjs'
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { ListableContentItem } from './Types/ListableContentItem'
|
||||
|
||||
type Props = {
|
||||
item: {
|
||||
protected: ListableContentItem['protected']
|
||||
updatedAtString?: ListableContentItem['updatedAtString']
|
||||
createdAtString?: ListableContentItem['createdAtString']
|
||||
}
|
||||
hideDate: boolean
|
||||
sortBy: keyof SortableItem | undefined
|
||||
}
|
||||
|
||||
export 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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { Icon } from '@/Components/Icon/Icon'
|
||||
|
||||
export const ListItemTags: FunctionComponent<{
|
||||
hideTags: boolean
|
||||
tags: string[]
|
||||
}> = ({ 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-grey-4-opacity-variant color-foreground rounded-0.5">
|
||||
<Icon type="hashtag" className="sn-icon--small color-grey-1 mr-1" />
|
||||
<span>{tag}</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { PLAIN_EDITOR_NAME } from '@/Constants'
|
||||
import { sanitizeHtmlString, SNNote } from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent } from 'preact'
|
||||
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'
|
||||
|
||||
export const NoteListItem: FunctionComponent<DisplayableListItemProps> = observer(
|
||||
({ application, appState, 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) => {
|
||||
appState.notes.setContextMenuClickLocation({
|
||||
x: posX,
|
||||
y: posY,
|
||||
})
|
||||
appState.notes.reloadContextMenuLayout()
|
||||
appState.notes.setContextMenuOpen(true)
|
||||
}
|
||||
|
||||
const openContextMenu = (posX: number, posY: number) => {
|
||||
void appState.selectedItems.selectItem(item.uuid, true)
|
||||
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 appState.selectedItems.selectItem(item.uuid, true)
|
||||
}}
|
||||
onContextMenu={(event) => {
|
||||
event.preventDefault()
|
||||
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>
|
||||
)
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,16 @@
|
||||
import { WebApplication } from '@/UIModels/Application'
|
||||
import { AppState } from '@/UIModels/AppState'
|
||||
import { SortableItem } from '@standardnotes/snjs'
|
||||
import { ListableContentItem } from './ListableContentItem'
|
||||
|
||||
export type AbstractListItemProps = {
|
||||
application: WebApplication
|
||||
appState: AppState
|
||||
hideDate: boolean
|
||||
hideIcon: boolean
|
||||
hideTags: boolean
|
||||
hidePreview: boolean
|
||||
item: ListableContentItem
|
||||
selected: boolean
|
||||
sortBy: keyof SortableItem | undefined
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { AbstractListItemProps } from './AbstractListItemProps'
|
||||
|
||||
export type DisplayableListItemProps = AbstractListItemProps & {
|
||||
tags: string[]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import { MAX_MENU_SIZE_MULTIPLIER, MENU_MARGIN_FROM_APP_BORDER } from '@/Constants'
|
||||
import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur'
|
||||
import { useCloseOnClickOutside } from '@/Hooks/useCloseOnClickOutside'
|
||||
import { AppState } from '@/UIModels/AppState'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
|
||||
import React from 'react'
|
||||
import { PopoverFileItemAction } from '../AttachedFilesPopover/PopoverFileItemAction'
|
||||
import { PopoverTabs } from '../AttachedFilesPopover/PopoverTabs'
|
||||
import { FileMenuOptions } from './FileMenuOptions'
|
||||
|
||||
type Props = {
|
||||
appState: AppState
|
||||
}
|
||||
|
||||
export const FileContextMenu: FunctionComponent<Props> = observer(({ appState }) => {
|
||||
const { selectedFiles, showFileContextMenu, setShowFileContextMenu, fileContextMenuLocation } = appState.files
|
||||
|
||||
const [contextMenuStyle, setContextMenuStyle] = useState<React.CSSProperties>({
|
||||
top: 0,
|
||||
left: 0,
|
||||
visibility: 'hidden',
|
||||
})
|
||||
const [contextMenuMaxHeight, setContextMenuMaxHeight] = useState<number | 'auto'>('auto')
|
||||
const contextMenuRef = useRef<HTMLDivElement>(null)
|
||||
const [closeOnBlur] = useCloseOnBlur(contextMenuRef, (open: boolean) => setShowFileContextMenu(open))
|
||||
useCloseOnClickOutside(contextMenuRef, () => appState.files.setShowFileContextMenu(false))
|
||||
|
||||
const selectedFile = Object.values(selectedFiles)[0]
|
||||
if (!showFileContextMenu || !selectedFile) {
|
||||
return null
|
||||
}
|
||||
|
||||
const reloadContextMenuLayout = useCallback(() => {
|
||||
const { clientHeight } = document.documentElement
|
||||
const defaultFontSize = window.getComputedStyle(document.documentElement).fontSize
|
||||
const maxContextMenuHeight = parseFloat(defaultFontSize) * MAX_MENU_SIZE_MULTIPLIER
|
||||
const footerElementRect = document.getElementById('footer-bar')?.getBoundingClientRect()
|
||||
const footerHeightInPx = footerElementRect?.height
|
||||
|
||||
let openUpBottom = true
|
||||
|
||||
if (footerHeightInPx) {
|
||||
const bottomSpace = clientHeight - footerHeightInPx - fileContextMenuLocation.y
|
||||
const upSpace = fileContextMenuLocation.y
|
||||
|
||||
if (maxContextMenuHeight > bottomSpace) {
|
||||
if (upSpace > maxContextMenuHeight) {
|
||||
openUpBottom = false
|
||||
setContextMenuMaxHeight('auto')
|
||||
} else {
|
||||
if (upSpace > bottomSpace) {
|
||||
setContextMenuMaxHeight(upSpace - MENU_MARGIN_FROM_APP_BORDER)
|
||||
openUpBottom = false
|
||||
} else {
|
||||
setContextMenuMaxHeight(bottomSpace - MENU_MARGIN_FROM_APP_BORDER)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setContextMenuMaxHeight('auto')
|
||||
}
|
||||
}
|
||||
|
||||
if (openUpBottom) {
|
||||
setContextMenuStyle({
|
||||
top: fileContextMenuLocation.y,
|
||||
left: fileContextMenuLocation.x,
|
||||
visibility: 'visible',
|
||||
})
|
||||
} else {
|
||||
setContextMenuStyle({
|
||||
bottom: clientHeight - fileContextMenuLocation.y,
|
||||
left: fileContextMenuLocation.x,
|
||||
visibility: 'visible',
|
||||
})
|
||||
}
|
||||
}, [fileContextMenuLocation.x, fileContextMenuLocation.y])
|
||||
|
||||
useEffect(() => {
|
||||
if (showFileContextMenu) {
|
||||
reloadContextMenuLayout()
|
||||
}
|
||||
}, [reloadContextMenuLayout, showFileContextMenu])
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('resize', reloadContextMenuLayout)
|
||||
return () => {
|
||||
window.removeEventListener('resize', reloadContextMenuLayout)
|
||||
}
|
||||
}, [reloadContextMenuLayout])
|
||||
|
||||
const handleFileAction = useCallback(
|
||||
async (action: PopoverFileItemAction) => {
|
||||
const { didHandleAction } = await appState.files.handleFileAction(action, PopoverTabs.AllFiles)
|
||||
return didHandleAction
|
||||
},
|
||||
[appState.files],
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={contextMenuRef}
|
||||
className="sn-dropdown min-w-60 max-h-120 max-w-xs flex flex-col py-2 overflow-y-auto fixed"
|
||||
style={{
|
||||
...contextMenuStyle,
|
||||
maxHeight: contextMenuMaxHeight,
|
||||
}}
|
||||
>
|
||||
<FileMenuOptions
|
||||
file={selectedFile}
|
||||
handleFileAction={handleFileAction}
|
||||
closeOnBlur={closeOnBlur}
|
||||
closeMenu={() => setShowFileContextMenu(false)}
|
||||
shouldShowRenameOption={false}
|
||||
shouldShowAttachOption={false}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,141 @@
|
||||
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants'
|
||||
import { FileItem } from '@standardnotes/snjs'
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { PopoverFileItemAction, PopoverFileItemActionType } from '../AttachedFilesPopover/PopoverFileItemAction'
|
||||
import { Icon } from '@/Components/Icon/Icon'
|
||||
import { Switch } from '@/Components/Switch/Switch'
|
||||
|
||||
type Props = {
|
||||
closeMenu: () => void
|
||||
closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void
|
||||
file: FileItem
|
||||
fileProtectionToggleCallback?: (isProtected: boolean) => void
|
||||
handleFileAction: (action: PopoverFileItemAction) => Promise<boolean>
|
||||
isFileAttachedToNote?: boolean
|
||||
renameToggleCallback?: (isRenamingFile: boolean) => void
|
||||
shouldShowRenameOption: boolean
|
||||
shouldShowAttachOption: boolean
|
||||
}
|
||||
|
||||
export const FileMenuOptions: FunctionComponent<Props> = ({
|
||||
closeMenu,
|
||||
closeOnBlur,
|
||||
file,
|
||||
fileProtectionToggleCallback,
|
||||
handleFileAction,
|
||||
isFileAttachedToNote,
|
||||
renameToggleCallback,
|
||||
shouldShowRenameOption,
|
||||
shouldShowAttachOption,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
onBlur={closeOnBlur}
|
||||
className="sn-dropdown-item focus:bg-info-backdrop"
|
||||
onClick={() => {
|
||||
handleFileAction({
|
||||
type: PopoverFileItemActionType.PreviewFile,
|
||||
payload: file,
|
||||
}).catch(console.error)
|
||||
closeMenu()
|
||||
}}
|
||||
>
|
||||
<Icon type="file" className="mr-2 color-neutral" />
|
||||
Preview file
|
||||
</button>
|
||||
{isFileAttachedToNote ? (
|
||||
<button
|
||||
onBlur={closeOnBlur}
|
||||
className="sn-dropdown-item focus:bg-info-backdrop"
|
||||
onClick={() => {
|
||||
handleFileAction({
|
||||
type: PopoverFileItemActionType.DetachFileToNote,
|
||||
payload: file,
|
||||
}).catch(console.error)
|
||||
closeMenu()
|
||||
}}
|
||||
>
|
||||
<Icon type="link-off" className="mr-2 color-neutral" />
|
||||
Detach from note
|
||||
</button>
|
||||
) : shouldShowAttachOption ? (
|
||||
<button
|
||||
onBlur={closeOnBlur}
|
||||
className="sn-dropdown-item focus:bg-info-backdrop"
|
||||
onClick={() => {
|
||||
handleFileAction({
|
||||
type: PopoverFileItemActionType.AttachFileToNote,
|
||||
payload: file,
|
||||
}).catch(console.error)
|
||||
closeMenu()
|
||||
}}
|
||||
>
|
||||
<Icon type="link" className="mr-2 color-neutral" />
|
||||
Attach to note
|
||||
</button>
|
||||
) : null}
|
||||
<div className="min-h-1px my-1 bg-border"></div>
|
||||
<button
|
||||
className="sn-dropdown-item justify-between focus:bg-info-backdrop"
|
||||
onClick={() => {
|
||||
handleFileAction({
|
||||
type: PopoverFileItemActionType.ToggleFileProtection,
|
||||
payload: file,
|
||||
callback: (isProtected: boolean) => {
|
||||
fileProtectionToggleCallback?.(isProtected)
|
||||
},
|
||||
}).catch(console.error)
|
||||
}}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
<span className="flex items-center">
|
||||
<Icon type="password" className="mr-2 color-neutral" />
|
||||
Password protection
|
||||
</span>
|
||||
<Switch className="px-0 pointer-events-none" tabIndex={FOCUSABLE_BUT_NOT_TABBABLE} checked={file.protected} />
|
||||
</button>
|
||||
<div className="min-h-1px my-1 bg-border"></div>
|
||||
<button
|
||||
onBlur={closeOnBlur}
|
||||
className="sn-dropdown-item focus:bg-info-backdrop"
|
||||
onClick={() => {
|
||||
handleFileAction({
|
||||
type: PopoverFileItemActionType.DownloadFile,
|
||||
payload: file,
|
||||
}).catch(console.error)
|
||||
closeMenu()
|
||||
}}
|
||||
>
|
||||
<Icon type="download" className="mr-2 color-neutral" />
|
||||
Download
|
||||
</button>
|
||||
{shouldShowRenameOption && (
|
||||
<button
|
||||
onBlur={closeOnBlur}
|
||||
className="sn-dropdown-item focus:bg-info-backdrop"
|
||||
onClick={() => {
|
||||
renameToggleCallback?.(true)
|
||||
}}
|
||||
>
|
||||
<Icon type="pencil" className="mr-2 color-neutral" />
|
||||
Rename
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onBlur={closeOnBlur}
|
||||
className="sn-dropdown-item focus:bg-info-backdrop"
|
||||
onClick={() => {
|
||||
handleFileAction({
|
||||
type: PopoverFileItemActionType.DeleteFile,
|
||||
payload: file,
|
||||
}).catch(console.error)
|
||||
closeMenu()
|
||||
}}
|
||||
>
|
||||
<Icon type="trash" className="mr-2 color-danger" />
|
||||
<span className="color-danger">Delete permanently</span>
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -130,7 +130,7 @@ export const FilePreviewModal: FunctionComponent<Props> = observer(({ applicatio
|
||||
width: '90%',
|
||||
maxWidth: '90%',
|
||||
minHeight: '90%',
|
||||
background: 'var(--sn-stylekit-background-color)',
|
||||
background: 'var(--modal-background-color)',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -2,7 +2,7 @@ import { WebAppEvent, WebApplication } from '@/UIModels/Application'
|
||||
import { ApplicationGroup } from '@/UIModels/ApplicationGroup'
|
||||
import { PureComponent } from '@/Components/Abstract/PureComponent'
|
||||
import { destroyAllObjectProperties, preventRefreshing } from '@/Utils'
|
||||
import { ApplicationEvent, ContentType, CollectionSort, ApplicationDescriptor } from '@standardnotes/snjs'
|
||||
import { ApplicationEvent, ApplicationDescriptor } from '@standardnotes/snjs'
|
||||
import {
|
||||
STRING_NEW_UPDATE_READY,
|
||||
STRING_CONFIRM_APP_QUIT_DURING_UPGRADE,
|
||||
@@ -15,7 +15,7 @@ import { AccountMenu, AccountMenuPane } from '@/Components/AccountMenu/AccountMe
|
||||
import { AppStateEvent, EventSource } from '@/UIModels/AppState'
|
||||
import { Icon } from '@/Components/Icon/Icon'
|
||||
import { QuickSettingsMenu } from '@/Components/QuickSettingsMenu/QuickSettingsMenu'
|
||||
import { SyncResolutionMenu } from '@/Components/SyncResolutionMenu'
|
||||
import { SyncResolutionMenu } from '@/Components/SyncResolutionMenu/SyncResolutionMenu'
|
||||
import { Fragment } from 'preact'
|
||||
|
||||
type Props = {
|
||||
@@ -118,7 +118,6 @@ export class Footer extends PureComponent<Props, State> {
|
||||
this.reloadUpgradeStatus()
|
||||
this.updateOfflineStatus()
|
||||
this.findErrors()
|
||||
this.streamItems()
|
||||
}
|
||||
|
||||
reloadUser() {
|
||||
@@ -217,10 +216,6 @@ export class Footer extends PureComponent<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
streamItems() {
|
||||
this.application.items.setDisplayOptions(ContentType.Theme, CollectionSort.Title, 'asc')
|
||||
}
|
||||
|
||||
updateSyncStatus() {
|
||||
const statusManager = this.application.status
|
||||
const syncStatus = this.application.sync.getSyncStatus()
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ComponentChildren, FunctionComponent, VNode } from 'preact'
|
||||
import { forwardRef, Ref } from 'preact/compat'
|
||||
import { JSXInternal } from 'preact/src/jsx'
|
||||
import { Icon } from '@/Components/Icon/Icon'
|
||||
import { Switch, SwitchProps } from '@/Components/Switch'
|
||||
import { Switch, SwitchProps } from '@/Components/Switch/Switch'
|
||||
import { IconType } from '@standardnotes/snjs'
|
||||
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants'
|
||||
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
import { WebApplication } from '@/UIModels/Application'
|
||||
import { KeyboardKey } from '@/Services/IOService'
|
||||
import { AppState } from '@/UIModels/AppState'
|
||||
import { DisplayOptions } from '@/UIModels/AppState/NotesViewState'
|
||||
import { SNNote, SNTag } from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { NotesListItem } from './NotesListItem'
|
||||
import { FOCUSABLE_BUT_NOT_TABBABLE, NOTES_LIST_SCROLL_THRESHOLD } from '@/Constants'
|
||||
import { useCallback } from 'preact/hooks'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
appState: AppState
|
||||
notes: SNNote[]
|
||||
selectedNotes: Record<string, SNNote>
|
||||
displayOptions: DisplayOptions
|
||||
paginate: () => void
|
||||
}
|
||||
|
||||
export const NotesList: FunctionComponent<Props> = observer(
|
||||
({ application, appState, notes, selectedNotes, displayOptions, paginate }) => {
|
||||
const selectNextNote = useCallback(() => appState.notesView.selectNextNote(), [appState])
|
||||
const selectPreviousNote = useCallback(() => appState.notesView.selectPreviousNote(), [appState])
|
||||
|
||||
const { hideTags, hideDate, hideNotePreview, hideEditorIcon, sortBy } = displayOptions
|
||||
|
||||
const tagsForNote = useCallback(
|
||||
(note: SNNote): string[] => {
|
||||
if (hideTags) {
|
||||
return []
|
||||
}
|
||||
const selectedTag = appState.tags.selected
|
||||
if (!selectedTag) {
|
||||
return []
|
||||
}
|
||||
const tags = appState.getNoteTags(note)
|
||||
if (selectedTag instanceof SNTag && tags.length === 1) {
|
||||
return []
|
||||
}
|
||||
return tags.map((tag) => tag.title).sort()
|
||||
},
|
||||
[appState, hideTags],
|
||||
)
|
||||
|
||||
const openNoteContextMenu = useCallback(
|
||||
(posX: number, posY: number) => {
|
||||
appState.notes.setContextMenuClickLocation({
|
||||
x: posX,
|
||||
y: posY,
|
||||
})
|
||||
appState.notes.reloadContextMenuLayout()
|
||||
appState.notes.setContextMenuOpen(true)
|
||||
},
|
||||
[appState],
|
||||
)
|
||||
|
||||
const onContextMenu = useCallback(
|
||||
(note: SNNote, posX: number, posY: number) => {
|
||||
appState.notes.selectNote(note.uuid, true).catch(console.error)
|
||||
openNoteContextMenu(posX, posY)
|
||||
},
|
||||
[appState, openNoteContextMenu],
|
||||
)
|
||||
|
||||
const onScroll = useCallback(
|
||||
(e: Event) => {
|
||||
const offset = NOTES_LIST_SCROLL_THRESHOLD
|
||||
const element = e.target as HTMLElement
|
||||
if (element.scrollTop + element.offsetHeight >= element.scrollHeight - offset) {
|
||||
paginate()
|
||||
}
|
||||
},
|
||||
[paginate],
|
||||
)
|
||||
|
||||
const onKeyDown = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
if (e.key === KeyboardKey.Up) {
|
||||
e.preventDefault()
|
||||
selectPreviousNote()
|
||||
} else if (e.key === KeyboardKey.Down) {
|
||||
e.preventDefault()
|
||||
selectNextNote()
|
||||
}
|
||||
},
|
||||
[selectNextNote, selectPreviousNote],
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
className="infinite-scroll focus:shadow-none focus:outline-none"
|
||||
id="notes-scrollable"
|
||||
onScroll={onScroll}
|
||||
onKeyDown={onKeyDown}
|
||||
tabIndex={FOCUSABLE_BUT_NOT_TABBABLE}
|
||||
>
|
||||
{notes.map((note) => (
|
||||
<NotesListItem
|
||||
application={application}
|
||||
key={note.uuid}
|
||||
note={note}
|
||||
tags={tagsForNote(note)}
|
||||
selected={!!selectedNotes[note.uuid]}
|
||||
hideDate={hideDate}
|
||||
hidePreview={hideNotePreview}
|
||||
hideTags={hideTags}
|
||||
hideEditorIcon={hideEditorIcon}
|
||||
sortedBy={sortBy}
|
||||
onClick={() => {
|
||||
appState.notes.selectNote(note.uuid, true).catch(console.error)
|
||||
}}
|
||||
onContextMenu={(e: MouseEvent) => {
|
||||
e.preventDefault()
|
||||
onContextMenu(note, e.clientX, e.clientY)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
)
|
||||
@@ -1,144 +0,0 @@
|
||||
import { WebApplication } from '@/UIModels/Application'
|
||||
import { CollectionSort, CollectionSortProperty, sanitizeHtmlString, SNNote } from '@standardnotes/snjs'
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { Icon } from '@/Components/Icon/Icon'
|
||||
import { PLAIN_EDITOR_NAME } from '@/Constants'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
note: SNNote
|
||||
tags: string[]
|
||||
hideDate: boolean
|
||||
hidePreview: boolean
|
||||
hideTags: boolean
|
||||
hideEditorIcon: boolean
|
||||
onClick: () => void
|
||||
onContextMenu: (e: MouseEvent) => void
|
||||
selected: boolean
|
||||
sortedBy?: CollectionSortProperty
|
||||
}
|
||||
|
||||
type NoteFlag = {
|
||||
text: string
|
||||
class: 'info' | 'neutral' | 'warning' | 'success' | 'danger'
|
||||
}
|
||||
|
||||
const flagsForNote = (note: SNNote) => {
|
||||
const flags = [] as NoteFlag[]
|
||||
if (note.conflictOf) {
|
||||
flags.push({
|
||||
text: 'Conflicted Copy',
|
||||
class: 'danger',
|
||||
})
|
||||
}
|
||||
|
||||
return flags
|
||||
}
|
||||
|
||||
export const NotesListItem: FunctionComponent<Props> = ({
|
||||
application,
|
||||
hideDate,
|
||||
hidePreview,
|
||||
hideTags,
|
||||
hideEditorIcon,
|
||||
note,
|
||||
onClick,
|
||||
onContextMenu,
|
||||
selected,
|
||||
sortedBy,
|
||||
tags,
|
||||
}) => {
|
||||
const flags = flagsForNote(note)
|
||||
const hasFiles = application.items.getFilesForNote(note).length > 0
|
||||
const showModifiedDate = sortedBy === CollectionSort.UpdatedAt
|
||||
const editorForNote = application.componentManager.editorForNote(note)
|
||||
const editorName = editorForNote?.name ?? PLAIN_EDITOR_NAME
|
||||
const [icon, tint] = application.iconsController.getIconAndTintForNoteType(editorForNote?.package_info.note_type)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`note ${selected ? 'selected' : ''}`}
|
||||
id={`note-${note.uuid}`}
|
||||
onClick={onClick}
|
||||
onContextMenu={onContextMenu}
|
||||
>
|
||||
{!hideEditorIcon && (
|
||||
<div className="icon">
|
||||
<Icon ariaLabel={`Icon for ${editorName}`} type={icon} className={`color-accessory-tint-${tint}`} />
|
||||
</div>
|
||||
)}
|
||||
<div className={`meta ${hideEditorIcon ? 'icon-hidden' : ''}`}>
|
||||
<div className="name-container">{note.title.length ? <div className="name">{note.title}</div> : null}</div>
|
||||
{!hidePreview && !note.hidePreview && !note.protected && (
|
||||
<div className="note-preview">
|
||||
{note.preview_html && (
|
||||
<div
|
||||
className="html-preview"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: sanitizeHtmlString(note.preview_html),
|
||||
}}
|
||||
></div>
|
||||
)}
|
||||
{!note.preview_html && note.preview_plain && <div className="plain-preview">{note.preview_plain}</div>}
|
||||
{!note.preview_html && !note.preview_plain && note.text && (
|
||||
<div className="default-preview">{note.text}</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!hideDate || note.protected ? (
|
||||
<div className="bottom-info faded">
|
||||
{note.protected && <span>Protected {hideDate ? '' : ' • '}</span>}
|
||||
{!hideDate && showModifiedDate && <span>Modified {note.updatedAtString || 'Now'}</span>}
|
||||
{!hideDate && !showModifiedDate && <span>{note.createdAtString || 'Now'}</span>}
|
||||
</div>
|
||||
) : null}
|
||||
{!hideTags && tags.length ? (
|
||||
<div className="tags-string">
|
||||
{tags.map((tag) => (
|
||||
<span className="tag color-foreground">
|
||||
<Icon type="hashtag" className="sn-icon--small color-grey-1 mr-1" />
|
||||
<span>{tag}</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
{flags.length ? (
|
||||
<div className="note-flags flex flex-wrap">
|
||||
{flags.map((flag) => (
|
||||
<div className={`flag ${flag.class}`}>
|
||||
<div className="label">{flag.text}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flag-icons">
|
||||
{note.locked && (
|
||||
<span title="Editing Disabled">
|
||||
<Icon ariaLabel="Editing Disabled" type="pencil-off" className="sn-icon--small color-info" />
|
||||
</span>
|
||||
)}
|
||||
{note.trashed && (
|
||||
<span title="Trashed">
|
||||
<Icon ariaLabel="Trashed" type="trash-filled" className="sn-icon--small color-danger" />
|
||||
</span>
|
||||
)}
|
||||
{note.archived && (
|
||||
<span title="Archived">
|
||||
<Icon ariaLabel="Archived" type="archive" className="sn-icon--mid color-accessory-tint-3" />
|
||||
</span>
|
||||
)}
|
||||
{note.pinned && (
|
||||
<span title="Pinned">
|
||||
<Icon ariaLabel="Pinned" type="pin-filled" className="sn-icon--small color-info" />
|
||||
</span>
|
||||
)}
|
||||
{hasFiles && (
|
||||
<span title="Files">
|
||||
<Icon ariaLabel="Files" type="attachment-file" className="sn-icon--small color-info" />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AppState } from '@/UIModels/AppState'
|
||||
import { Icon } from '@/Components/Icon/Icon'
|
||||
import { Switch } from '@/Components/Switch'
|
||||
import { Switch } from '@/Components/Switch/Switch'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useState, useEffect, useMemo, useCallback } from 'preact/hooks'
|
||||
import { SNApplication, SNNote } from '@standardnotes/snjs'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Dropdown, DropdownItem } from '@/Components/Dropdown/Dropdown'
|
||||
import { usePremiumModal } from '@/Hooks/usePremiumModal'
|
||||
import { HorizontalSeparator } from '@/Components/Shared/HorizontalSeparator'
|
||||
import { Switch } from '@/Components/Switch'
|
||||
import { Switch } from '@/Components/Switch/Switch'
|
||||
import { WebApplication } from '@/UIModels/Application'
|
||||
import { ContentType, FeatureIdentifier, FeatureStatus, PrefKey, GetFeatures, SNTheme } from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
@@ -43,8 +43,9 @@ export const Appearance: FunctionComponent<Props> = observer(({ application }) =
|
||||
|
||||
useEffect(() => {
|
||||
const themesAsItems: DropdownItem[] = application.items
|
||||
.getDisplayableItems<SNTheme>(ContentType.Theme)
|
||||
.filter((theme) => !theme.isLayerable())
|
||||
.getDisplayableComponents()
|
||||
.filter((component) => component.isTheme())
|
||||
.filter((component) => !(component as SNTheme).isLayerable())
|
||||
.sort(sortThemes)
|
||||
.map((theme) => {
|
||||
return {
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
} from '@standardnotes/snjs'
|
||||
import { FunctionComponent } from 'preact'
|
||||
|
||||
import { Switch } from '@/Components/Switch'
|
||||
import { Switch } from '@/Components/Switch/Switch'
|
||||
import { convertStringifiedBooleanToBoolean } from '@/Utils'
|
||||
import { STRING_FAILED_TO_UPDATE_USER_SETTING } from '@/Strings'
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
Title,
|
||||
} from '@/Components/Preferences/PreferencesComponents'
|
||||
import { Dropdown, DropdownItem } from '@/Components/Dropdown/Dropdown'
|
||||
import { Switch } from '@/Components/Switch'
|
||||
import { Switch } from '@/Components/Switch/Switch'
|
||||
import { HorizontalSeparator } from '@/Components/Shared/HorizontalSeparator'
|
||||
import {
|
||||
FeatureStatus,
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
} from '@/Components/Preferences/PreferencesComponents'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'preact/hooks'
|
||||
import { Button } from '@/Components/Button/Button'
|
||||
import { Switch } from '@/Components/Switch'
|
||||
import { Switch } from '@/Components/Switch/Switch'
|
||||
import { HorizontalSeparator } from '@/Components/Shared/HorizontalSeparator'
|
||||
import { EncryptionStatusItem } from '../../Security/Encryption'
|
||||
import { Icon } from '@/Components/Icon/Icon'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { SNComponent } from '@standardnotes/snjs'
|
||||
import { PreferencesSegment, SubtitleLight } from '@/Components/Preferences/PreferencesComponents'
|
||||
import { Switch } from '@/Components/Switch'
|
||||
import { Switch } from '@/Components/Switch/Switch'
|
||||
import { WebApplication } from '@/UIModels/Application'
|
||||
import { useState } from 'preact/hooks'
|
||||
import { Button } from '@/Components/Button/Button'
|
||||
|
||||
@@ -11,7 +11,7 @@ import { WebApplication } from '@/UIModels/Application'
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { useEffect, useState } from 'preact/hooks'
|
||||
import { HorizontalSeparator } from '@/Components/Shared/HorizontalSeparator'
|
||||
import { Switch } from '@/Components/Switch'
|
||||
import { Switch } from '@/Components/Switch/Switch'
|
||||
import { PLAIN_EDITOR_NAME } from '@/Constants'
|
||||
|
||||
type Props = {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Switch } from '@/Components/Switch'
|
||||
import { Switch } from '@/Components/Switch/Switch'
|
||||
import {
|
||||
PreferencesGroup,
|
||||
PreferencesSegment,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { HorizontalSeparator } from '@/Components/Shared/HorizontalSeparator'
|
||||
import { Switch } from '@/Components/Switch'
|
||||
import { Switch } from '@/Components/Switch/Switch'
|
||||
import {
|
||||
PreferencesGroup,
|
||||
PreferencesSegment,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { HorizontalSeparator } from '@/Components/Shared/HorizontalSeparator'
|
||||
import { Switch } from '@/Components/Switch'
|
||||
import { Switch } from '@/Components/Switch/Switch'
|
||||
import {
|
||||
PreferencesGroup,
|
||||
PreferencesSegment,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { Title, Text, PreferencesGroup, PreferencesSegment } from '@/Components/Preferences/PreferencesComponents'
|
||||
import { Switch } from '@/Components/Switch'
|
||||
import { Switch } from '@/Components/Switch/Switch'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { is2FAActivation, is2FADisabled, TwoFactorAuth } from './TwoFactorAuth'
|
||||
import { TwoFactorActivationView } from './TwoFactorActivationView'
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useCallback } from 'preact/hooks'
|
||||
import { JSXInternal } from 'preact/src/jsx'
|
||||
import { Icon } from '@/Components/Icon/Icon'
|
||||
import { usePremiumModal } from '@/Hooks/usePremiumModal'
|
||||
import { Switch } from '@/Components/Switch'
|
||||
import { Switch } from '@/Components/Switch/Switch'
|
||||
|
||||
type Props = {
|
||||
application: WebApplication
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { WebApplication } from '@/UIModels/Application'
|
||||
import { AppState } from '@/UIModels/AppState'
|
||||
import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure'
|
||||
import { ComponentArea, ContentType, FeatureIdentifier, GetFeatures, SNComponent, SNTheme } from '@standardnotes/snjs'
|
||||
import { ComponentArea, ContentType, FeatureIdentifier, GetFeatures, SNComponent } from '@standardnotes/snjs'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { FunctionComponent } from 'preact'
|
||||
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
|
||||
import { JSXInternal } from 'preact/src/jsx'
|
||||
import { Icon } from '@/Components/Icon/Icon'
|
||||
import { Switch } from '@/Components/Switch'
|
||||
import { Switch } from '@/Components/Switch/Switch'
|
||||
import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur'
|
||||
import { quickSettingsKeyDownHandler, themesMenuKeyDownHandler } from './EventHandlers'
|
||||
import { FocusModeSwitch } from './FocusModeSwitch'
|
||||
@@ -18,7 +18,7 @@ import { sortThemes } from '@/Utils/SortThemes'
|
||||
|
||||
const focusModeAnimationDuration = 1255
|
||||
|
||||
const MENU_CLASSNAME = 'sn-menu-border sn-dropdown min-w-80 max-h-120 max-w-xs flex flex-col py-2 overflow-y-auto'
|
||||
const MENU_CLASSNAME = 'sn-dropdown min-w-80 max-h-120 max-w-xs flex flex-col py-2 overflow-y-auto'
|
||||
|
||||
type MenuProps = {
|
||||
appState: AppState
|
||||
@@ -65,13 +65,16 @@ export const QuickSettingsMenu: FunctionComponent<MenuProps> = observer(({ appli
|
||||
}, [focusModeEnabled])
|
||||
|
||||
const reloadThemes = useCallback(() => {
|
||||
const themes = application.items.getDisplayableItems<SNTheme>(ContentType.Theme).map((item) => {
|
||||
return {
|
||||
name: item.displayName,
|
||||
identifier: item.identifier,
|
||||
component: item,
|
||||
}
|
||||
}) as ThemeItem[]
|
||||
const themes = application.items
|
||||
.getDisplayableComponents()
|
||||
.filter((component) => component.isTheme())
|
||||
.map((item) => {
|
||||
return {
|
||||
name: item.displayName,
|
||||
identifier: item.identifier,
|
||||
component: item,
|
||||
}
|
||||
}) as ThemeItem[]
|
||||
|
||||
GetFeatures()
|
||||
.filter((feature) => feature.content_type === ContentType.Theme && !feature.layerable)
|
||||
@@ -91,9 +94,10 @@ export const QuickSettingsMenu: FunctionComponent<MenuProps> = observer(({ appli
|
||||
|
||||
const reloadToggleableComponents = useCallback(() => {
|
||||
const toggleableComponents = application.items
|
||||
.getDisplayableItems<SNComponent>(ContentType.Component)
|
||||
.getDisplayableComponents()
|
||||
.filter(
|
||||
(component) =>
|
||||
!component.isTheme() &&
|
||||
[ComponentArea.EditorStack].includes(component.area) &&
|
||||
component.identifier !== FeatureIdentifier.DeprecatedFoldersComponent,
|
||||
)
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useCallback, useMemo } from 'preact/hooks'
|
||||
import { JSXInternal } from 'preact/src/jsx'
|
||||
import { Icon } from '@/Components/Icon/Icon'
|
||||
import { usePremiumModal } from '@/Hooks/usePremiumModal'
|
||||
import { Switch } from '@/Components/Switch'
|
||||
import { Switch } from '@/Components/Switch/Switch'
|
||||
import { ThemeItem } from './ThemeItem'
|
||||
|
||||
type Props = {
|
||||
|
||||
@@ -57,13 +57,17 @@ export const RevisionHistoryModal: FunctionComponent<RevisionHistoryModalProps>
|
||||
({ application, appState }) => {
|
||||
const closeButtonRef = useRef<HTMLButtonElement>(null)
|
||||
|
||||
const dismissModal = () => {
|
||||
const dismissModal = useCallback(() => {
|
||||
appState.notes.setShowRevisionHistoryModal(false)
|
||||
}
|
||||
}, [appState.notes])
|
||||
|
||||
const note = Object.values(appState.notes.selectedNotes)[0]
|
||||
const note = appState.notes.firstSelectedNote
|
||||
const editorForCurrentNote = useMemo(() => {
|
||||
return application.componentManager.editorForNote(note)
|
||||
if (note) {
|
||||
return application.componentManager.editorForNote(note)
|
||||
} else {
|
||||
return undefined
|
||||
}
|
||||
}, [application, note])
|
||||
|
||||
const [isFetchingSelectedRevision, setIsFetchingSelectedRevision] = useState(false)
|
||||
@@ -100,7 +104,7 @@ export const RevisionHistoryModal: FunctionComponent<RevisionHistoryModalProps>
|
||||
}
|
||||
}, [fetchRemoteHistory, remoteHistory?.length])
|
||||
|
||||
const restore = () => {
|
||||
const restore = useCallback(() => {
|
||||
if (selectedRevision) {
|
||||
const originalNote = application.items.findItem(selectedRevision.payload.uuid) as SNNote
|
||||
|
||||
@@ -130,9 +134,9 @@ export const RevisionHistoryModal: FunctionComponent<RevisionHistoryModalProps>
|
||||
})
|
||||
.catch(console.error)
|
||||
}
|
||||
}
|
||||
}, [application.alertService, application.items, application.mutator, dismissModal, selectedRevision])
|
||||
|
||||
const restoreAsCopy = async () => {
|
||||
const restoreAsCopy = useCallback(async () => {
|
||||
if (selectedRevision) {
|
||||
const originalNote = application.items.findSureItem<SNNote>(selectedRevision.payload.uuid)
|
||||
|
||||
@@ -143,11 +147,11 @@ export const RevisionHistoryModal: FunctionComponent<RevisionHistoryModalProps>
|
||||
: undefined,
|
||||
})
|
||||
|
||||
appState.notes.selectNote(duplicatedItem.uuid).catch(console.error)
|
||||
appState.selectedItems.selectItem(duplicatedItem.uuid).catch(console.error)
|
||||
|
||||
dismissModal()
|
||||
}
|
||||
}
|
||||
}, [appState.selectedItems, application.items, application.mutator, dismissModal, selectedRevision])
|
||||
|
||||
useEffect(() => {
|
||||
const fetchTemplateNote = async () => {
|
||||
@@ -164,7 +168,7 @@ export const RevisionHistoryModal: FunctionComponent<RevisionHistoryModalProps>
|
||||
fetchTemplateNote().catch(console.error)
|
||||
}, [application, selectedRevision])
|
||||
|
||||
const deleteSelectedRevision = () => {
|
||||
const deleteSelectedRevision = useCallback(() => {
|
||||
if (!selectedRemoteEntry) {
|
||||
return
|
||||
}
|
||||
@@ -178,7 +182,7 @@ export const RevisionHistoryModal: FunctionComponent<RevisionHistoryModalProps>
|
||||
'Cancel',
|
||||
)
|
||||
.then((shouldDelete) => {
|
||||
if (shouldDelete) {
|
||||
if (shouldDelete && note) {
|
||||
setIsDeletingRevision(true)
|
||||
|
||||
application.historyManager
|
||||
@@ -195,7 +199,7 @@ export const RevisionHistoryModal: FunctionComponent<RevisionHistoryModalProps>
|
||||
}
|
||||
})
|
||||
.catch(console.error)
|
||||
}
|
||||
}, [application.alertService, application.historyManager, fetchRemoteHistory, note, selectedRemoteEntry])
|
||||
|
||||
return (
|
||||
<DialogOverlay
|
||||
@@ -210,7 +214,7 @@ export const RevisionHistoryModal: FunctionComponent<RevisionHistoryModalProps>
|
||||
width: '90%',
|
||||
maxWidth: '90%',
|
||||
minHeight: '90%',
|
||||
background: 'var(--sn-stylekit-background-color)',
|
||||
background: 'var(--modal-background-color)',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
@@ -219,16 +223,18 @@ export const RevisionHistoryModal: FunctionComponent<RevisionHistoryModalProps>
|
||||
}`}
|
||||
>
|
||||
<div className="flex flex-grow min-h-0">
|
||||
<HistoryListContainer
|
||||
application={application}
|
||||
note={note}
|
||||
remoteHistory={remoteHistory}
|
||||
isFetchingRemoteHistory={isFetchingRemoteHistory}
|
||||
setSelectedRevision={setSelectedRevision}
|
||||
setSelectedRemoteEntry={setSelectedRemoteEntry}
|
||||
setShowContentLockedScreen={setShowContentLockedScreen}
|
||||
setIsFetchingSelectedRevision={setIsFetchingSelectedRevision}
|
||||
/>
|
||||
{note && (
|
||||
<HistoryListContainer
|
||||
application={application}
|
||||
note={note}
|
||||
remoteHistory={remoteHistory}
|
||||
isFetchingRemoteHistory={isFetchingRemoteHistory}
|
||||
setSelectedRevision={setSelectedRevision}
|
||||
setSelectedRemoteEntry={setSelectedRemoteEntry}
|
||||
setShowContentLockedScreen={setShowContentLockedScreen}
|
||||
setIsFetchingSelectedRevision={setIsFetchingSelectedRevision}
|
||||
/>
|
||||
)}
|
||||
<div className={'flex flex-col flex-grow relative'}>
|
||||
<RevisionContentPlaceholder
|
||||
selectedRevision={selectedRevision}
|
||||
|
||||
@@ -20,6 +20,9 @@ const smartViewIconType = (view: SmartView): IconType => {
|
||||
if (view.uuid === SystemViewId.AllNotes) {
|
||||
return 'notes'
|
||||
}
|
||||
if (view.uuid === SystemViewId.Files) {
|
||||
return 'file'
|
||||
}
|
||||
if (view.uuid === SystemViewId.ArchivedNotes) {
|
||||
return 'archive'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user