Skip to content

Commit

Permalink
feat: generic items list (#1035)
Browse files Browse the repository at this point in the history
  • Loading branch information
amanharwara authored May 22, 2022
1 parent 1643311 commit 6401da2
Show file tree
Hide file tree
Showing 76 changed files with 1,803 additions and 1,276 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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
Expand Down Expand Up @@ -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>

Expand All @@ -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}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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({
Expand All @@ -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))
}
Expand Down Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
156 changes: 81 additions & 75 deletions app/assets/javascripts/Components/ChangeEditor/ChangeEditorMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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/, '-')
Expand Down Expand Up @@ -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
})
.catch(console.error)
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[] = []
const transactions: TransactionalMutation[] = []

await application.getAppState().notesView.insertCurrentIfTemplate()
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
},
})
}
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))
if (note.locked) {
application.alertService.alert(STRING_EDIT_LOCKED_ATTEMPT).catch(console.error)
return
}
const prefersPlain = note.prefersPlainEditor
if (prefersPlain) {
transactions.push({
itemUuid: note.uuid,
mutate: (m: ItemMutator) => {
const noteMutator = m as NoteMutator
noteMutator.prefersPlainEditor = false
},
})

if (!component) {
if (!note.prefersPlainEditor) {
transactions.push({
itemUuid: note.uuid,
mutate: (m: ItemMutator) => {
const noteMutator = m as NoteMutator
noteMutator.prefersPlainEditor = true
},
})
}
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))
}
transactions.push(transactionForAssociateComponentWithCurrentNote(component, note))
}

await application.mutator.runTransactionalMutations(transactions)
/** Dirtying can happen above */
application.sync.sync().catch(console.error)
await application.mutator.runTransactionalMutations(transactions)
/** Dirtying can happen above */
application.sync.sync().catch(console.error)

setCurrentEditor(application.componentManager.editorForNote(note))
}
setCurrentEditor(application.componentManager.editorForNote(note))
},
[application],
)

const selectEditor = async (itemToBeSelected: EditorMenuItem) => {
if (!itemToBeSelected.isEntitled) {
premiumModal.activate(itemToBeSelected.name)
return
}
const selectEditor = useCallback(
async (itemToBeSelected: EditorMenuItem) => {
if (!itemToBeSelected.isEntitled) {
premiumModal.activate(itemToBeSelected.name)
return
}

const areBothEditorsPlain = !currentEditor && !itemToBeSelected.component
const areBothEditorsPlain = !currentEditor && !itemToBeSelected.component

if (areBothEditorsPlain) {
return
}
if (areBothEditorsPlain) {
return
}

let shouldSelectEditor = true
let shouldSelectEditor = true

if (itemToBeSelected.component) {
const changeRequiresAlert = application.componentManager.doesEditorChangeRequireAlert(
currentEditor,
itemToBeSelected.component,
)
if (itemToBeSelected.component) {
const changeRequiresAlert = application.componentManager.doesEditorChangeRequireAlert(
currentEditor,
itemToBeSelected.component,
)

if (changeRequiresAlert) {
shouldSelectEditor = await application.componentManager.showEditorChangeAlert()
if (changeRequiresAlert) {
shouldSelectEditor = await application.componentManager.showEditorChangeAlert()
}
}
}

if (shouldSelectEditor) {
selectComponent(itemToBeSelected.component ?? null, note).catch(console.error)
}
if (shouldSelectEditor && note) {
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}>
Expand Down
75 changes: 75 additions & 0 deletions app/assets/javascripts/Components/ContentListView/ContentList.tsx
Original file line number Diff line number Diff line change
@@ -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>
)
},
)
Loading

0 comments on commit 6401da2

Please sign in to comment.