From 8877c42079e0b78fe44a087453b810d2724adecb Mon Sep 17 00:00:00 2001 From: Mo Date: Mon, 23 May 2022 17:01:44 -0500 Subject: [PATCH] feat: ability to cancel multiple selection from UI (#1045) --- .../FileContextMenu/FileContextMenu.tsx | 2 +- .../MultipleSelectedNotes.tsx | 9 +++ .../Components/NotesOptions/NotesOptions.tsx | 2 +- .../PinNoteButton/PinNoteButton.tsx | 2 +- app/assets/javascripts/Services/IOService.ts | 5 ++ .../UIModels/AppState/FilesState.ts | 2 +- .../UIModels/AppState/NotesState.ts | 12 ++-- .../UIModels/AppState/SelectedItemsState.ts | 56 ++++++++++++++++--- app/assets/stylesheets/_theme.scss | 6 +- 9 files changed, 74 insertions(+), 22 deletions(-) diff --git a/app/assets/javascripts/Components/FileContextMenu/FileContextMenu.tsx b/app/assets/javascripts/Components/FileContextMenu/FileContextMenu.tsx index 7c3d6d9b5..ec27d4636 100644 --- a/app/assets/javascripts/Components/FileContextMenu/FileContextMenu.tsx +++ b/app/assets/javascripts/Components/FileContextMenu/FileContextMenu.tsx @@ -27,7 +27,7 @@ export const FileContextMenu: FunctionComponent = observer(({ appState }) const [closeOnBlur] = useCloseOnBlur(contextMenuRef, (open: boolean) => setShowFileContextMenu(open)) useCloseOnClickOutside(contextMenuRef, () => appState.files.setShowFileContextMenu(false)) - const selectedFile = Object.values(selectedFiles)[0] + const selectedFile = selectedFiles[0] if (!showFileContextMenu || !selectedFile) { return null } diff --git a/app/assets/javascripts/Components/MultipleSelectedNotes/MultipleSelectedNotes.tsx b/app/assets/javascripts/Components/MultipleSelectedNotes/MultipleSelectedNotes.tsx index 229c7d7a7..b22577124 100644 --- a/app/assets/javascripts/Components/MultipleSelectedNotes/MultipleSelectedNotes.tsx +++ b/app/assets/javascripts/Components/MultipleSelectedNotes/MultipleSelectedNotes.tsx @@ -4,6 +4,8 @@ import { observer } from 'mobx-react-lite' import { NotesOptionsPanel } from '@/Components/NotesOptions/NotesOptionsPanel' import { WebApplication } from '@/UIModels/Application' import { PinNoteButton } from '@/Components/PinNoteButton/PinNoteButton' +import { Button } from '../Button/Button' +import { useCallback } from 'preact/hooks' type Props = { application: WebApplication @@ -13,6 +15,10 @@ type Props = { export const MultipleSelectedNotes = observer(({ application, appState }: Props) => { const count = appState.notes.selectedNotesCount + const cancelMultipleSelection = useCallback(() => { + appState.selectedItems.cancelMultipleSelection() + }, [appState]) + return (
@@ -28,6 +34,9 @@ export const MultipleSelectedNotes = observer(({ application, appState }: Props)

{count} selected notes

Actions will be performed on all selected notes.

+
) diff --git a/app/assets/javascripts/Components/NotesOptions/NotesOptions.tsx b/app/assets/javascripts/Components/NotesOptions/NotesOptions.tsx index b5e88571c..b6ef31b03 100644 --- a/app/assets/javascripts/Components/NotesOptions/NotesOptions.tsx +++ b/app/assets/javascripts/Components/NotesOptions/NotesOptions.tsx @@ -185,7 +185,7 @@ export const NotesOptions = observer(({ application, appState, closeOnBlur }: No return notesMatchingAttribute.length > notesNotMatchingAttribute.length } - const notes = Object.values(appState.notes.selectedNotes) + const notes = appState.notes.selectedNotes const hidePreviews = toggleOn((note) => note.hidePreview) const locked = toggleOn((note) => note.locked) const protect = toggleOn((note) => note.protected) diff --git a/app/assets/javascripts/Components/PinNoteButton/PinNoteButton.tsx b/app/assets/javascripts/Components/PinNoteButton/PinNoteButton.tsx index bd18546d1..75c5d9929 100644 --- a/app/assets/javascripts/Components/PinNoteButton/PinNoteButton.tsx +++ b/app/assets/javascripts/Components/PinNoteButton/PinNoteButton.tsx @@ -18,7 +18,7 @@ export const PinNoteButton: FunctionComponent = observer( return null } - const notes = Object.values(appState.notes.selectedNotes) + const notes = appState.notes.selectedNotes const pinned = notes.some((note) => note.pinned) const togglePinned = useCallback(async () => { diff --git a/app/assets/javascripts/Services/IOService.ts b/app/assets/javascripts/Services/IOService.ts index 9f79b0f93..cf37f4857 100644 --- a/app/assets/javascripts/Services/IOService.ts +++ b/app/assets/javascripts/Services/IOService.ts @@ -87,9 +87,14 @@ export class IOService { if (!modifier) { return } + this.activeModifiers.delete(modifier) } + public cancelAllKeyboardModifiers = (): void => { + this.activeModifiers.clear() + } + public handleComponentKeyDown = (modifier: KeyboardModifier | undefined): void => { this.addActiveModifier(modifier) } diff --git a/app/assets/javascripts/UIModels/AppState/FilesState.ts b/app/assets/javascripts/UIModels/AppState/FilesState.ts index 7981e444f..6c9aaab28 100644 --- a/app/assets/javascripts/UIModels/AppState/FilesState.ts +++ b/app/assets/javascripts/UIModels/AppState/FilesState.ts @@ -62,7 +62,7 @@ export class FilesState extends AbstractState { ) } - get selectedFiles() { + get selectedFiles(): FileItem[] { return this.appState.selectedItems.getSelectedItems(ContentType.File) } diff --git a/app/assets/javascripts/UIModels/AppState/NotesState.ts b/app/assets/javascripts/UIModels/AppState/NotesState.ts index cff236367..ef56dc478 100644 --- a/app/assets/javascripts/UIModels/AppState/NotesState.ts +++ b/app/assets/javascripts/UIModels/AppState/NotesState.ts @@ -60,12 +60,12 @@ export class NotesState extends AbstractState { application.streamItems(ContentType.Note, ({ changed, inserted, removed }) => { runInAction(() => { for (const removedNote of removed) { - delete this.selectedNotes[removedNote.uuid] + this.appState.selectedItems.deselectItem(removedNote) } for (const note of [...changed, ...inserted]) { - if (this.selectedNotes[note.uuid]) { - this.selectedNotes[note.uuid] = note + if (this.appState.selectedItems.isItemSelected(note)) { + this.appState.selectedItems.updateReferenceOfSelectedItem(note) } } }) @@ -80,14 +80,14 @@ export class NotesState extends AbstractState { for (const selectedId of selectedUuids) { if (!activeNoteUuids.includes(selectedId)) { - delete this.selectedNotes[selectedId] + this.appState.selectedItems.deselectItem({ uuid: selectedId }) } } }), ) } - get selectedNotes() { + public get selectedNotes(): SNNote[] { return this.appState.selectedItems.getSelectedItems(ContentType.Note) } @@ -262,7 +262,7 @@ export class NotesState extends AbstractState { if (permanently) { for (const note of this.getSelectedNotesList()) { await this.application.mutator.deleteItem(note) - delete this.selectedNotes[note.uuid] + this.appState.selectedItems.deselectItem(note) } } else { await this.changeSelectedNotes((mutator) => { diff --git a/app/assets/javascripts/UIModels/AppState/SelectedItemsState.ts b/app/assets/javascripts/UIModels/AppState/SelectedItemsState.ts index f27cd92fc..e311d146f 100644 --- a/app/assets/javascripts/UIModels/AppState/SelectedItemsState.ts +++ b/app/assets/javascripts/UIModels/AppState/SelectedItemsState.ts @@ -51,17 +51,32 @@ export class SelectedItemsState extends AbstractState { return Object.keys(this.selectedItems).length } - getSelectedItems = (contentType: ContentType) => { - const filteredEntries = Object.entries(this.appState.selectedItems.selectedItems).filter( - ([_, item]) => item.content_type === contentType, - ) as [UuidString, T][] - return Object.fromEntries(filteredEntries) + getSelectedItems = (contentType?: ContentType): T[] => { + return Object.values(this.selectedItems).filter((item) => { + return !contentType ? true : item.content_type === contentType + }) as T[] } setSelectedItems = (selectedItems: SelectedItems) => { this.selectedItems = selectedItems } + public deselectItem = (item: { uuid: ListableContentItem['uuid'] }): void => { + delete this.selectedItems[item.uuid] + + if (item.uuid === this.lastSelectedItem?.uuid) { + this.lastSelectedItem = undefined + } + } + + public isItemSelected = (item: ListableContentItem): boolean => { + return this.selectedItems[item.uuid] != undefined + } + + public updateReferenceOfSelectedItem = (item: ListableContentItem): void => { + this.selectedItems[item.uuid] = item + } + private selectItemsRange = async (selectedItem: ListableContentItem): Promise => { const items = this.appState.contentListView.renderedItems @@ -88,6 +103,32 @@ export class SelectedItemsState extends AbstractState { } } + cancelMultipleSelection = () => { + this.io.cancelAllKeyboardModifiers() + + const firstSelectedItem = this.getSelectedItems()[0] + + if (firstSelectedItem) { + this.replaceSelection(firstSelectedItem) + } else { + this.deselectAll() + } + } + + private replaceSelection = (item: ListableContentItem): void => { + this.setSelectedItems({ + [item.uuid]: item, + }) + + this.lastSelectedItem = item + } + + private deselectAll = (): void => { + this.setSelectedItems({}) + + this.lastSelectedItem = undefined + } + selectItem = async ( uuid: UuidString, userTriggered?: boolean, @@ -119,10 +160,7 @@ export class SelectedItemsState extends AbstractState { } else { const shouldSelectNote = hasMoreThanOneSelected || !this.selectedItems[uuid] if (shouldSelectNote && isAuthorizedForAccess) { - this.setSelectedItems({ - [item.uuid]: item, - }) - this.lastSelectedItem = item + this.replaceSelection(item) } } diff --git a/app/assets/stylesheets/_theme.scss b/app/assets/stylesheets/_theme.scss index 87b1056f9..a34a79f84 100644 --- a/app/assets/stylesheets/_theme.scss +++ b/app/assets/stylesheets/_theme.scss @@ -15,17 +15,17 @@ --text-selection-background-color: var(--sn-stylekit-info-color); --note-preview-progress-color: var(--sn-stylekit-info-color); - --note-preview-progress-background-color: var(--sn-stylekit-contrast-background-color); + --note-preview-progress-background-color: var(--sn-stylekit-grey-4-opacity-variant); --note-preview-selected-progress-color: var(--sn-stylekit-secondary-background-color); - --note-preview-selected-progress-background-color: var(--sn-stylekit-secondary-foreground-color); + --note-preview-selected-progress-background-color:var(--sn-stylekit-grey-4-opacity-variant); --items-column-background-color: var(--sn-stylekit-background-color); --items-column-items-background-color: var(--sn-stylekit-background-color); --items-column-border-left-color: var(--sn-stylekit-border-color); --items-column-border-right-color: var(--sn-stylekit-border-color); --items-column-search-background-color: var(--sn-stylekit-contrast-background-color); - --item-cell-selected-background-color: var(--sn-stylekit-grey-5); + --item-cell-selected-background-color: var(--sn-stylekit-contrast-background-color); --item-cell-selected-border-left-color: var(--sn-stylekit-info-color); --navigation-column-background-color: var(--sn-stylekit-secondary-background-color);