Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: generic items list #1035

Merged
merged 49 commits into from
May 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
2008511
feat: make notes list generic to display files
amanharwara May 18, 2022
88940b6
fix: file context menu
amanharwara May 19, 2022
b943bc6
chore: remove unnecessary console log
amanharwara May 19, 2022
85360a1
feat: prepare for snjs#743
amanharwara May 20, 2022
aa39321
Merge branch 'develop' of github.com:standardnotes/web into feat/gene…
moughxyz May 20, 2022
aca8086
fix: display options type compatibility
moughxyz May 20, 2022
582a8ee
fix: make webDisplayOptions observable
moughxyz May 20, 2022
2f85df4
refactor: fix file names
amanharwara May 20, 2022
a476b9f
Merge branch 'feat/generic-items-list' of github.com:standardnotes/we…
amanharwara May 20, 2022
79400f1
fix: file selection issues
amanharwara May 20, 2022
fa1844e
chore: merge develop
moughxyz May 20, 2022
e12af41
Merge branch 'feat/generic-items-list' of github.com:standardnotes/we…
moughxyz May 20, 2022
e68fae2
Merge branch 'develop' of github.com:standardnotes/web into feat/gene…
amanharwara May 20, 2022
bd3b1b6
feat: use single-click to preview file list item
amanharwara May 20, 2022
dd267fb
feat(wip): theming improvements
moughxyz May 13, 2022
102be38
feat: extend color variables available to themes
moughxyz May 21, 2022
f9ab039
feat: use file icon for files smart view
amanharwara May 21, 2022
7e40ed7
fix: file item authorization
amanharwara May 21, 2022
d1070b7
fix: note/file list item hover
amanharwara May 21, 2022
977a23f
Merge branch 'feat/generic-items-list' of github.com:standardnotes/we…
amanharwara May 21, 2022
e2abe2f
Revert "fix: file item authorization"
amanharwara May 21, 2022
6f97720
chore: bump deps
amanharwara May 21, 2022
cbf9156
feat: use useCallback
amanharwara May 21, 2022
812ace6
feat: consolidate item stream
amanharwara May 21, 2022
3967284
refactor: use object-based params
amanharwara May 21, 2022
f3803d1
refactor: move long function invocation above jsx
amanharwara May 21, 2022
4c46f0f
feat: use better css variable
amanharwara May 21, 2022
8c2b41f
feat: use generic protected access authorization
amanharwara May 21, 2022
0956c5f
feat: expose getSelectedItems function in SelectedItemsState
amanharwara May 21, 2022
d22740f
feat: use named return value
amanharwara May 21, 2022
794efd3
refactor: conditional improvements
amanharwara May 21, 2022
fce1f5c
feat: expose firstSelectedNote getter
amanharwara May 21, 2022
760e6b2
feat: remove unnecessary reload calls
amanharwara May 21, 2022
0bc0fde
fix: type errors
amanharwara May 21, 2022
fc76e2b
refactor: better control flow and props
amanharwara May 21, 2022
d3e1a94
refactor: remove unnecessary prop
amanharwara May 21, 2022
5fed838
refactor: get only required properties
amanharwara May 21, 2022
0edb7dc
refactor: move conditional to named const
amanharwara May 21, 2022
fb66083
feat: make getSelectedItems generic
amanharwara May 21, 2022
4a76e84
feat: return didSelect after selecting item
amanharwara May 21, 2022
a2c19ba
feat: support optional files navigation
moughxyz May 22, 2022
e0d4e09
fix: web display options
amanharwara May 22, 2022
6b26000
feat: add new utility class
amanharwara May 22, 2022
a02c5c5
refactor: pass whole item for params
amanharwara May 22, 2022
6bd94f5
refactor: use early returns
amanharwara May 22, 2022
8661188
fix: hidden icon styling
amanharwara May 22, 2022
d92c20b
chore: bump deps
amanharwara May 22, 2022
36719e0
refactor: rename to AbstractListItemProps
amanharwara May 22, 2022
3a77a6e
feat: add file selection const
amanharwara May 22, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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