feat: generic items list (#1035)
This commit is contained in:
@@ -7,7 +7,6 @@ import {
|
||||
ContentType,
|
||||
DeinitSource,
|
||||
PrefKey,
|
||||
SNNote,
|
||||
SNTag,
|
||||
removeFromArray,
|
||||
WebOrDesktopDeviceInterface,
|
||||
@@ -17,7 +16,7 @@ import { ActionsMenuState } from './ActionsMenuState'
|
||||
import { FeaturesState } from './FeaturesState'
|
||||
import { FilesState } from './FilesState'
|
||||
import { NotesState } from './NotesState'
|
||||
import { NotesViewState } from './NotesViewState'
|
||||
import { ContentListViewState } from './ContentListViewState'
|
||||
import { NoteTagsState } from './NoteTagsState'
|
||||
import { NoAccountWarningState } from './NoAccountWarningState'
|
||||
import { PreferencesState } from './PreferencesState'
|
||||
@@ -29,6 +28,8 @@ import { SyncState } from './SyncState'
|
||||
import { TagsState } from './TagsState'
|
||||
import { FilePreviewModalState } from './FilePreviewModalState'
|
||||
import { AbstractState } from './AbstractState'
|
||||
import { SelectedItemsState } from './SelectedItemsState'
|
||||
import { ListableContentItem } from '@/Components/ContentListView/Types/ListableContentItem'
|
||||
|
||||
export enum AppStateEvent {
|
||||
TagChanged,
|
||||
@@ -70,7 +71,7 @@ export class AppState extends AbstractState {
|
||||
readonly files: FilesState
|
||||
readonly noAccountWarning: NoAccountWarningState
|
||||
readonly notes: NotesState
|
||||
readonly notesView: NotesViewState
|
||||
readonly contentListView: ContentListViewState
|
||||
readonly noteTags: NoteTagsState
|
||||
readonly preferences = new PreferencesState()
|
||||
readonly purchaseFlow: PurchaseFlowState
|
||||
@@ -79,6 +80,7 @@ export class AppState extends AbstractState {
|
||||
readonly subscription: SubscriptionState
|
||||
readonly sync = new SyncState()
|
||||
readonly tags: TagsState
|
||||
readonly selectedItems: SelectedItemsState
|
||||
|
||||
isSessionsModalVisible = false
|
||||
|
||||
@@ -89,6 +91,7 @@ export class AppState extends AbstractState {
|
||||
constructor(application: WebApplication, private device: WebOrDesktopDeviceInterface) {
|
||||
super(application)
|
||||
|
||||
this.selectedItems = new SelectedItemsState(application, this, this.appEventObserverRemovers)
|
||||
this.notes = new NotesState(
|
||||
application,
|
||||
this,
|
||||
@@ -106,8 +109,8 @@ export class AppState extends AbstractState {
|
||||
this.searchOptions = new SearchOptionsState(application, this.appEventObserverRemovers)
|
||||
this.subscription = new SubscriptionState(application, this.appEventObserverRemovers)
|
||||
this.purchaseFlow = new PurchaseFlowState(application)
|
||||
this.notesView = new NotesViewState(application, this, this.appEventObserverRemovers)
|
||||
this.files = new FilesState(application)
|
||||
this.contentListView = new ContentListViewState(application, this, this.appEventObserverRemovers)
|
||||
this.files = new FilesState(application, this, this.appEventObserverRemovers)
|
||||
this.addAppEventObserver()
|
||||
this.onVisibilityChange = () => {
|
||||
const visible = document.visibilityState === 'visible'
|
||||
@@ -177,8 +180,8 @@ export class AppState extends AbstractState {
|
||||
this.notes.deinit(source)
|
||||
;(this.notes as unknown) = undefined
|
||||
|
||||
this.notesView.deinit(source)
|
||||
;(this.notesView as unknown) = undefined
|
||||
this.contentListView.deinit(source)
|
||||
;(this.contentListView as unknown) = undefined
|
||||
|
||||
this.noteTags.deinit(source)
|
||||
;(this.noteTags as unknown) = undefined
|
||||
@@ -321,8 +324,8 @@ export class AppState extends AbstractState {
|
||||
}
|
||||
|
||||
/** Returns the tags that are referncing this note */
|
||||
public getNoteTags(note: SNNote) {
|
||||
return this.application.items.itemsReferencingItem(note).filter((ref) => {
|
||||
public getItemTags(item: ListableContentItem) {
|
||||
return this.application.items.itemsReferencingItem(item).filter((ref) => {
|
||||
return ref.content_type === ContentType.Tag
|
||||
}) as SNTag[]
|
||||
}
|
||||
|
||||
@@ -1,72 +1,64 @@
|
||||
import { ListableContentItem } from '@/Components/ContentListView/Types/ListableContentItem'
|
||||
import { destroyAllObjectProperties } from '@/Utils'
|
||||
import {
|
||||
ApplicationEvent,
|
||||
CollectionSort,
|
||||
CollectionSortProperty,
|
||||
ContentType,
|
||||
DeinitSource,
|
||||
findInArray,
|
||||
NotesDisplayCriteria,
|
||||
NoteViewController,
|
||||
PrefKey,
|
||||
SmartView,
|
||||
SNNote,
|
||||
SNTag,
|
||||
SystemViewId,
|
||||
DisplayOptions,
|
||||
} from '@standardnotes/snjs'
|
||||
import { action, computed, makeObservable, observable, reaction, runInAction } from 'mobx'
|
||||
import { AppState, AppStateEvent } from '.'
|
||||
import { WebApplication } from '../Application'
|
||||
import { AbstractState } from './AbstractState'
|
||||
import { WebDisplayOptions } from './WebDisplayOptions'
|
||||
|
||||
const MIN_NOTE_CELL_HEIGHT = 51.0
|
||||
const DEFAULT_LIST_NUM_NOTES = 20
|
||||
const ELEMENT_ID_SEARCH_BAR = 'search-bar'
|
||||
const ELEMENT_ID_SCROLL_CONTAINER = 'notes-scrollable'
|
||||
const MinNoteCellHeight = 51.0
|
||||
const DefaultListNumNotes = 20
|
||||
const ElementIdSearchBar = 'search-bar'
|
||||
const ElementIdScrollContainer = 'notes-scrollable'
|
||||
const SupportsFileSelectionState = false
|
||||
|
||||
export type DisplayOptions = {
|
||||
sortBy: CollectionSortProperty
|
||||
sortReverse: boolean
|
||||
hidePinned: boolean
|
||||
showArchived: boolean
|
||||
showTrashed: boolean
|
||||
hideProtected: boolean
|
||||
hideTags: boolean
|
||||
hideNotePreview: boolean
|
||||
hideDate: boolean
|
||||
hideEditorIcon: boolean
|
||||
}
|
||||
|
||||
export class NotesViewState extends AbstractState {
|
||||
export class ContentListViewState extends AbstractState {
|
||||
completedFullSync = false
|
||||
noteFilterText = ''
|
||||
notes: SNNote[] = []
|
||||
items: ListableContentItem[] = []
|
||||
notesToDisplay = 0
|
||||
pageSize = 0
|
||||
panelTitle = 'All Notes'
|
||||
panelWidth = 0
|
||||
renderedNotes: SNNote[] = []
|
||||
renderedItems: ListableContentItem[] = []
|
||||
searchSubmitted = false
|
||||
showDisplayOptionsMenu = false
|
||||
displayOptions = {
|
||||
displayOptions: DisplayOptions = {
|
||||
sortBy: CollectionSort.CreatedAt,
|
||||
sortReverse: false,
|
||||
hidePinned: false,
|
||||
showArchived: false,
|
||||
showTrashed: false,
|
||||
hideProtected: false,
|
||||
sortDirection: 'dsc',
|
||||
includePinned: true,
|
||||
includeArchived: false,
|
||||
includeTrashed: false,
|
||||
includeProtected: true,
|
||||
}
|
||||
webDisplayOptions: WebDisplayOptions = {
|
||||
hideTags: true,
|
||||
hideDate: false,
|
||||
hideNotePreview: false,
|
||||
hideEditorIcon: false,
|
||||
}
|
||||
private reloadNotesPromise?: Promise<unknown>
|
||||
private reloadItemsPromise?: Promise<unknown>
|
||||
|
||||
override deinit(source: DeinitSource) {
|
||||
super.deinit(source)
|
||||
;(this.noteFilterText as unknown) = undefined
|
||||
;(this.notes as unknown) = undefined
|
||||
;(this.renderedNotes as unknown) = undefined
|
||||
;(this.renderedItems as unknown) = undefined
|
||||
;(window.onresize as unknown) = undefined
|
||||
|
||||
destroyAllObjectProperties(this)
|
||||
@@ -79,7 +71,7 @@ export class NotesViewState extends AbstractState {
|
||||
|
||||
appObservers.push(
|
||||
application.streamItems<SNNote>(ContentType.Note, () => {
|
||||
void this.reloadNotes()
|
||||
void this.reloadItems()
|
||||
}),
|
||||
|
||||
application.streamItems<SNTag>([ContentType.Tag], async ({ changed, inserted }) => {
|
||||
@@ -88,7 +80,7 @@ export class NotesViewState extends AbstractState {
|
||||
/** A tag could have changed its relationships, so we need to reload the filter */
|
||||
this.reloadNotesDisplayOptions()
|
||||
|
||||
void this.reloadNotes()
|
||||
void this.reloadItems()
|
||||
|
||||
if (appState.tags.selected && findInArray(tags, 'uuid', appState.tags.selected.uuid)) {
|
||||
/** Tag title could have changed */
|
||||
@@ -100,11 +92,11 @@ export class NotesViewState extends AbstractState {
|
||||
}, ApplicationEvent.PreferencesChanged),
|
||||
application.addEventObserver(async () => {
|
||||
this.application.noteControllerGroup.closeAllNoteControllers()
|
||||
void this.selectFirstNote()
|
||||
void this.selectFirstItem()
|
||||
this.setCompletedFullSync(false)
|
||||
}, ApplicationEvent.SignedIn),
|
||||
application.addEventObserver(async () => {
|
||||
void this.reloadNotes().then(() => {
|
||||
void this.reloadItems().then(() => {
|
||||
if (
|
||||
this.notes.length === 0 &&
|
||||
appState.tags.selected instanceof SmartView &&
|
||||
@@ -126,7 +118,7 @@ export class NotesViewState extends AbstractState {
|
||||
],
|
||||
() => {
|
||||
this.reloadNotesDisplayOptions()
|
||||
void this.reloadNotes()
|
||||
void this.reloadItems()
|
||||
},
|
||||
),
|
||||
|
||||
@@ -144,14 +136,15 @@ export class NotesViewState extends AbstractState {
|
||||
makeObservable(this, {
|
||||
completedFullSync: observable,
|
||||
displayOptions: observable.struct,
|
||||
webDisplayOptions: observable.struct,
|
||||
noteFilterText: observable,
|
||||
notes: observable,
|
||||
notesToDisplay: observable,
|
||||
panelTitle: observable,
|
||||
renderedNotes: observable,
|
||||
renderedItems: observable,
|
||||
showDisplayOptionsMenu: observable,
|
||||
|
||||
reloadNotes: action,
|
||||
reloadItems: action,
|
||||
reloadPanelTitle: action,
|
||||
reloadPreferences: action,
|
||||
resetPagination: action,
|
||||
@@ -186,7 +179,7 @@ export class NotesViewState extends AbstractState {
|
||||
}
|
||||
|
||||
get searchBarElement() {
|
||||
return document.getElementById(ELEMENT_ID_SEARCH_BAR)
|
||||
return document.getElementById(ElementIdSearchBar)
|
||||
}
|
||||
|
||||
get isFiltering(): boolean {
|
||||
@@ -206,17 +199,17 @@ export class NotesViewState extends AbstractState {
|
||||
this.panelTitle = title
|
||||
}
|
||||
|
||||
reloadNotes = async (): Promise<void> => {
|
||||
if (this.reloadNotesPromise) {
|
||||
await this.reloadNotesPromise
|
||||
reloadItems = async (): Promise<void> => {
|
||||
if (this.reloadItemsPromise) {
|
||||
await this.reloadItemsPromise
|
||||
}
|
||||
|
||||
this.reloadNotesPromise = this.performReloadNotes()
|
||||
this.reloadItemsPromise = this.performReloadItems()
|
||||
|
||||
await this.reloadNotesPromise
|
||||
await this.reloadItemsPromise
|
||||
}
|
||||
|
||||
private async performReloadNotes() {
|
||||
private async performReloadItems() {
|
||||
const tag = this.appState.tags.selected
|
||||
if (!tag) {
|
||||
return
|
||||
@@ -224,32 +217,43 @@ export class NotesViewState extends AbstractState {
|
||||
|
||||
const notes = this.application.items.getDisplayableNotes()
|
||||
|
||||
const renderedNotes = notes.slice(0, this.notesToDisplay)
|
||||
const items = this.application.items.getDisplayableNotesAndFiles()
|
||||
|
||||
this.notes = notes
|
||||
const renderedItems = items.slice(0, this.notesToDisplay)
|
||||
|
||||
runInAction(() => {
|
||||
this.renderedNotes = renderedNotes
|
||||
this.notes = notes
|
||||
this.items = items
|
||||
this.renderedItems = renderedItems
|
||||
})
|
||||
|
||||
await this.recomputeSelectionAfterNotesReload()
|
||||
await this.recomputeSelectionAfterItemsReload()
|
||||
|
||||
this.reloadPanelTitle()
|
||||
}
|
||||
|
||||
private async recomputeSelectionAfterNotesReload() {
|
||||
private async recomputeSelectionAfterItemsReload() {
|
||||
const appState = this.appState
|
||||
const activeController = this.getActiveNoteController()
|
||||
const activeNote = activeController?.note
|
||||
const isSearching = this.noteFilterText.length > 0
|
||||
const hasMultipleNotesSelected = appState.notes.selectedNotesCount >= 2
|
||||
const hasMultipleItemsSelected = appState.selectedItems.selectedItemsCount >= 2
|
||||
|
||||
if (hasMultipleNotesSelected) {
|
||||
if (hasMultipleItemsSelected) {
|
||||
return
|
||||
}
|
||||
|
||||
const selectedItem = Object.values(appState.selectedItems.selectedItems)[0]
|
||||
|
||||
const isSelectedItemFile =
|
||||
this.items.includes(selectedItem) && selectedItem && selectedItem.content_type === ContentType.File
|
||||
|
||||
if (isSelectedItemFile && !SupportsFileSelectionState) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!activeNote) {
|
||||
await this.selectFirstNote()
|
||||
await this.selectFirstItem()
|
||||
|
||||
return
|
||||
}
|
||||
@@ -262,7 +266,7 @@ export class NotesViewState extends AbstractState {
|
||||
if (!noteExistsInUpdatedResults && !isSearching) {
|
||||
this.closeNoteController(activeController)
|
||||
|
||||
this.selectNextNote()
|
||||
this.selectNextItem()
|
||||
|
||||
return
|
||||
}
|
||||
@@ -277,9 +281,9 @@ export class NotesViewState extends AbstractState {
|
||||
this.application.getPreference(PrefKey.NotesShowArchived, false)
|
||||
|
||||
if ((activeNote.trashed && !showTrashedNotes) || (activeNote.archived && !showArchivedNotes)) {
|
||||
await this.selectNextOrCreateNew()
|
||||
} else if (!this.appState.notes.selectedNotes[activeNote.uuid]) {
|
||||
await this.selectNoteWithScrollHandling(activeNote).catch(console.error)
|
||||
await this.selectNextItemOrCreateNewNote()
|
||||
} else if (!this.appState.selectedItems.selectedItems[activeNote.uuid]) {
|
||||
await this.appState.selectedItems.selectItem(activeNote.uuid).catch(console.error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,30 +299,32 @@ export class NotesViewState extends AbstractState {
|
||||
includeArchived = this.appState.searchOptions.includeArchived
|
||||
includeTrashed = this.appState.searchOptions.includeTrashed
|
||||
} else {
|
||||
includeArchived = this.displayOptions.showArchived ?? false
|
||||
includeTrashed = this.displayOptions.showTrashed ?? false
|
||||
includeArchived = this.displayOptions.includeArchived ?? false
|
||||
includeTrashed = this.displayOptions.includeTrashed ?? false
|
||||
}
|
||||
|
||||
const criteria = NotesDisplayCriteria.Create({
|
||||
sortProperty: this.displayOptions.sortBy,
|
||||
sortDirection: this.displayOptions.sortReverse ? 'asc' : 'dsc',
|
||||
const criteria: DisplayOptions = {
|
||||
sortBy: this.displayOptions.sortBy,
|
||||
sortDirection: this.displayOptions.sortDirection,
|
||||
tags: tag instanceof SNTag ? [tag] : [],
|
||||
views: tag instanceof SmartView ? [tag] : [],
|
||||
includeArchived,
|
||||
includeTrashed,
|
||||
includePinned: !this.displayOptions.hidePinned,
|
||||
includeProtected: !this.displayOptions.hideProtected,
|
||||
includePinned: this.displayOptions.includePinned,
|
||||
includeProtected: this.displayOptions.includeProtected,
|
||||
searchQuery: {
|
||||
query: searchText,
|
||||
includeProtectedNoteText: this.appState.searchOptions.includeProtectedContents,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
this.application.items.setNotesDisplayCriteria(criteria)
|
||||
this.application.items.setPrimaryItemDisplayOptions(criteria)
|
||||
}
|
||||
|
||||
reloadPreferences = async () => {
|
||||
const freshDisplayOptions = {} as DisplayOptions
|
||||
const newDisplayOptions = {} as DisplayOptions
|
||||
const newWebDisplayOptions = {} as WebDisplayOptions
|
||||
|
||||
const currentSortBy = this.displayOptions.sortBy
|
||||
|
||||
let sortBy = this.application.getPreference(PrefKey.SortNotesBy, CollectionSort.CreatedAt)
|
||||
@@ -326,42 +332,47 @@ export class NotesViewState extends AbstractState {
|
||||
sortBy = CollectionSort.UpdatedAt
|
||||
}
|
||||
|
||||
freshDisplayOptions.sortBy = sortBy
|
||||
freshDisplayOptions.sortReverse = this.application.getPreference(PrefKey.SortNotesReverse, false)
|
||||
freshDisplayOptions.showArchived = this.application.getPreference(PrefKey.NotesShowArchived, false)
|
||||
freshDisplayOptions.showTrashed = this.application.getPreference(PrefKey.NotesShowTrashed, false) as boolean
|
||||
freshDisplayOptions.hidePinned = this.application.getPreference(PrefKey.NotesHidePinned, false)
|
||||
freshDisplayOptions.hideProtected = this.application.getPreference(PrefKey.NotesHideProtected, false)
|
||||
freshDisplayOptions.hideNotePreview = this.application.getPreference(PrefKey.NotesHideNotePreview, false)
|
||||
freshDisplayOptions.hideDate = this.application.getPreference(PrefKey.NotesHideDate, false)
|
||||
freshDisplayOptions.hideTags = this.application.getPreference(PrefKey.NotesHideTags, true)
|
||||
freshDisplayOptions.hideEditorIcon = this.application.getPreference(PrefKey.NotesHideEditorIcon, false)
|
||||
newDisplayOptions.sortBy = sortBy
|
||||
newDisplayOptions.sortDirection =
|
||||
this.application.getPreference(PrefKey.SortNotesReverse, false) === false ? 'dsc' : 'asc'
|
||||
newDisplayOptions.includeArchived = this.application.getPreference(PrefKey.NotesShowArchived, false)
|
||||
newDisplayOptions.includeTrashed = this.application.getPreference(PrefKey.NotesShowTrashed, false) as boolean
|
||||
newDisplayOptions.includePinned = !this.application.getPreference(PrefKey.NotesHidePinned, false)
|
||||
newDisplayOptions.includeProtected = !this.application.getPreference(PrefKey.NotesHideProtected, false)
|
||||
|
||||
newWebDisplayOptions.hideNotePreview = this.application.getPreference(PrefKey.NotesHideNotePreview, false)
|
||||
newWebDisplayOptions.hideDate = this.application.getPreference(PrefKey.NotesHideDate, false)
|
||||
newWebDisplayOptions.hideTags = this.application.getPreference(PrefKey.NotesHideTags, true)
|
||||
newWebDisplayOptions.hideEditorIcon = this.application.getPreference(PrefKey.NotesHideEditorIcon, false)
|
||||
|
||||
const displayOptionsChanged =
|
||||
freshDisplayOptions.sortBy !== this.displayOptions.sortBy ||
|
||||
freshDisplayOptions.sortReverse !== this.displayOptions.sortReverse ||
|
||||
freshDisplayOptions.hidePinned !== this.displayOptions.hidePinned ||
|
||||
freshDisplayOptions.showArchived !== this.displayOptions.showArchived ||
|
||||
freshDisplayOptions.showTrashed !== this.displayOptions.showTrashed ||
|
||||
freshDisplayOptions.hideProtected !== this.displayOptions.hideProtected ||
|
||||
freshDisplayOptions.hideEditorIcon !== this.displayOptions.hideEditorIcon ||
|
||||
freshDisplayOptions.hideTags !== this.displayOptions.hideTags
|
||||
newDisplayOptions.sortBy !== this.displayOptions.sortBy ||
|
||||
newDisplayOptions.sortDirection !== this.displayOptions.sortDirection ||
|
||||
newDisplayOptions.includePinned !== this.displayOptions.includePinned ||
|
||||
newDisplayOptions.includeArchived !== this.displayOptions.includeArchived ||
|
||||
newDisplayOptions.includeTrashed !== this.displayOptions.includeTrashed ||
|
||||
newDisplayOptions.includeProtected !== this.displayOptions.includeProtected ||
|
||||
newWebDisplayOptions.hideNotePreview !== this.webDisplayOptions.hideNotePreview ||
|
||||
newWebDisplayOptions.hideDate !== this.webDisplayOptions.hideDate ||
|
||||
newWebDisplayOptions.hideEditorIcon !== this.webDisplayOptions.hideEditorIcon ||
|
||||
newWebDisplayOptions.hideTags !== this.webDisplayOptions.hideTags
|
||||
|
||||
this.displayOptions = freshDisplayOptions
|
||||
this.displayOptions = newDisplayOptions
|
||||
this.webDisplayOptions = newWebDisplayOptions
|
||||
|
||||
if (displayOptionsChanged) {
|
||||
this.reloadNotesDisplayOptions()
|
||||
}
|
||||
|
||||
await this.reloadNotes()
|
||||
await this.reloadItems()
|
||||
|
||||
const width = this.application.getPreference(PrefKey.NotesPanelWidth)
|
||||
if (width) {
|
||||
this.panelWidth = width
|
||||
}
|
||||
|
||||
if (freshDisplayOptions.sortBy !== currentSortBy) {
|
||||
await this.selectFirstNote()
|
||||
if (newDisplayOptions.sortBy !== currentSortBy) {
|
||||
await this.selectFirstItem()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -389,6 +400,7 @@ export class NotesViewState extends AbstractState {
|
||||
|
||||
get optionsSubtitle(): string {
|
||||
let base = ''
|
||||
|
||||
if (this.displayOptions.sortBy === CollectionSort.CreatedAt) {
|
||||
base += ' Date Added'
|
||||
} else if (this.displayOptions.sortBy === CollectionSort.UpdatedAt) {
|
||||
@@ -396,28 +408,34 @@ export class NotesViewState extends AbstractState {
|
||||
} else if (this.displayOptions.sortBy === CollectionSort.Title) {
|
||||
base += ' Title'
|
||||
}
|
||||
if (this.displayOptions.showArchived) {
|
||||
|
||||
if (this.displayOptions.includeArchived) {
|
||||
base += ' | + Archived'
|
||||
}
|
||||
if (this.displayOptions.showTrashed) {
|
||||
|
||||
if (this.displayOptions.includeTrashed) {
|
||||
base += ' | + Trashed'
|
||||
}
|
||||
if (this.displayOptions.hidePinned) {
|
||||
|
||||
if (!this.displayOptions.includePinned) {
|
||||
base += ' | – Pinned'
|
||||
}
|
||||
if (this.displayOptions.hideProtected) {
|
||||
|
||||
if (!this.displayOptions.includeProtected) {
|
||||
base += ' | – Protected'
|
||||
}
|
||||
if (this.displayOptions.sortReverse) {
|
||||
|
||||
if (this.displayOptions.sortDirection === 'asc') {
|
||||
base += ' | Reversed'
|
||||
}
|
||||
|
||||
return base
|
||||
}
|
||||
|
||||
paginate = () => {
|
||||
this.notesToDisplay += this.pageSize
|
||||
|
||||
void this.reloadNotes()
|
||||
void this.reloadItems()
|
||||
|
||||
if (this.searchSubmitted) {
|
||||
this.application.getDesktopService()?.searchText(this.noteFilterText)
|
||||
@@ -426,9 +444,9 @@ export class NotesViewState extends AbstractState {
|
||||
|
||||
resetPagination = (keepCurrentIfLarger = false) => {
|
||||
const clientHeight = document.documentElement.clientHeight
|
||||
this.pageSize = Math.ceil(clientHeight / MIN_NOTE_CELL_HEIGHT)
|
||||
this.pageSize = Math.ceil(clientHeight / MinNoteCellHeight)
|
||||
if (this.pageSize === 0) {
|
||||
this.pageSize = DEFAULT_LIST_NUM_NOTES
|
||||
this.pageSize = DefaultListNumNotes
|
||||
}
|
||||
if (keepCurrentIfLarger && this.notesToDisplay > this.pageSize) {
|
||||
return
|
||||
@@ -436,60 +454,64 @@ export class NotesViewState extends AbstractState {
|
||||
this.notesToDisplay = this.pageSize
|
||||
}
|
||||
|
||||
getFirstNonProtectedNote = () => {
|
||||
return this.notes.find((note) => !note.protected)
|
||||
getFirstNonProtectedItem = () => {
|
||||
return this.items.find((item) => !item.protected)
|
||||
}
|
||||
|
||||
get notesListScrollContainer() {
|
||||
return document.getElementById(ELEMENT_ID_SCROLL_CONTAINER)
|
||||
return document.getElementById(ElementIdScrollContainer)
|
||||
}
|
||||
|
||||
selectNoteWithScrollHandling = async (
|
||||
note: SNNote,
|
||||
userTriggered?: boolean,
|
||||
scrollIntoView = true,
|
||||
selectItemWithScrollHandling = async (
|
||||
item: {
|
||||
uuid: ListableContentItem['uuid']
|
||||
},
|
||||
{ userTriggered = false, scrollIntoView = true },
|
||||
): Promise<void> => {
|
||||
await this.appState.notes.selectNote(note.uuid, userTriggered)
|
||||
await this.appState.selectedItems.selectItem(item.uuid, userTriggered)
|
||||
|
||||
if (scrollIntoView) {
|
||||
const noteElement = document.getElementById(`note-${note.uuid}`)
|
||||
noteElement?.scrollIntoView({
|
||||
const itemElement = document.getElementById(item.uuid)
|
||||
itemElement?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
selectFirstNote = async () => {
|
||||
const note = this.getFirstNonProtectedNote()
|
||||
selectFirstItem = async () => {
|
||||
const item = this.getFirstNonProtectedItem()
|
||||
|
||||
if (note) {
|
||||
await this.selectNoteWithScrollHandling(note, false, false)
|
||||
if (item) {
|
||||
await this.selectItemWithScrollHandling(item, {
|
||||
userTriggered: false,
|
||||
scrollIntoView: false,
|
||||
})
|
||||
|
||||
this.resetScrollPosition()
|
||||
}
|
||||
}
|
||||
|
||||
selectNextNote = () => {
|
||||
const displayableNotes = this.notes
|
||||
selectNextItem = () => {
|
||||
const displayableItems = this.items
|
||||
|
||||
const currentIndex = displayableNotes.findIndex((candidate) => {
|
||||
return candidate.uuid === this.activeControllerNote?.uuid
|
||||
const currentIndex = displayableItems.findIndex((candidate) => {
|
||||
return candidate.uuid === this.appState.selectedItems.lastSelectedItem?.uuid
|
||||
})
|
||||
|
||||
let nextIndex = currentIndex + 1
|
||||
|
||||
while (nextIndex < displayableNotes.length) {
|
||||
const nextNote = displayableNotes[nextIndex]
|
||||
while (nextIndex < displayableItems.length) {
|
||||
const nextItem = displayableItems[nextIndex]
|
||||
|
||||
nextIndex++
|
||||
|
||||
if (nextNote.protected) {
|
||||
if (nextItem.protected) {
|
||||
continue
|
||||
}
|
||||
|
||||
this.selectNoteWithScrollHandling(nextNote).catch(console.error)
|
||||
this.selectItemWithScrollHandling(nextItem, { userTriggered: true }).catch(console.error)
|
||||
|
||||
const nextNoteElement = document.getElementById(`note-${nextNote.uuid}`)
|
||||
const nextNoteElement = document.getElementById(nextItem.uuid)
|
||||
|
||||
nextNoteElement?.focus()
|
||||
|
||||
@@ -497,39 +519,42 @@ export class NotesViewState extends AbstractState {
|
||||
}
|
||||
}
|
||||
|
||||
selectNextOrCreateNew = async () => {
|
||||
const note = this.getFirstNonProtectedNote()
|
||||
selectNextItemOrCreateNewNote = async () => {
|
||||
const item = this.getFirstNonProtectedItem()
|
||||
|
||||
if (note) {
|
||||
await this.selectNoteWithScrollHandling(note, false, false).catch(console.error)
|
||||
if (item) {
|
||||
await this.selectItemWithScrollHandling(item, {
|
||||
userTriggered: false,
|
||||
scrollIntoView: false,
|
||||
}).catch(console.error)
|
||||
} else {
|
||||
await this.createNewNote()
|
||||
}
|
||||
}
|
||||
|
||||
selectPreviousNote = () => {
|
||||
const displayableNotes = this.notes
|
||||
selectPreviousItem = () => {
|
||||
const displayableItems = this.items
|
||||
|
||||
if (!this.activeControllerNote) {
|
||||
if (!this.appState.selectedItems.lastSelectedItem) {
|
||||
return
|
||||
}
|
||||
|
||||
const currentIndex = displayableNotes.indexOf(this.activeControllerNote)
|
||||
const currentIndex = displayableItems.indexOf(this.appState.selectedItems.lastSelectedItem)
|
||||
|
||||
let previousIndex = currentIndex - 1
|
||||
|
||||
while (previousIndex >= 0) {
|
||||
const previousNote = displayableNotes[previousIndex]
|
||||
const previousItem = displayableItems[previousIndex]
|
||||
|
||||
previousIndex--
|
||||
|
||||
if (previousNote.protected) {
|
||||
if (previousItem.protected) {
|
||||
continue
|
||||
}
|
||||
|
||||
this.selectNoteWithScrollHandling(previousNote).catch(console.error)
|
||||
this.selectItemWithScrollHandling(previousItem, { userTriggered: true }).catch(console.error)
|
||||
|
||||
const previousNoteElement = document.getElementById(`note-${previousNote.uuid}`)
|
||||
const previousNoteElement = document.getElementById(previousItem.uuid)
|
||||
|
||||
previousNoteElement?.focus()
|
||||
|
||||
@@ -591,7 +616,7 @@ export class NotesViewState extends AbstractState {
|
||||
|
||||
this.reloadNotesDisplayOptions()
|
||||
|
||||
void this.reloadNotes()
|
||||
void this.reloadItems()
|
||||
}
|
||||
|
||||
onFilterEnter = () => {
|
||||
@@ -624,7 +649,7 @@ export class NotesViewState extends AbstractState {
|
||||
|
||||
this.reloadNotesDisplayOptions()
|
||||
|
||||
void this.reloadNotes()
|
||||
void this.reloadItems()
|
||||
}
|
||||
|
||||
clearFilterText = () => {
|
||||
@@ -1,4 +1,10 @@
|
||||
import {
|
||||
PopoverFileItemAction,
|
||||
PopoverFileItemActionType,
|
||||
} from '@/Components/AttachedFilesPopover/PopoverFileItemAction'
|
||||
import { PopoverTabs } from '@/Components/AttachedFilesPopover/PopoverTabs'
|
||||
import { BYTES_IN_ONE_MEGABYTE } from '@/Constants'
|
||||
import { confirmDialog } from '@/Services/AlertService'
|
||||
import { concatenateUint8Arrays } from '@/Utils/ConcatenateUint8Arrays'
|
||||
import {
|
||||
ClassicFileReader,
|
||||
@@ -7,11 +13,202 @@ import {
|
||||
ClassicFileSaver,
|
||||
parseFileName,
|
||||
} from '@standardnotes/filepicker'
|
||||
import { ClientDisplayableError, FileItem } from '@standardnotes/snjs'
|
||||
import { ChallengeReason, ClientDisplayableError, ContentType, FileItem } from '@standardnotes/snjs'
|
||||
import { addToast, dismissToast, ToastType, updateToast } from '@standardnotes/stylekit'
|
||||
import { action, computed, makeObservable, observable, reaction } from 'mobx'
|
||||
import { WebApplication } from '../Application'
|
||||
import { AbstractState } from './AbstractState'
|
||||
import { AppState } from './AppState'
|
||||
|
||||
const UnprotectedFileActions = [PopoverFileItemActionType.ToggleFileProtection]
|
||||
const NonMutatingFileActions = [PopoverFileItemActionType.DownloadFile, PopoverFileItemActionType.PreviewFile]
|
||||
|
||||
type FileContextMenuLocation = { x: number; y: number }
|
||||
|
||||
export class FilesState extends AbstractState {
|
||||
allFiles: FileItem[] = []
|
||||
attachedFiles: FileItem[] = []
|
||||
showFileContextMenu = false
|
||||
fileContextMenuLocation: FileContextMenuLocation = { x: 0, y: 0 }
|
||||
|
||||
constructor(application: WebApplication, override appState: AppState, appObservers: (() => void)[]) {
|
||||
super(application, appState)
|
||||
|
||||
makeObservable(this, {
|
||||
allFiles: observable,
|
||||
attachedFiles: observable,
|
||||
showFileContextMenu: observable,
|
||||
fileContextMenuLocation: observable,
|
||||
|
||||
selectedFiles: computed,
|
||||
|
||||
reloadAllFiles: action,
|
||||
reloadAttachedFiles: action,
|
||||
setShowFileContextMenu: action,
|
||||
setFileContextMenuLocation: action,
|
||||
})
|
||||
|
||||
appObservers.push(
|
||||
application.streamItems(ContentType.File, () => {
|
||||
this.reloadAllFiles()
|
||||
this.reloadAttachedFiles()
|
||||
}),
|
||||
reaction(
|
||||
() => appState.notes.selectedNotes,
|
||||
() => {
|
||||
this.reloadAttachedFiles()
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
get selectedFiles() {
|
||||
return this.appState.selectedItems.getSelectedItems<FileItem>(ContentType.File)
|
||||
}
|
||||
|
||||
setShowFileContextMenu = (enabled: boolean) => {
|
||||
this.showFileContextMenu = enabled
|
||||
}
|
||||
|
||||
setFileContextMenuLocation = (location: FileContextMenuLocation) => {
|
||||
this.fileContextMenuLocation = location
|
||||
}
|
||||
|
||||
reloadAllFiles = () => {
|
||||
this.allFiles = this.application.items.getDisplayableFiles()
|
||||
}
|
||||
|
||||
reloadAttachedFiles = () => {
|
||||
const note = this.appState.notes.firstSelectedNote
|
||||
if (note) {
|
||||
this.attachedFiles = this.application.items.getFilesForNote(note)
|
||||
}
|
||||
}
|
||||
|
||||
deleteFile = async (file: FileItem) => {
|
||||
const shouldDelete = await confirmDialog({
|
||||
text: `Are you sure you want to permanently delete "${file.name}"?`,
|
||||
confirmButtonStyle: 'danger',
|
||||
})
|
||||
if (shouldDelete) {
|
||||
const deletingToastId = addToast({
|
||||
type: ToastType.Loading,
|
||||
message: `Deleting file "${file.name}"...`,
|
||||
})
|
||||
await this.application.files.deleteFile(file)
|
||||
addToast({
|
||||
type: ToastType.Success,
|
||||
message: `Deleted file "${file.name}"`,
|
||||
})
|
||||
dismissToast(deletingToastId)
|
||||
}
|
||||
}
|
||||
|
||||
attachFileToNote = async (file: FileItem) => {
|
||||
const note = this.appState.notes.firstSelectedNote
|
||||
if (!note) {
|
||||
addToast({
|
||||
type: ToastType.Error,
|
||||
message: 'Could not attach file because selected note was deleted',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
await this.application.items.associateFileWithNote(file, note)
|
||||
}
|
||||
|
||||
detachFileFromNote = async (file: FileItem) => {
|
||||
const note = this.appState.notes.firstSelectedNote
|
||||
if (!note) {
|
||||
addToast({
|
||||
type: ToastType.Error,
|
||||
message: 'Could not attach file because selected note was deleted',
|
||||
})
|
||||
return
|
||||
}
|
||||
await this.application.items.disassociateFileWithNote(file, note)
|
||||
}
|
||||
|
||||
toggleFileProtection = async (file: FileItem) => {
|
||||
let result: FileItem | undefined
|
||||
if (file.protected) {
|
||||
result = await this.application.mutator.unprotectFile(file)
|
||||
} else {
|
||||
result = await this.application.mutator.protectFile(file)
|
||||
}
|
||||
const isProtected = result ? result.protected : file.protected
|
||||
return isProtected
|
||||
}
|
||||
|
||||
authorizeProtectedActionForFile = async (file: FileItem, challengeReason: ChallengeReason) => {
|
||||
const authorizedFiles = await this.application.protections.authorizeProtectedActionForItems([file], challengeReason)
|
||||
const isAuthorized = authorizedFiles.length > 0 && authorizedFiles.includes(file)
|
||||
return isAuthorized
|
||||
}
|
||||
|
||||
renameFile = async (file: FileItem, fileName: string) => {
|
||||
await this.application.items.renameFile(file, fileName)
|
||||
}
|
||||
|
||||
handleFileAction = async (
|
||||
action: PopoverFileItemAction,
|
||||
currentTab: PopoverTabs,
|
||||
): Promise<{
|
||||
didHandleAction: boolean
|
||||
}> => {
|
||||
const file = action.type !== PopoverFileItemActionType.RenameFile ? action.payload : action.payload.file
|
||||
let isAuthorizedForAction = true
|
||||
|
||||
const requiresAuthorization = file.protected && !UnprotectedFileActions.includes(action.type)
|
||||
|
||||
if (requiresAuthorization) {
|
||||
isAuthorizedForAction = await this.authorizeProtectedActionForFile(file, ChallengeReason.AccessProtectedFile)
|
||||
}
|
||||
|
||||
if (!isAuthorizedForAction) {
|
||||
return {
|
||||
didHandleAction: false,
|
||||
}
|
||||
}
|
||||
|
||||
switch (action.type) {
|
||||
case PopoverFileItemActionType.AttachFileToNote:
|
||||
await this.attachFileToNote(file)
|
||||
break
|
||||
case PopoverFileItemActionType.DetachFileToNote:
|
||||
await this.detachFileFromNote(file)
|
||||
break
|
||||
case PopoverFileItemActionType.DeleteFile:
|
||||
await this.deleteFile(file)
|
||||
break
|
||||
case PopoverFileItemActionType.DownloadFile:
|
||||
await this.downloadFile(file)
|
||||
break
|
||||
case PopoverFileItemActionType.ToggleFileProtection: {
|
||||
const isProtected = await this.toggleFileProtection(file)
|
||||
action.callback(isProtected)
|
||||
break
|
||||
}
|
||||
case PopoverFileItemActionType.RenameFile:
|
||||
await this.renameFile(file, action.payload.name)
|
||||
break
|
||||
case PopoverFileItemActionType.PreviewFile:
|
||||
this.appState.filePreviewModal.activate(
|
||||
file,
|
||||
currentTab === PopoverTabs.AllFiles ? this.allFiles : this.attachedFiles,
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
if (!NonMutatingFileActions.includes(action.type)) {
|
||||
this.application.sync.sync().catch(console.error)
|
||||
}
|
||||
|
||||
return {
|
||||
didHandleAction: true,
|
||||
}
|
||||
}
|
||||
|
||||
public async downloadFile(file: FileItem): Promise<void> {
|
||||
let downloadingToastId = ''
|
||||
|
||||
|
||||
@@ -147,7 +147,7 @@ export class NoteTagsState extends AbstractState {
|
||||
searchActiveNoteAutocompleteTags(): void {
|
||||
const newResults = this.application.items.searchTags(
|
||||
this.autocompleteSearchQuery,
|
||||
this.appState.notesView.activeControllerNote,
|
||||
this.appState.contentListView.activeControllerNote,
|
||||
)
|
||||
this.setAutocompleteTagResults(newResults)
|
||||
}
|
||||
@@ -157,7 +157,7 @@ export class NoteTagsState extends AbstractState {
|
||||
}
|
||||
|
||||
reloadTags(): void {
|
||||
const activeNote = this.appState.notesView.activeControllerNote
|
||||
const activeNote = this.appState.contentListView.activeControllerNote
|
||||
|
||||
if (activeNote) {
|
||||
const tags = this.application.items.getSortedTagsForNote(activeNote)
|
||||
@@ -173,7 +173,7 @@ export class NoteTagsState extends AbstractState {
|
||||
}
|
||||
|
||||
async addTagToActiveNote(tag: SNTag): Promise<void> {
|
||||
const activeNote = this.appState.notesView.activeControllerNote
|
||||
const activeNote = this.appState.contentListView.activeControllerNote
|
||||
|
||||
if (activeNote) {
|
||||
await this.application.items.addTagToNote(activeNote, tag, this.addNoteToParentFolders)
|
||||
@@ -183,7 +183,7 @@ export class NoteTagsState extends AbstractState {
|
||||
}
|
||||
|
||||
async removeTagFromActiveNote(tag: SNTag): Promise<void> {
|
||||
const activeNote = this.appState.notesView.activeControllerNote
|
||||
const activeNote = this.appState.contentListView.activeControllerNote
|
||||
|
||||
if (activeNote) {
|
||||
await this.application.mutator.changeItem(tag, (mutator) => {
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { destroyAllObjectProperties } from '@/Utils'
|
||||
import { confirmDialog } from '@/Services/AlertService'
|
||||
import { KeyboardModifier } from '@/Services/IOService'
|
||||
import { StringEmptyTrash, Strings, StringUtils } from '@/Strings'
|
||||
import { MENU_MARGIN_FROM_APP_BORDER } from '@/Constants'
|
||||
import { UuidString, SNNote, NoteMutator, ContentType, SNTag, ChallengeReason, DeinitSource } from '@standardnotes/snjs'
|
||||
import { SNNote, NoteMutator, ContentType, SNTag, DeinitSource, TagMutator } from '@standardnotes/snjs'
|
||||
import { makeObservable, observable, action, computed, runInAction } from 'mobx'
|
||||
import { WebApplication } from '../Application'
|
||||
import { AppState } from './AppState'
|
||||
@@ -11,7 +10,6 @@ import { AbstractState } from './AbstractState'
|
||||
|
||||
export class NotesState extends AbstractState {
|
||||
lastSelectedNote: SNNote | undefined
|
||||
selectedNotes: Record<UuidString, SNNote> = {}
|
||||
contextMenuOpen = false
|
||||
contextMenuPosition: { top?: number; left: number; bottom?: number } = {
|
||||
top: 0,
|
||||
@@ -25,7 +23,6 @@ export class NotesState extends AbstractState {
|
||||
override deinit(source: DeinitSource) {
|
||||
super.deinit(source)
|
||||
;(this.lastSelectedNote as unknown) = undefined
|
||||
;(this.selectedNotes as unknown) = undefined
|
||||
;(this.onActiveEditorChanged as unknown) = undefined
|
||||
|
||||
destroyAllObjectProperties(this)
|
||||
@@ -40,12 +37,13 @@ export class NotesState extends AbstractState {
|
||||
super(application, appState)
|
||||
|
||||
makeObservable(this, {
|
||||
selectedNotes: observable,
|
||||
contextMenuOpen: observable,
|
||||
contextMenuPosition: observable,
|
||||
showProtectedWarning: observable,
|
||||
showRevisionHistoryModal: observable,
|
||||
|
||||
selectedNotes: computed,
|
||||
firstSelectedNote: computed,
|
||||
selectedNotesCount: computed,
|
||||
trashedNotesCount: computed,
|
||||
|
||||
@@ -89,6 +87,14 @@ export class NotesState extends AbstractState {
|
||||
)
|
||||
}
|
||||
|
||||
get selectedNotes() {
|
||||
return this.appState.selectedItems.getSelectedItems<SNNote>(ContentType.Note)
|
||||
}
|
||||
|
||||
get firstSelectedNote(): SNNote | undefined {
|
||||
return Object.values(this.selectedNotes)[0]
|
||||
}
|
||||
|
||||
get selectedNotesCount(): number {
|
||||
if (this.dealloced) {
|
||||
return 0
|
||||
@@ -101,79 +107,8 @@ export class NotesState extends AbstractState {
|
||||
return this.application.items.trashedItems.length
|
||||
}
|
||||
|
||||
private async selectNotesRange(selectedNote: SNNote): Promise<void> {
|
||||
const notes = this.application.items.getDisplayableNotes()
|
||||
|
||||
const lastSelectedNoteIndex = notes.findIndex((note) => note.uuid == this.lastSelectedNote?.uuid)
|
||||
const selectedNoteIndex = notes.findIndex((note) => note.uuid == selectedNote.uuid)
|
||||
|
||||
let notesToSelect = []
|
||||
if (selectedNoteIndex > lastSelectedNoteIndex) {
|
||||
notesToSelect = notes.slice(lastSelectedNoteIndex, selectedNoteIndex + 1)
|
||||
} else {
|
||||
notesToSelect = notes.slice(selectedNoteIndex, lastSelectedNoteIndex + 1)
|
||||
}
|
||||
|
||||
const authorizedNotes = await this.application.authorizeProtectedActionForNotes(
|
||||
notesToSelect,
|
||||
ChallengeReason.SelectProtectedNote,
|
||||
)
|
||||
|
||||
for (const note of authorizedNotes) {
|
||||
runInAction(() => {
|
||||
this.selectedNotes[note.uuid] = note
|
||||
this.lastSelectedNote = note
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async selectNote(uuid: UuidString, userTriggered?: boolean): Promise<void> {
|
||||
const note = this.application.items.findItem(uuid) as SNNote
|
||||
if (!note) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.selectedNotes[uuid]) {
|
||||
return
|
||||
}
|
||||
|
||||
const hasMeta = this.io.activeModifiers.has(KeyboardModifier.Meta)
|
||||
const hasCtrl = this.io.activeModifiers.has(KeyboardModifier.Ctrl)
|
||||
const hasShift = this.io.activeModifiers.has(KeyboardModifier.Shift)
|
||||
|
||||
const isMultipleSelectSingle = userTriggered && (hasMeta || hasCtrl)
|
||||
const isMultipleSelectRange = userTriggered && hasShift
|
||||
|
||||
if (isMultipleSelectSingle) {
|
||||
if (this.selectedNotes[uuid]) {
|
||||
delete this.selectedNotes[uuid]
|
||||
} else if (await this.application.authorizeNoteAccess(note)) {
|
||||
runInAction(() => {
|
||||
this.selectedNotes[uuid] = note
|
||||
this.lastSelectedNote = note
|
||||
})
|
||||
}
|
||||
} else if (isMultipleSelectRange) {
|
||||
await this.selectNotesRange(note)
|
||||
} else {
|
||||
const shouldSelectNote = this.selectedNotesCount > 1 || !this.selectedNotes[uuid]
|
||||
if (shouldSelectNote && (await this.application.authorizeNoteAccess(note))) {
|
||||
runInAction(() => {
|
||||
this.selectedNotes = {
|
||||
[note.uuid]: note,
|
||||
}
|
||||
this.lastSelectedNote = note
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (this.selectedNotesCount === 1) {
|
||||
await this.openNote(Object.keys(this.selectedNotes)[0])
|
||||
}
|
||||
}
|
||||
|
||||
private async openNote(noteUuid: string): Promise<void> {
|
||||
if (this.appState.notesView.activeControllerNote?.uuid === noteUuid) {
|
||||
async openNote(noteUuid: string): Promise<void> {
|
||||
if (this.appState.contentListView.activeControllerNote?.uuid === noteUuid) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -359,7 +294,7 @@ export class NotesState extends AbstractState {
|
||||
})
|
||||
|
||||
runInAction(() => {
|
||||
this.selectedNotes = {}
|
||||
this.appState.selectedItems.setSelectedItems({})
|
||||
this.contextMenuOpen = false
|
||||
})
|
||||
}
|
||||
@@ -376,7 +311,7 @@ export class NotesState extends AbstractState {
|
||||
}
|
||||
|
||||
unselectNotes(): void {
|
||||
this.selectedNotes = {}
|
||||
this.appState.selectedItems.setSelectedItems({})
|
||||
}
|
||||
|
||||
getSpellcheckStateForNote(note: SNNote) {
|
||||
@@ -400,9 +335,9 @@ export class NotesState extends AbstractState {
|
||||
const tagsToAdd = [...parentChainTags, tag]
|
||||
await Promise.all(
|
||||
tagsToAdd.map(async (tag) => {
|
||||
await this.application.mutator.changeItem(tag, (mutator) => {
|
||||
await this.application.mutator.changeItem<TagMutator>(tag, (mutator) => {
|
||||
for (const note of selectedNotes) {
|
||||
mutator.addItemAsRelationship(note)
|
||||
mutator.addNote(note)
|
||||
}
|
||||
})
|
||||
}),
|
||||
@@ -422,7 +357,7 @@ export class NotesState extends AbstractState {
|
||||
|
||||
isTagInSelectedNotes(tag: SNTag): boolean {
|
||||
const selectedNotes = this.getSelectedNotesList()
|
||||
return selectedNotes.every((note) => this.appState.getNoteTags(note).find((noteTag) => noteTag.uuid === tag.uuid))
|
||||
return selectedNotes.every((note) => this.appState.getItemTags(note).find((noteTag) => noteTag.uuid === tag.uuid))
|
||||
}
|
||||
|
||||
setShowProtectedWarning(show: boolean): void {
|
||||
@@ -445,10 +380,6 @@ export class NotesState extends AbstractState {
|
||||
return Object.values(this.selectedNotes)
|
||||
}
|
||||
|
||||
private get io() {
|
||||
return this.application.io
|
||||
}
|
||||
|
||||
setShowRevisionHistoryModal(show: boolean): void {
|
||||
this.showRevisionHistoryModal = show
|
||||
}
|
||||
|
||||
140
app/assets/javascripts/UIModels/AppState/SelectedItemsState.ts
Normal file
140
app/assets/javascripts/UIModels/AppState/SelectedItemsState.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import { ListableContentItem } from '@/Components/ContentListView/Types/ListableContentItem'
|
||||
import { ChallengeReason, ContentType, KeyboardModifier, FileItem, SNNote, UuidString } from '@standardnotes/snjs'
|
||||
import { action, computed, makeObservable, observable, runInAction } from 'mobx'
|
||||
import { WebApplication } from '../Application'
|
||||
import { AbstractState } from './AbstractState'
|
||||
import { AppState } from './AppState'
|
||||
|
||||
type SelectedItems = Record<UuidString, ListableContentItem>
|
||||
|
||||
export class SelectedItemsState extends AbstractState {
|
||||
lastSelectedItem: ListableContentItem | undefined
|
||||
selectedItems: SelectedItems = {}
|
||||
|
||||
constructor(application: WebApplication, override appState: AppState, appObservers: (() => void)[]) {
|
||||
super(application)
|
||||
|
||||
makeObservable(this, {
|
||||
selectedItems: observable,
|
||||
|
||||
selectedItemsCount: computed,
|
||||
|
||||
selectItem: action,
|
||||
setSelectedItems: action,
|
||||
})
|
||||
|
||||
appObservers.push(
|
||||
application.streamItems<SNNote | FileItem>(
|
||||
[ContentType.Note, ContentType.File],
|
||||
({ changed, inserted, removed }) => {
|
||||
runInAction(() => {
|
||||
for (const removedNote of removed) {
|
||||
delete this.selectedItems[removedNote.uuid]
|
||||
}
|
||||
|
||||
for (const item of [...changed, ...inserted]) {
|
||||
if (this.selectedItems[item.uuid]) {
|
||||
this.selectedItems[item.uuid] = item
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private get io() {
|
||||
return this.application.io
|
||||
}
|
||||
|
||||
get selectedItemsCount(): number {
|
||||
return Object.keys(this.selectedItems).length
|
||||
}
|
||||
|
||||
getSelectedItems = <T extends ListableContentItem>(contentType: ContentType) => {
|
||||
const filteredEntries = Object.entries(this.appState.selectedItems.selectedItems).filter(
|
||||
([_, item]) => item.content_type === contentType,
|
||||
) as [UuidString, T][]
|
||||
return Object.fromEntries<T>(filteredEntries)
|
||||
}
|
||||
|
||||
setSelectedItems = (selectedItems: SelectedItems) => {
|
||||
this.selectedItems = selectedItems
|
||||
}
|
||||
|
||||
private selectItemsRange = async (selectedItem: ListableContentItem): Promise<void> => {
|
||||
const items = this.appState.contentListView.renderedItems
|
||||
|
||||
const lastSelectedItemIndex = items.findIndex((item) => item.uuid == this.lastSelectedItem?.uuid)
|
||||
const selectedItemIndex = items.findIndex((item) => item.uuid == selectedItem.uuid)
|
||||
|
||||
let itemsToSelect = []
|
||||
if (selectedItemIndex > lastSelectedItemIndex) {
|
||||
itemsToSelect = items.slice(lastSelectedItemIndex, selectedItemIndex + 1)
|
||||
} else {
|
||||
itemsToSelect = items.slice(selectedItemIndex, lastSelectedItemIndex + 1)
|
||||
}
|
||||
|
||||
const authorizedItems = await this.application.protections.authorizeProtectedActionForItems(
|
||||
itemsToSelect,
|
||||
ChallengeReason.SelectProtectedNote,
|
||||
)
|
||||
|
||||
for (const item of authorizedItems) {
|
||||
runInAction(() => {
|
||||
this.selectedItems[item.uuid] = item
|
||||
this.lastSelectedItem = item
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
selectItem = async (
|
||||
uuid: UuidString,
|
||||
userTriggered?: boolean,
|
||||
): Promise<{
|
||||
didSelect: boolean
|
||||
}> => {
|
||||
const item = this.application.items.findItem<ListableContentItem>(uuid)
|
||||
if (!item) {
|
||||
return {
|
||||
didSelect: false,
|
||||
}
|
||||
}
|
||||
|
||||
const hasMeta = this.io.activeModifiers.has(KeyboardModifier.Meta)
|
||||
const hasCtrl = this.io.activeModifiers.has(KeyboardModifier.Ctrl)
|
||||
const hasShift = this.io.activeModifiers.has(KeyboardModifier.Shift)
|
||||
const hasMoreThanOneSelected = this.selectedItemsCount > 1
|
||||
const isAuthorizedForAccess = await this.application.protections.authorizeItemAccess(item)
|
||||
|
||||
if (userTriggered && (hasMeta || hasCtrl)) {
|
||||
if (this.selectedItems[uuid] && hasMoreThanOneSelected) {
|
||||
delete this.selectedItems[uuid]
|
||||
} else if (isAuthorizedForAccess) {
|
||||
this.selectedItems[uuid] = item
|
||||
this.lastSelectedItem = item
|
||||
}
|
||||
} else if (userTriggered && hasShift) {
|
||||
await this.selectItemsRange(item)
|
||||
} else {
|
||||
const shouldSelectNote = hasMoreThanOneSelected || !this.selectedItems[uuid]
|
||||
if (shouldSelectNote && isAuthorizedForAccess) {
|
||||
this.setSelectedItems({
|
||||
[item.uuid]: item,
|
||||
})
|
||||
this.lastSelectedItem = item
|
||||
}
|
||||
}
|
||||
|
||||
if (this.selectedItemsCount === 1) {
|
||||
const item = Object.values(this.selectedItems)[0]
|
||||
if (item.content_type === ContentType.Note) {
|
||||
await this.appState.notes.openNote(item.uuid)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
didSelect: this.selectedItems[uuid] != undefined,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ type AnyTag = SNTag | SmartView
|
||||
const rootTags = (application: SNApplication): SNTag[] => {
|
||||
const hasNoParent = (tag: SNTag) => !application.items.getTagParent(tag)
|
||||
|
||||
const allTags = application.items.getDisplayableItems<SNTag>(ContentType.Tag)
|
||||
const allTags = application.items.getDisplayableTags()
|
||||
const rootTags = allTags.filter(hasNoParent)
|
||||
|
||||
return rootTags
|
||||
@@ -132,7 +132,7 @@ export class TagsState extends AbstractState {
|
||||
appEventListeners.push(
|
||||
this.application.streamItems([ContentType.Tag, ContentType.SmartView], ({ changed, removed }) => {
|
||||
runInAction(() => {
|
||||
this.tags = this.application.items.getDisplayableItems<SNTag>(ContentType.Tag)
|
||||
this.tags = this.application.items.getDisplayableTags()
|
||||
|
||||
this.smartViews = this.application.items.getSmartViews()
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
export type WebDisplayOptions = {
|
||||
hideTags: boolean
|
||||
hideDate: boolean
|
||||
hideNotePreview: boolean
|
||||
hideEditorIcon: boolean
|
||||
}
|
||||
Reference in New Issue
Block a user